If you’re developing with the TFS API, you may find yourself in a situation where you need to retrieve a list of all users’ names who belong to a certain Team Project.

This is demonstrated in an application I’m about to post here (not quite done yet), but until that happens, this post will illustrate how to go about doing this.

Retrieving the list of users is not as straight forward as accessing other types of services and data from TFS, such as the WorkItemStore service, a TFS service used to retrieve work items. Most likely you are scratching your head in regards to what Team Foundation service one uses to get the user list, as the action of doing so doesn’t exactly fit under the general definition of any of the common services such as WorkItemStore and IBuildServer.

Because users vary per Team Project (although there are server-wide users), it would appear intuitive to conclude that one uses the VersionControlServer service to get the users. This is incorrect, in fact. A completely separate service is used, one that you may not have heard of before: IGroupSecurityService.

This interface, and the types that derive from it, are somewhat of an exception from the other services (VersionControlServer, IBuildServer), as they do not implement the traditional interface found among these services: ITeamFoundationServerObject, making this service a bit harder for one to discover.

That is interesting, but a moot point nonetheless; to continue, I will explain the process behind getting the users, and then provide some sample code.

Examination of the Group Security Service

User and group information is passed around as instances of the Microsoft.TeamFoundation.Server.Identity class. Each instance contains a wealth of information, such as Security Identifiers, Distinguished Names, and other Active Directory related information. This is nice, but remember that both groups and users can be described by these objects, so we will need to use the IGroupSecurity interface multiple times until we break them down to the individual user level.

In order to filter by Team Project, we use the ListApplicationGroups method of the group security service. This requires passing the Team Project’s absolute artifact URI as a parameter. Calling the ListApplicationGroups method invokes a SOAP call to the server, which is going to end up using a method on the server side of the same name.

Now, I think most people would stop there and continue on with describing how you get users, but that would be boring.

The first thing that happens on the server side when you attempt to retrieve the list of groups for a Team Project, is a check that is made against the user making the call (you) to see if they have Generic Read privileges. So, all requests for security group information made by individuals without Generic Read permissions set for that specific Team Project are denied. If no Team Project is specified, then global TFS permissions are checked instead.

If all checks pass successfully, then the IdentityCacheComponent is accessed and the Team Project URI is passed in. What’s happening at this point is the execution of the prc_security_list_groups stored procedure located in the TfsIntegration database, which will finally grab the data we’re looking for from the tbl_security_identity_cache table.

This will return an Identity array of all the groups that belong to the project. Now, in order to get the identities of a user, we need to query for their Identity based on their SID. This requires getting the SIDs all of the users in the group.

How do we get all of the SIDs of the users who belong to the group? We do that by querying for those SIDs based on the SID of the group.

Using the ReadIdentities method, once again belonging to the IGroupSecurity service, we can do a query using the each group’s SID. Besides passing those two parameters, we’re also required to supply a QueryMembership enumerator type, which can either be: Direct, None, or Expanded. For this we want Expanded, and I’ll explain why in a little bit.

Once again, this requires Generic Read permissions, which are checked as soon as the server gets the request. If that goes well, the array of SIDs we provided get converted to binary SIDs via some unmanaged calls, and then the real work begins.

This is the expensive part of the process (as far as I can tell), and this is where QueryMembers.Expanded has an effect. At this point, a temporary table in a TF database is recreated, and it is filled accordingly with identity information based on the provided SIDs and the specified QueryMembers enumerator. The table population is achieved via the passing of the two parameters through the prc_security_read_identity table. After a bunch of complicated maneuvaring, another Identity array will be sent back to us.

This time, the Identity array contins all the users’ SIDs who belonged to the groups. Nice, but SIDs are SIDs, we need more information about the user, so we need to perform another search, this time using the users’ SIDs as the criteria.

This results in multiple calls to the ReadIdentity method, which returns a single Identity object. Providing the user’s SID along with a QueryMembership of None for each user found in the groups will eventually grant us a nice listing of all the users who are a part of the project.

Sample Code

Here’s some sample code illustrating how we go about doing this:


IGroupSecurityService groupSecurity =
    (IGroupSecurityService)_tfs.GetService(typeof(IGroupSecurityService));

//Retrieve the security group identities based on the Team project's artifact Uri.
Identity[] groups =
    groupSecurity.ListApplicationGroups(teamProject.ArtifactUri.AbsoluteUri);

foreach (Identity group in groups)
{
    //Retrieve the security user SIDs from their respective groups.
    Identity[] groupMembers = groupSecurity.ReadIdentities(
                                            SearchFactor.Sid,
                                            new[] { group.Sid },
                                            QueryMembership.Expanded);

    foreach (Identity member in groupMembers)
    {
        if (member.Members != null)
        {
            foreach (string memberSid in member.Members)
            {
                //We finally gain an individual user's identity based on their SID.
                Identity memberInfo = groupSecurity.ReadIdentity(
                                                    SearchFactor.Sid,
                                                    memberSid,
                                                    QueryMembership.None);

                //In this case, all I care about is the user display name.
                projectUsers.Add(memberInfo.DisplayName);
            }
        }
    }
}

Matt Weber

I'm the founder of Bad Echo LLC, which offers consulting services to clients who need an expert in C#, WPF, Outlook, and other advanced .NET related areas. I enjoy well-designed code, independent thought, and the application of rationality in general. You can reach me at matt@badecho.com.

 Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title="" rel=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

 
   
© 2012-2013 Matt Weber. All Rights Reserved. Terms of Use.