Using PowerShell and TFS API to list users in TFS 2010 (and 2012)
Update: Just wanted to let everyone know that this PowerShell script also runs under PowerShell 3.0 and it also works with TFS 2012 Team Explorer.
Update 2: After some training in Powershell, I updated the script below to also be in the form of an advanced function, taking optional parameters and also using TeamProjectPicker in case no parameters are passed. Enjoy!
First and foremost: this is my first post! :-)
Now, let's get down to business. I've been studying PowerShell for a short time and wanted to practice it and I have recently had a lot of requests from clients where they ask me the easiest way to get a "dump" of all the users on all projects in TFS.
When, why not use this as a good example to learn PowerShell and solve this problem?
Let's begin.
You can get a list of all users by using tfssecurity utility. For example: running
tfssecurity /imx "Project Collection Valid Users" /collection: <collectionUrl> will get you something like this:
Which is helpful somewhat, but not quite, since it doesn't group users by group membership.
Brian Harry posted in his blog a good solution to this problem using C#. The output of that code is something similar like this:
This is close to what we want, but the listing is not organized in a hierarchical fashion. Yes, you could fix the code to do it, but we're talking about using PowerShell here. :-)
Anyway, the script in this blog post will not require IT admins to have Visual Studio to compile code or rely on a developer to do it for them. It only requires PowerShell 2.0 (also works with 3.0) and Team Explorer 2012 (also works with 2010 SP1)
The script source code is:
#TFS Powershell script to dump users by groups
#for all team projects (or a specific team project) in a given collection
#
#Author: Marcelo Silva (marcelo.silva@microsoft.com)
#
#Copyright 2013
#
function Get-TFSGroupMembership
{
Param([string] $CollectionUrlParam,
[string[]] $Projects,
[switch] $ShowEmptyGroups)
$identation = 0
$max_call_depth = 30
function write-idented([string]$text)
{
Write-Output $text.PadLeft($text.Length + (6 * $identation))
}
function list_identities ($queryOption,
$tfsIdentity,
$readIdentityOptions
)
{
$identities = $idService.ReadIdentities($tfsIdentity, $queryOption, $readIdentityOptions)
$identation++
foreach($id in $identities)
{
if ($id.IsContainer)
{
if ($id.Members.Count -gt 0)
{
if ($identation -lt $max_call_depth) #Safe number for max call depth
{
write-idented "Group: ", $id.DisplayName
list_identities $queryOption $id.Members $readIdentityOptions
}
else
{
Write-Output "Maximum call depth reached. Moving on to next group or project..."
}
}
else
{
if ($ShowEmptyGroups)
{
write-idented "Group: ", $id.DisplayName
$identation++;
write-idented "-- No users --"
$identation--;
}
}
}
else
{
if ($id.UniqueName) {
write-idented "Member user: ", $id.UniqueName
}
else {
write-idented "Member user: ", $id.DisplayName
}
}
}
$identation--
}
# load the required dlls
Add-Type -AssemblyName "Microsoft.TeamFoundation.Client, Version=11.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a",
"Microsoft.TeamFoundation.Common, Version=11.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a",
"Microsoft.TeamFoundation, Version=11.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
#[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.TeamFoundation.Client")
#[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.TeamFoundation.Common")
#[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.TeamFoundation")
$tfs
$projectList = @()
if ($CollectionUrlParam)
{
#if collection is passed then use it and select all projects
$tfs = [Microsoft.TeamFoundation.Client.TfsTeamProjectCollectionFactory]::GetTeamProjectCollection($CollectionUrlParam)
$cssService = $tfs.GetService("Microsoft.TeamFoundation.Server.ICommonStructureService3")
if ($Projects)
{
#validate project names
foreach ($p in $Projects)
{
try
{
$projectList += $cssService.GetProjectFromName($p)
}
catch
{
Write-Error "Invalid project name: $p"
exit
}
}
}
else
{
$projectList = $cssService.ListAllProjects()
}
}
else
{
#if no collection specified, open project picker to select it via gui
$picker = New-Object Microsoft.TeamFoundation.Client.TeamProjectPicker([Microsoft.TeamFoundation.Client.TeamProjectPickerMode]::MultiProject, $false)
$dialogResult = $picker.ShowDialog()
if ($dialogResult -ne "OK")
{
exit
}
$tfs = $picker.SelectedTeamProjectCollection
$projectList = $picker.SelectedProjects
}
try
{
$tfs.EnsureAuthenticated()
}
catch
{
Write-Error "Error occurred trying to connect to project collection: $_ "
exit 1
}
$idService = $tfs.GetService("Microsoft.TeamFoundation.Framework.Client.IIdentityManagementService")
Write-Output ""
Write-Output "Team project collection: " $CollectionUrlParam
Write-Output ""
Write-Output "Membership information: "
$identation++
foreach($teamProject in $projectList)
{
Write-Output ""
write-idented "Team project: ",$teamProject.Name
foreach($group in $idService.ListApplicationGroups($teamProject.Name,
[Microsoft.TeamFoundation.Framework.Common.ReadIdentityOptions]::TrueSid))
{
list_identities ([Microsoft.TeamFoundation.Framework.Common.MembershipQuery]::Direct) $group.Descriptor ([Microsoft.TeamFoundation.Framework.Common.ReadIdentityOptions]::TrueSid)
}
}
$identation = 1
Write-Output ""
Write-Output "Users that have access to this collection but do not belong to any group:"
Write-Output ""
$validUsersGroup = $idService.ReadIdentities([Microsoft.TeamFoundation.Framework.Common.IdentitySearchFactor]::AccountName,
"Project Collection Valid Users",
[Microsoft.TeamFoundation.Framework.Common.MembershipQuery]::Expanded,
[Microsoft.TeamFoundation.Framework.Common.ReadIdentityOptions]::TrueSid)
foreach($member in $validUsersGroup[0][0].Members)
{
$user = $idService.ReadIdentity($member,
[Microsoft.TeamFoundation.Framework.Common.MembershipQuery]::Expanded,
[Microsoft.TeamFoundation.Framework.Common.ReadIdentityOptions]::TrueSid)
if ($user.MemberOf.Count -eq 1 -and -not $user.IsContainer)
{
if ($user.UniqueName) {
write-idented "User: ", $user.UniqueName
}
else {
write-idented "User: ", $user.DisplayName
}
}
}
}
There are a couple of points to note about this script:
- The script uses recursion to navigate a tree of group memberships. In my tests, I found that I had to specify a max # of recursions in order to avoid a call depth error in PowerShell.
- The ShowEmptyGroups option can be used to list all groups, even ones that are empty
- You can pass an array of team project names if you want, otherwise it will list all team projects in the given collection
- If you don't pass a team collection URL, it will pop up a dialog so you can select the team project collection and the team projects you want the report for.
- There is a "bonus" piece of code at the end that lists anyone that have permissions set at individual level without belonging to any groups.
You should get an output similar to the one below:
PS C:\Users\marcelos\Documents\TFS PowerShell> . .\TFS-Dump-Group-Membership.ps1
PS C:\Users\marcelos\Documents\TFS PowerShell> Get-TFSGroupMembership https://192.168.1.72:8080/tfs/defaultcollection -ShowEmptyGroups
Team project collection:
https://192.168.1.72:8080/tfs/defaultcollection
Membership information:
Team project: JobsSite
Group: [JobsSite]\Project Administrators
Member user: TFS10RTM\TFSSETUP
Group: [JobsSite]\Contributors
Member user: TFS10RTM\Darren
Member user: TFS10RTM\Nicole
Group: [JobsSite]\Readers
Member user: TFS10RTM\Larry
Group: [JobsSite]\Builders
-- No users --
Team project: TailspinToys
Group: [TailspinToys]\Builders
-- No users --
Group: [TailspinToys]\Readers
-- No users --
Group: [TailspinToys]\Project Administrators
Member user: TFS10RTM\Andrew
Member user: TFS10RTM\TFSSETUP
Group: [TailspinToys]\Contributors
Member user: TFS10RTM\Darren
Member user: TFS10RTM\Larry
Team project: MyApplication
Group: [MyApplication]\Project Administrators
Member user: TFS10RTM\TFSSETUP
Group: [MyApplication]\Contributors
Group: [MyApplication]\DatabaseDevelopers
Member user: TFS10RTM\Andrew
Member user: TFS10RTM\Darren
Group: [MyApplication]\Readers
-- No users --
Group: [MyApplication]\Builders
-- No users --
Group: [MyApplication]\DatabaseDevelopers
Member user: TFS10RTM\Andrew
Member user: TFS10RTM\Darren
Users that have access to this collection but do not belong to any group:
User: TFS10RTM\Renee
PS C:\Users\marcelos\Documents\TFS PowerShell>
There you have it!
Comments
Anonymous
April 01, 2013
The comment has been removedAnonymous
April 12, 2013
Hi Val!What version of Team Explorer you have on your PC? This script will work on Team Explorer 2010 and 2012.It also should work on PowerShell v2 and v3.Also, check out the updated version with better handling on parameters, and it also makes it a function.Can you post the script output? (make sure to mask identities already displayed, your server name and collection/team project names also)Anonymous
July 07, 2013
We need to query work items for particular member from the project.We are not able to connect to TFS 2012 through Powershell.We have installed visual studio 2012 and TFS Power tools and Powershell v2.Kindly let us know if what all tools we need to install at our machine to connect to the TFS.Anonymous
July 10, 2013
Mangesh, all you need is what you have installed: Visual Studio 2012 and PowerShell v2. You don't need the PowerTools but they don't hurt.The connection failure may be due to a problem in your script or maybe permissions issue. Can you post the error msg you get when trying to run the PowerShell script?Anonymous
November 15, 2013
The comment has been removedAnonymous
November 21, 2013
Like George, the script does not do anything for me either. I am using the TFS Power Tools Powershell command prompt. Any ideas?Anonymous
December 12, 2013
If you're running the script and nothing happens, try calling the function directly like:PS C:>Get-TFSGroupMembership 192.168.1.72/.../defaultcollectionI'll update the example since it assumes the script does not have a function.Anonymous
February 14, 2014
The comment has been removedAnonymous
March 11, 2014
Getting this error:Exception calling "ReadIdentities" with "4" argument(s): "An item with the same key has already been added."At C:ScriptsPowershellTFS-Dump-Group-Membership.ps1:155 char:5 $validUsersGroup = $idService.ReadIdentities([Microsoft.TeamFoundation.Fram ...~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [], MethodInvocationException + FullyQualifiedErrorId : TeamFoundationServiceException Cannot index into a null array.At C:ScriptsPowershellTFS-Dump-Group-Membership.ps1:160 char:24 foreach($member in $validUsersGroup[0][0].Members) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidOperation: (:) [], RuntimeException + FullyQualifiedErrorId : NullArrayAnonymous
May 09, 2014
Script worked great. Did get a few errors please see below. Just got to remember to use the . .Scriptname, I almost missed the first dot. :)Exception calling "ReadIdentities" with "3" argument(s): "Server was unable to process request. ---> There was an error generating the XML document. ---> TF20507: The stringargument contains a character that is not valid:'u8236'. Correct the argument, and then try the operation again.Parameter name: value"At C:Network_DrivePowerShellTFSUserList.ps1:28 char:9 $identities = $idService.ReadIdentities($tfsIdentity, $queryOption, $rea ...~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [], MethodInvocationException + FullyQualifiedErrorId : TeamFoundationServiceExceptionAnonymous
January 27, 2015
In the next version of this script, it would be nice to store the output into a PowerShell object instead of plain text. An object will allow information to be extracted by other PowerShell commands very easily.Thanks.- Anonymous
July 20, 2016
When I try to run get-TFSGROUPMEMBERShip it can't find the cmdlet.. I have version 3.0 Powershell where do I get the cmdlet- Anonymous
February 10, 2017
That cmdlet is actually a function described on the script. You have to run the script first so it loads Get-TFSGroupMembership function into memory.
- Anonymous
- Anonymous
Anonymous
September 19, 2016
It's great!!! still useful for TFS2015.Though there is something unimportant error like this:https://social.msdn.microsoft.com/Forums/windowsdesktop/zh-CN/64465c9b-83d4-45e4-a455-231027f83790/how-to-list-all-the-groups-and-their-users-under-one-project?forum=tfsadmin- Anonymous
February 10, 2017
I saw that but not sure why it could not find TFSSecurity. It may have been due to a path issue. I recently had a colleague that used the script to run it on TFS 2015.3, so it should work fine on the latest TFS 2015 as long as you make sure the assemblies loaded are Version 14.0.0
- Anonymous
Anonymous
February 23, 2017
Is there a way to retrieve each user's assigned permissions or a group's permissions? I need to create a "system generated list" that shows users, groups, and their permissions to meet our financial/CM auditing requirements.- Anonymous
February 24, 2017
The comment has been removed
- Anonymous
Anonymous
November 27, 2017
Hi Marcelo,First of all Thank you for this wonderful post! This has been very helpful to me for pulling the list of users and their membership from my TFS installation. I also need to get the Team Administrator name for a TFS Team, could you please help me what updates do I need to make to your script to get that?Thanks,Pawan- Anonymous
February 08, 2018
Hi Pawan. I haven't had time, unfortunately, to update the script to look at team memberships.Maybe in the next month or so, when I publish my "yearly" blog entry :-) I may consider doing it.RgdsMarcelo
- Anonymous
Anonymous
April 11, 2018
Marcelo, thank you so much for this post. I have found it very valuable on multiple occasions.