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 http://192.168.1.72:8080/tfs/defaultcollection -ShowEmptyGroups

Team project collection:

http://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 (17)

  1. Val says:

    Hi Marcelo,

    I am new with TFS & Powershwll. We have an Audit requirement & I thought I would give your script a try. I am getting the following error: Cannot find an overload for "ReadIdentities" and the argument count: "3". I am getting the Project names - It occurs right after Member user: the next line has TrueSid and then the error follows.

    Thanks

  2. 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)

  3. Mangesh says:

    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.

  4. 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?

  5. George says:

    Nothing happens when i run the script... Can you add more details on the implementation itself? is it just create the ps1 and execute? does it have to be in a specific folder?

    thx!

  6. DMM91 says:

    Like George, the script does not do anything for me either.  I am using the TFS Power Tools Powershell command prompt.  Any ideas?

  7. Marcelo says:

    If you're running the script and nothing happens, try calling the function directly like:

    PS C:>Get-TFSGroupMembership 192.168.1.72/.../defaultcollection

    I'll update the example since it assumes the script does not have a function.

  8. Imran says:

    Hello All,

    I too had the different problems like nothing happens when the script runs. Below are the changes which I have made to make this program work in my environment.

    1. For some reason the script didn't enter the function for me, so that's why it didn't do nothing when I ran the script. So I have commented the function and made the program start from Parm.
    2. Check the version of Microsoft assembly available in your PC where you run this program. You can check this under C:Windowsassembly. In my case, I have version 10.0.0.0, so changed it in Add-Type section accordingly.

    3. Change the $o to $max_call_depth in the if condition of the comment - #Safe number for max call depth. It's typo in script.

    4. For some reason, it didn't accept the command line parameter for me when I executed this program with the relevant syntax (.Get-TFSGroupMembership.ps1 http://localhost:80/tfs/Test -CollectionUrlParam TestProj or whatever syntax I use as per power shell command line arguments) but it gave me the project picker gui through which I have selected the collection and project and it was accepted by the script and got the output as mentioned By Marcelo.

    or

    you can hard code the collection url if you don't want to use project picker GUI.

    Thanks for the script Marcelo! It was helpful.

  9. Steve L says:

    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)
  10.                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

       + CategoryInfo          : InvalidOperation: (:) [], RuntimeException

       + FullyQualifiedErrorId : NullArray

  • Thane says:

    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 string

    argument 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 : TeamFoundationServiceException

  • Larry says:

    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.

    1. Miike Ingle says:

      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

      1. 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.

    1. 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

  • Richard Anderson says:

    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.

    1. Richard

      I think it's doable, but you have to remember that there are several areas where security can be set in TFS: source control, work items, builds, releases, administration among others.
      The code would have to traverse each area, making it a little more complex endeavor.

  • Skip to main content