How to add licensed users to VS Team Services via the API


NOTE (Sept. 16, 2016): I need to update the solution file to work with VS 2015 and test the code again.

A while back someone asked a question about how to use the API to add licensed users to a VSTS account and change licenses for existing users (instead of the web UI to invite and assign licenses to users). Abhijeet, a dev on my team, gave me some code to get me started, as the identity APIs are not easy to figure out.

Licensing is different on VSTS than it is with Team Foundation Server. With VSTS, every user who is added to an account must have a license assigned. We need to be able to add a new user and also to change the license for an existing user. Let’s take a look at how the code to do this works.

Shared Platform Services

The first thing to notice is that the code is operating on the account that is stored in what we call Shared Platform Services (SPS). SPS is a core set of services, such as identity, account, and profile, that are used by every service in VSTS. The notion of a collection, which is a concept that was introduced in TFS 2010, doesn’t exist in SPS. That’s because not every service in VSTS has to have the notion of a collection. Collection is a TFS concept that is used by version control, work item tracking, etc.

Since we need to make a change to the account, we are going to be talking to SPS. Rather than making calls to https://MyAccount.visualstudio.com as you are used to seeing, we are going to be calling https://MyAccount.vssps.visualstudio.com. The “vssps” part of the URL takes us to SPS, and the account name in the URL gives SPS the context.

You can see SPS in some scenarios if you watch the address bar in your browser. For example, go to http://visualstudio.com and sign in. Then click on your name to go to your profile page. You will see that the URL is https://app.vssps.visualstudio.com/profile/view?mkt=en-us (or you can directly click that URL). In that context, you are asking SPS for your profile and the list of accounts that you have across the system. SPS is responsible for that information.

Once we’ve connected to SPS using the same credentials that you normally use as administrator of your account, we get the client representations of the identity and account services in SPS that we’ll use. The next thing we do is to determine if the user is new or an existing user.

Adding a New User with a License

New users have to be added to the system. We call that process “bind pending.” What we mean by that is that we will add a reference to a user – nothing but the email address. That email address is not bound to anything. Once a user logs into VSTS with an email address that matches a bind-pending entry, we’ll create a full Identity object that references the unique identifier of that user. We’ll also have enter basic info for a profile (e.g., display name) and link that to the identity.

The Identity API calls for this operation are not at all obvious, and this is something we need to improve. I’ve added a lot of comments to the code to help explain what’s going on. At a high level, we first have to construct a descriptor for this bind pending identity. Each account in VSTS is either an MSA-backed account (i.e., it only uses MSAs, also known as LiveIDs) or AAD-backed (i.e., all identities reside in an Azure Active Directory tenant). We make that determination using the properties of the administrative user. Then we first have to add a new user to a group in order for the user to be part of the system – for there to be a reference to the identity. Since every user must have a license, we add the user to the licensed users group.

Changing the License for an Existing User

This case is much simpler. If the user is already in the account, the code just makes a call to set the license with licensingClient.AssignEntitlementAsync.

Notes on Licensing

The names for the licenses match what you’ll find in our documentation for pricing levels except AccountLicense.Express. That’s the name that the code uses for Basic (at one time it was going to be called Express, and the code didn’t get updated). If you look at the AccountLicense enum, you’ll also find AccountLicense.EarlyAdopter. That was only valid until VSTS became GA, so it can no longer be used.

The MSDN benefits license is different than the other licenses because it is dependent upon a user’s MSDN license level. While you could explicitly set the license to a particular MSDN benefits level, you’d only cause yourself problems. Setting it to MsdnLicense.Eligible as the code does below means that the service will handle setting the user’s MSDN benefits level properly upon logging in.

The licensing API right now uses an enum rather than a string for the licenses. The result is that AccountLicense.Stakeholder doesn’t exist in the client API prior to Update 4. You’ll see in the code that I commented out Stakeholder so that it builds with Update 3, which is the latest RTM update at the time this post is being written. In the future the API will allow for a string so that as new license types are added the API will still be able to use them.

There are limits for licenses based on what you have purchased. For example, if you try to add a sixth user licensed for Basic, you will get an error message. Here’s how to add licenses to your VSTS account.

One other thing that I’ll mention is that you may have to search around for the credential dialog prompt. That dialog ends up parented to the desktop, so it’s easy for it to get hidden by another window.

I’ve attached the VS solution to this blog post, in addition to including the code in the post. I recommend downloading the solution, which will build with VS 2013 Update 3 or newer (it may work with earlier versions of 2013, but I didn’t try it).

Enjoy!

Follow me on Twitter at twitter.com/tfsbuck

using System;
using System.Linq;
using Microsoft.TeamFoundation;
using Microsoft.TeamFoundation.Framework.Common;
using Microsoft.VisualStudio.Services.Client;
using Microsoft.VisualStudio.Services.Identity;
using Microsoft.VisualStudio.Services.Identity.Client;
using Microsoft.VisualStudio.Services.Licensing;
using Microsoft.VisualStudio.Services.Licensing.Client;

namespace AddUserToAccount
{
    public class Program
    {
        public static void Main(string[] args)
        {
            if (args.Length == 0)
            {
                // For ease of running from the debugger, hard-code the account and the email address if not supplied
                // The account name here is just the name, not the URL.
                //args = new[] { "Awesome", "example@outlook.com", "basic" };
            }

            if (!Init(args))
            {
                Console.WriteLine("Add a licensed user to a Visual Studio Account");
                Console.WriteLine("Usage: [accountName] [userEmailAddress] <license>");
                Console.WriteLine("  accountName - just the name of the account, not the URL");
                Console.WriteLine("  userEmailAddress - email address of the user to be added");
                Console.WriteLine("  license - optional license (default is Basic): Basic, Professional, or Advanced");
                return;
            }

            AddUserToAccount();
        }

        private static void AddUserToAccount()
        {
            try
            {
                // Create a connection to the specified account.
                // If you change the false to true, your credentials will be saved.
                var creds = new VssClientCredentials(false);
                var vssConnection = new VssConnection(new Uri(VssAccountUrl), creds);

                // We need the clients for two services: Licensing and Identity
                var licensingClient = vssConnection.GetClient<LicensingHttpClient>();
                var identityClient = vssConnection.GetClient<IdentityHttpClient>();

                // The first call is to see if the user already exists in the account.
                // Since this is the first call to the service, this will trigger the sign-in window to pop up.
                Console.WriteLine("Sign in as the admin of account {0}. You will see a sign-in window on the desktop.",
                                  VssAccountName);
                var userIdentity = identityClient.ReadIdentitiesAsync(IdentitySearchFilter.AccountName, 
                                                                      VssUserToAddMailAddress).Result.FirstOrDefault();

                // If the identity is null, this is a user that has not yet been added to the account.
                // We'll need to add the user as a "bind pending" - meaning that the email address of the identity is 
                // recorded so that the user can log into the account, but the rest of the details of the identity 
                // won't be filled in until first login.
                if (userIdentity == null)
                {
                    Console.WriteLine("Creating a new identity and adding it to the collection's licensed users group.");

                    // We are adding the user to a collection, and at the moment only one collection is supported per
                    // account in VSTS.
                    var collectionScope = identityClient.GetScopeAsync(VSSAccountName).Result;

                    // First get the descriptor for the licensed users group, which is a well known (built in) group.
                    var licensedUsersGroupDescriptor = new IdentityDescriptor(IdentityConstants.TeamFoundationType, 
                                                                              GroupWellKnownSidConstants.LicensedUsersGroupSid);

                    // Now convert that into the licensed users group descriptor into a collection scope identifier.
                    var identifier = String.Concat(SidIdentityHelper.GetDomainSid(collectionScope.Id),
                                                   SidIdentityHelper.WellKnownSidType,
                                                   licensedUsersGroupDescriptor.Identifier.Substring(SidIdentityHelper.WellKnownSidPrefix.Length));

                    // Here we take the string representation and create the strongly-type descriptor
                    var collectionLicensedUsersGroupDescriptor = new IdentityDescriptor(IdentityConstants.TeamFoundationType, 
                                                                                        identifier);


                    // Get the domain from the user that runs this code. This domain will then be used to construct
                    // the bind-pending identity. The domain is either going to be "Windows Live ID" or the Azure 
                    // Active Directory (AAD) unique identifier, depending on whether the account is connected to
                    // an AAD tenant. Then we'll format this as a UPN string.
                    var currUserIdentity = vssConnection.AuthorizedIdentity.Descriptor;
                    var directory = "Windows Live ID"; // default to an MSA (fka Live ID)
                    if (currUserIdentity.Identifier.Contains('\\'))
                    {
                        // The identifier is domain\userEmailAddress, which is used by AAD-backed accounts.
                        // We'll extract the domain from the admin user.
                        directory = currUserIdentity.Identifier.Split(new char[] { '\\' })[0];
                    }
                    var upnIdentity = string.Format("upn:{0}\\{1}", directory, VssUserToAddMailAddress);

                    // Next we'll create the identity descriptor for a new "bind pending" user identity.
                    var newUserDesciptor = new IdentityDescriptor(IdentityConstants.BindPendingIdentityType, 
                                                                  upnIdentity);

                    // We are ready to actually create the "bind pending" identity entry. First we have to add the
                    // identity to the collection's licensed users group. Then we'll retrieve the Identity object
                    // for this newly-added user. Without being added to the licensed users group, the identity 
                    // can't exist in the account.
                    bool result = identityClient.AddMemberToGroupAsync(collectionLicensedUsersGroupDescriptor, 
                                                                       newUserDesciptor).Result;
                    userIdentity = identityClient.ReadIdentitiesAsync(IdentitySearchFilter.AccountName, 
                                                                      VssUserToAddMailAddress).Result.FirstOrDefault();
                }

                Console.WriteLine("Assigning license to user.");
                var entitlement = licensingClient.AssignEntitlementAsync(userIdentity.Id, VssLicense).Result;

                Console.WriteLine("Success!");
            }
            catch (Exception e)
            {
                Console.WriteLine("\r\nSomething went wrong...");
                Console.WriteLine(e.Message);
                if (e.InnerException != null)
                {
                    Console.WriteLine(e.InnerException.Message);
                }
            }
        }

        private static bool Init(string[] args)
        {
            if (args == null || args.Length < 2)
            {
                return false;
            }

            if (string.IsNullOrWhiteSpace(args[0]))
            {
                Console.WriteLine("Error: Invalid accountName");
                return false;
            }

            VssAccountName = args[0];

            // We need to talk to SPS in order to add a user and assign a license.
            VssAccountUrl = "https://" + VssAccountName + ".vssps.visualstudio.com/";

            if (string.IsNullOrWhiteSpace(args[1]))
            {
                Console.WriteLine("Error: Invalid userEmailAddress");
                return false;
            }

            VssUserToAddMailAddress = args[1];

            VssLicense = AccountLicense.Express; // default to Basic license
            if (args.Length == 3)
            {
                string license = args[2].ToLowerInvariant();
                switch (license)
                {
                    case "basic":
                        VssLicense = AccountLicense.Express;
                        break;
                    case "professional":
                        VssLicense = AccountLicense.Professional;
                        break;
                    case "advanced":
                        VssLicense = AccountLicense.Advanced;
                        break;
                    case "msdn":
                        // When the user logs in, the system will determine the actual MSDN benefits for the user.
                        VssLicense = MsdnLicense.Eligible;
                        break;
                    // Uncomment the code for Stakeholder if you are using VS 2013 Update 4 or newer.
                    //case "Stakeholder":
                    //    VssLicense = AccountLicense.Stakeholder;
                    //    break;
                    default:
                        Console.WriteLine("Error: License must be Basic, Professional, Advanced, or MSDN");
                        //Console.WriteLine("Error: License must be Stakeholder, Basic, Professional, Advanced, or MSDN");
                        return false;
                }
            }

            return true;
        }

        public static string VssAccountUrl { get; set; }
        public static string VssAccountName { get; set; }
        public static string VssUserToAddMailAddress { get; set; }
        public static License VssLicense { get; set; }
    }
}

AddUserApp.zip

Comments (10)

  1. Betty says:

    Is there a way to get a list of users and what license is assigned? I can only seem to get a count of license types using the LicensingHttpClient and not the actual data.

  2. Buck Hodges says:

    Betty, sorry I missed this question back in December. Here is how to get that info.

               var entitlements = licensingClient.GetAccountEntitlementsAsync().Result;

               var userIds = entitlements.Select(entitlement => entitlement.UserId).ToList();

               var users = identityClient.ReadIdentitiesAsync(userIds).Result.ToDictionary(item => item.Id);

               foreach (var entitlement in entitlements)

               {

                   var user = users[entitlement.UserId];

                   Console.WriteLine("Name: {0}, UserId: {1}, License: {2}.", user.DisplayName, entitlement.UserId, entitlement.License);

               }

  3. I got an exception during the following line.

    Console.WriteLine("Assigning license to user.");

    var entitlement = licensingClient.AssignEntitlementAsync(userIdentity.Id, VssLicense).Result;

    The exception message:

    API resource location d264dfb2-bea8-46f6-9ba5-162536283a5a is not registered on myaccount.vssps.visualstudio.com.

  4. Sreeram N says:

    If you are using VS 2015, the sample will work correctly. For VS 2012 users, there is a bug in the part of the code that assigns a given license to the user (lines 111 & 112) in Program.cs. Please comment out the lines and the rest of the code to add users to the account should work. The user will be auto-assigned the license when they first sign-in (using best available license at the time of sign-in). Account owners can change the license using the User Hub in VSO. My will be fixing the part of the code where license assignment is broken for VS2013 users as soon as possible.

  5. James B says:

    This is exactly the code I was looking for, unfortunately, I get an exception calling AddMemberToGroupAsync

    {“TF14045: The identity with type ‘Microsoft.TeamFoundation.Identity’ and identifier ‘blah blah’ could not be found.”}

    and this after I changed

    var collectionScope = identityClient.GetScopeAsync(“DefaultCollection”).Result;

    to reference my ProjectName instead of “DefaultCollection” as the original code throws an exception of

    TF50620: The Team Foundation identity scope DefaultCollection does not exist

    VS2015 and a AAD backed VSO instance, if that helps.

    1. Buck Hodges says:

      James, I made an update to the code based on what I was told was change that broke it – DefaultCollection was changed to use VssAccountName instead. I haven’t had time to test it, though. I also need to update the attached solution to work with VS 2015. I will bump it up in my list of things to do.

      1. Sam S says:

        I encountered the same errors as James. I’m also using VS2015 and an AAD backed VSO instance.

        I’ve updated the code to use VssAccountName instead of “DefaultCollection” and I’m getting the following error calling AddMemberToGroupAsync:

        Exception thrown: ‘Microsoft.VisualStudio.Services.Identity.IdentityAccountNameAlreadyInUseException’ in mscorlib.dll

        Additional information: TF400815: The identity account name ‘blah’ is already in use.

        1. Daniel Mueller says:

          Same thing here: TF400815: The identity account name ‘user@account’ is already in use.

      2. Hi Buck. We have tried a lot, to get license assignment working with AAD and VSTS. Our best guess at the moment is, that AAD Users are already softbound somehow, because license assignement seems to be working partially, in one scenario we could login with the newly licensed user, but he was never listed in the VSTS Account Users page. Can you direct us to some information, how to continue to make that work as expected?

        1. Daniel, We did some research into the issue you (and others) reported below. We found a bug in the workflow. We have a fix in master that will be released with sprint 114 (about 3 weeks from now). This bug will only occur in the case where an MSA or AAD user is already a member of VSTS, but not a member of the account you are adding them too.

          For the other problem you mentioned. You have a user who is a member of the account but does not show up in the User Hub. That will occur if the user does not have a license. You can assign the license in the UI or using Buck’s code above.

Skip to main content