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!