Search code examples
azure-devopsactive-directoryazure-devops-server-2019

Force Azure DevOps Server 2019 to manually sync with ActiveDirectory


We have an on-premise Azure DevOps Server that works with a corporate ActiveDirectory. When adding new users, Azure DevOps Server pulls their information from ActiveDirectory. But the user's information was later updated in ActiveDirectory to fix an issue - their email account was missing.

In the past, I have been able to remove and re-add the user to Azure DevOps Server to fix the problem, as my administrative account has access and can see the user's email in ActiveDirectory. But the users are not being picked up by the sync job in Azure DevOps Server anymore, so their email address continues to be blank. (Users have been added for weeks or months without the update being picked up.)

We have verified that the Azure DevOps Server service account can see the email address in ActiveDirectory when logged into the server. So it's not an access issue with the service account.

How do I manually force Azure DevOps Server to run an ActiveDirectory sync? There used to be a JobService web service that I could access for this in previous versions of TFS, but that service doesn't appear to be available anymore, or is no longer scheduled to run.


Solution

  • Since no solution has worked, I decided to see what could be done from a coding perspective. The answer turned out to be straightforward. NOTE: Please make sure that you check the solutions provided below prior to the coding approach, as Azure DevOps Server is supposed to be refreshing identities automatically.

    First, I found a Stack Overflow article about finding users by name:

    TFS get user by name

    This can be used to fetch a user or a group by its display name, among other attributes, using the ReadIdentity method.

    This same IIDentityServiceProvider also has a method on it called RefreshIdentity. This method, when called with the IdentityDescriptor of the user, forces the identity to be immediately refreshed from its provider. See the documentation here:

    https://learn.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2013/ff734945(v=vs.120)?redirectedfrom=MSDN

    This method returns true if the refresh was successful, or false if the refresh failed. it is also possible for the refresh to throw an exception. For example, the Azure DevOps identity named "Project Collection Build Service" is listed as a user when retrieved, but this identity throws an exception when refreshed.

    Using these methods, a complete tool was able to be constructed to repair the identities of individual users, or to scan through all users in the group "Project Collection Valid Users" and refresh the entire system. Using this tool, we were able to fix our synchronization issues between Azure DevOps Server and Active Directory.

    Here's some sample code showing how to use these methods:

    string rootSourceControlUrl = "TODO: Root URL of Azure DevOps";
    string projectCollection = "TODO: Individual project collection within Azure DevOps";    
    
    TfsTeamProjectCollection tfsCollection = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(new Uri($"{rootSourceControlUrl}/{projectCollection}"));
    IIdentityManagementService ims = (IIdentityManagementService)tfsCollection.GetService(typeof(IIdentityManagementService));
    TeamFoundationIdentity foundUser = ims.ReadIdentity(IdentitySearchFactor.DisplayName, 
                                           "TODO: Display name of user", MembershipQuery.Direct, 
                                           ReadIdentityOptions.ExtendedProperties);
    if(foundUser != null)
    {
        try
        {
            if (ims.RefreshIdentity(foundUser.Descriptor))
            {
                // Find the user by its original IdentityDescriptor, which shouldn't change during the refresh
                TeamFoundationIdentity refreshedUser = ims.ReadIdentity(foundUser.Descriptor, 
                              MembershipQuery.Direct, ReadIdentityOptions.ExtendedProperties);
    
                // TODO : Display changes from foundUser to refreshedUser, using individual properties 
                //        and the method foundUser.GetProperties(), which returns an 
                //        IEnumerable<KeyValuePair<string, object>> collection.
            }
            else
            {
                 // TODO : Notify that user failed to refresh
            }
        }
        catch(Exception exc)
        {
            // TODO : Notify that exception occurred
        }
    }
    else
    {
        // TODO : Notify that user was not found
    }