Introducing TFS Impersonation

One of the many things I will be covering in this blog will be the Team Foundation Server APIs and the functionality they offer.  The first example of this will be the new TFS Impersonation API that is new in TFS 2010.

Disclaimer: The code presented in this blog post assumes you are using the RTM version of TFS. These APIs were refactored between TFS 2010 Beta 2 and TFS 2010 RTM. Please see the section at the end of the post to see how the new API names map to those in the Beta 1 and Beta 2 versions of TFS 2010.

If you are new to the TFS 2010 OM you may want to start with this post which introduces the TfsConnection, TfsConfigurationServer and TfsTeamProjectCollection objects.

What is TFS Impersonation?

TFS Impersonation is a feature that was introduced in TFS 2010 in order to allow a process running as User A to make web service calls to TFS in such a way that TFS thinks the actions are being performed by User B.

This is often useful when writing a tool or service that wants to perform actions in Team Foundation Server and have the server apply permissions and record the action as if a given user had performed it.  For the purposes of this blog post we will assume that this action is the queuing of a build on behalf of another user (perhaps in response to a notification that a CheckIn by this user has just occurred).

Isn’t this already possible through the use of Windows Impersonation?

Well, yes, that is true.  I assume you mean by doing something like this:

 private void QueueBuildForUserUsingWindowsImpersonation(
    Uri teamProjectCollectionUri,
    String teamProjectName,
    String buildDefinitionName,
    String principal, 
    String authority, 
    String password)
{
    IntPtr impersonatedUserToken;
    LogonUser(principal, authority, password, LogonTypes.Interactive, 
            LogonProviders.Default, out impersonatedUserToken);

    WindowsIdentity identity = new WindowsIdentity(impersonatedUserToken);
    using (WindowsImpersonationContext impersonationContext = identity.Impersonate())
    {
        // Create the proxy object so that we can communicate with the Team Project Collection
        TfsTeamProjectCollection teamProjectCollection = 
                new TfsTeamProjectCollection(teamProjectCollectionUri);

        IBuildServer buildServer = teamProjectCollection.GetService<IBuildServer>();
        
        // Find the build definition for the build to queue.
        IBuildDefinition buildDefinition = GetBuildDefinition(buildServer, teamProjectName, 
                buildDefinitionName);

        if (buildDefinition == null)
        {
            WriteToLog("Couldn't find build definition");
            return;
        }
        else
        {
            WriteToLog("Found build definition, queuing now...");
        }

        // Queue the build as our impersonated user.
        buildServer.QueueBuild(buildDefinition.CreateBuildRequest());

        WriteToLog("Build successfully queued");
    }
}

Yes, the code above will in fact queue a build as the user who’s information is passed into the function (principal, authority and password) but this has some caveats. 

  • First and foremost, you must have the user’s username and password in order to call LogonUser.  This data is usually only available through user input and in the case where your tool is automatically queuing builds after CheckIns, it is likely that you will not have this information or will have to do something unsafe to get access to it.
  • This only works for windows identities on windows clients.  With Microsoft’s acquisition of the Teamprise Client Suite it is important that the TFS Client Object Model offers its own method of impersonation instead of relying on the operating system to support it and it is worthwhile to have one standard solution that applies across all operating systems.
  • It means that you use the LogonUser unmanaged method which requires dll importing the advapi32.dll.  LogonUser can be slow, is easy to misuse and requires that you handle a series of error conditions (all of which are not done here).
  • Finally, this also means that your current thread is running as the user that you have impersonated.  If the thread does logging (as above) or other actions that cause disk resources to be created, deleted or modified then the user that is being impersonated will need the appropriate permissions to these resources or methods of delaying these actions until the impersonation has ended will need to be implemented.

Well, what about supplying a different set of credentials to the TfsTeamProjectCollection object that you are creating? Doesn’t that get around some of these problems and still accomplish the main goal?

Well, yes, supplying different credentials to the TfsTeamProjectCollection object will get you around a few of the above caveats.  Let’s look at an example of this:

 private void QueueBuildForUserUsingDifferentCredentials(
    Uri teamProjectCollectionUri,
    String teamProjectName,
    String buildDefinitionName,
    String principal,
    String authority,
    String password)
{
    // Create the proxy object so that we can communicate with the Team Project Collection
    TfsTeamProjectCollection teamProjectCollection =
            new TfsTeamProjectCollection(teamProjectCollectionUri, 
                new NetworkCredential(principal, password, authority));

    IBuildServer buildServer = teamProjectCollection.GetService<IBuildServer>();

    // Find the build definition for the build to queue.
    IBuildDefinition buildDefinition = GetBuildDefinition(buildServer, teamProjectName,
            buildDefinitionName);

    if (buildDefinition == null)
    {
        WriteToLog("Couldn't find build definition");
        return;
    }
    else
    {
        WriteToLog("Found build definition, queuing now...");
    }

    // Queue the build as our impersonated user.
    buildServer.QueueBuild(buildDefinition.CreateBuildRequest());

    WriteToLog("Build successfully queued");
}

In this example we are able to stop using the clunky Windows Impersonation which saves us the use of unmanaged code and DllImports and also prevents the problems with logging mentioned above.  Also, since this is a wholly TFS solution, this solution can be used by Teamprise users as well. 

However, this solution still runs into the main problem that we had above which is that in order to create the NetworkCredential object for the other user that user’s password is needed. 

Ahh, gotcha. So I am guessing this is where TFS Impersonation comes in?

Yes, this is where TFS Impersonation comes in.  The main advantages that TFS Impersonation has over the two solutions above is that it does not require that the user’s password is supplied.

Before we dive into the example, let’s look at the APIs that are related to TFS Impersonation.  Off of any object that derives from the TfsConnection object (currently only TfsTeamProjectCollection and TfsConfigurationServer) there are new constructors that take the IdentityDescriptor of the identity to impersonate:

 public TfsTeamProjectCollection(
    Uri uri,
    IdentityDescriptor identityToImpersonate)

This method (and the other overloads that take the identityToImpersonate parameter) constructs an instance of the TfsTeamProjectCollection object that will make all web requests as the impersonated user.  Also, for the example below you will also need to include the following two namespaces that are new in TFS 2010:

 using Microsoft.TeamFoundation.Framework.Client;
using Microsoft.TeamFoundation.Framework.Common;

Let’s see how our example looks using TFS Impersonation:

 private void QueueBuildForUserUsingTFSImpersonation(
    TfsTeamProjectCollection currentUserCollection,
    String teamProjectName,
    String buildDefinitionName,
    String username)
{
    // Get the TFS Identity Management Service
    IIdentityManagementService identityManagementService =
            currentUserCollection.GetService<IIdentityManagementService>();

    // Look up the user that we want to impersonate
    TeamFoundationIdentity identity = identityManagementService.ReadIdentity(
            IdentitySearchFactor.AccountName, username, MembershipQuery.None, ReadIdentityOptions.None);

    TfsTeamProjectCollection impersonatedCollection = 
            new TfsTeamProjectCollection(currentUserCollection.Uri, identity.Descriptor);
    IBuildServer impersonatedBuildServer = impersonatedCollection.GetService<IBuildServer>();

    // Find the build definition for the build to queue.
    IBuildDefinition buildDefinition = GetBuildDefinition(impersonatedBuildServer, teamProjectName,
            buildDefinitionName);

    if (buildDefinition == null)
    {
        WriteToLog("Couldn't find build definition");
        return;
    }
    else
    {
        WriteToLog("Found build definition, queuing now...");
    }

    // Queue the build as our impersonated user.
    impersonatedBuildServer.QueueBuild(buildDefinition.CreateBuildRequest());

    WriteToLog("Build successfully queued");
}

As you can see in the example, the need for the user’s password is now gone. 

The only thing new to this example is how we discovered the user’s IdentityDescriptor by calling into the Identity Management Service.  This means that in order to use TFS Impersonation either the user’s account name or IdentityDescriptor is needed.  Some of the objects that are new to TFS 2010 will have the TeamFoundationIdentity or IdentityDescriptor objects available as properties so this extra call may not always be needed.  If not, the account name should be discoverable and the IdentityDescriptor can be resolved as it was above.

Wait, doesn’t it seem dangerous that one user can perform actions in Team Foundation Server on behalf of another user without that user’s password?

Yes!  Luckily, not everyone is able to do this.  Since TFS Impersonation actually takes place in the server (see the next section for more of an explanation on this) there is a permission in the server that controls who is and who is not allowed to perform impersonation.  This permission is titled “Make requests on behalf of others” and it can be managed for a Team Project Collection in the TPC security dialog within Team Explorer and it can be managed for the Configuration Server from within the Administer Security dialog off of the Application Tier node in the TfsMgmt console.

This permission is encapsulated within each Team Project Collection and within the Configuration Server.  This means that if User A has this permission on TPC1 he will not be allowed to impersonate users when talking to TPC2 or the Configuration Server.  Similarly, if User B has this permission on the Configuration Server she will not be able impersonate users when talking to any of the Team Project Collections.

Administrators should be very careful when granting this permission.  For the most part, it should only be granted to well-protected service accounts or tool accounts that can be trusted to take actions on behalf of other users.  Because of this, it is one of the few TFS permissions that is not automatically granted to the Configuration Server level or Team Project Collection level Administrators groups. 

Note, the following issue has been fixed in TFS 2010 RTM. I will leave the description up for now since the betas are still heavily being used.

Unfortunately, the concept of not granting a permission to the Administrators group does not mesh well with the current TFS security dialog.  This is because that dialog makes the assumption that the Administrators group already has all of the permissions that it needs and actually does not allow you to change these permissions. 

image

In the above screenshot, the dialog is lying to you.  By default, the Administrators group does not have the “Make requests on behalf of others” permission.  Making things worse is that since the dialog prevents any permissions from being changed for the Administrators group the user is unable to actually give the Administrators group this permission using the UI. 

However, the ability to not set this permission may have a good consequence.  As discussed above, administrators should be very careful when granting this permission.  If it were given to the administrators group then this would mean that any administrator would be able to make a request on behalf of another user.  Now, granted, since they are an administrator they still have the power to grant this permission to them self but at least that takes an explicit action.

If it is decided that this permission needs to be given to the Administrators group then this is possible via the following command from the command-line:

 tfssecurity.exe /collection:https://taylaf-dev:8080/tfs/Collection0 /a+ Server FrameworkGlobalSecurity Impersonate adm: ALLOW

Cool, is there anything else I should know about TFS Impersonation?

Of course!  We should definitely talk about the difference between an authenticated identity and the authorized identity and how they relate to TFS Impersonation.  In any web service call, the authenticated identity is the identity that authenticates with the web server (IIS in this case) and the authorized identity is the identity that the web application applies its internal permission restrictions to and is who it records as “performing” the given action.

In the normal case where impersonation is not being used, the authenticated identity and the authorized identity will always be the same.  For the first two examples in this post (Windows Impersonation and Using Different TFS Credentials) the authenticated and authorized identities for the web service calls will also be the same.  This is because in both of those cases, the web service call is being made by the user being “impersonated”.  Since this is true, both the authenticated and authorized identities refer to this “impersonated” user. 

As you might have guessed, these two identities will be different when TFS Impersonation is used.  When using TFS Impersonation the web service call is still made by the user running the process.  This means that the authenticated identity is the user running the process.  It isn’t until the web request enters the TFS web application and the “Make requests on behalf of others” permission is verified that the request becomes associated with the user being impersonated.  Thus, in this case the authorized identity is the identity that is being impersonated.

These ideas are exposed on the TfsConnection base class using the following properties:

 public TeamFoundationIdentity AuthorizedIdentity { get; }

 public void GetAuthenticatedIdentity(out TeamFoundationIdentity identity);

When there is an active ITeamFoundationImpersonationContext the AuthorizedIdentity property refers to the user being impersonated and the identity returned by GetAuthenticatedIdentity is the user doing the impersonation.

You may be wondering why we chose to implement the authenticated identity accessor as a method with an out parameter instead of a property like we did with AuthorizedIdentity.  The answer is that when a consumer of this API wants to know “who the active user is” in order to display some details about that user they should almost always show information for the user the who the requests are being made on behalf of (the authorized identity).  Because of this and because of how easy it is to confuse the authorized and authenticated identities we actually wanted to make it hard to get the authenticated identity.  Hopefully, this will prevent programmers from accidentally using the authenticated identity when they should be using the authorized identity and will force them to think about their scenario if they do think they should be using the authenticated identity.

Updated! Want to see another example using TFS Impersonation? Check out this example.

Hopefully that should be enough to get you started with TFS Impersonation.  I am sure there will be more posts about it as people ask more questions about it.  Speaking of, if you have questions feel free to ask in the comments or via the contact form.

If you are using TFS 2010 Beta 1 or Beta 2 then everything you just read is still true but some of the APIs have different names. Here are the mappings that you will need to make this work in one of the betas:

TfsTeamProjectCollection is equivalent to TeamFoundationServer.  For more information on this see Grant Holliday's blog post.

In the Beta 2 version of this API you will not find the new constructors that are needed to perform the impersonation.  Instead, you will find a method off of the TeamFoundationServer object called ApplyThreadImpersonationContext().  This should be called immediately after constructing the TfsTeamProjectCollection object in order to do impersonation.