Tuesday, January 8, 2019

Searching for users in Active Directory

As developers for windows based systems, there are situations where we need to deal with Active Directory. This post is about searching user in Active Directory from C#. It can run from desktop as well as web applications as long as the user running application has the permission to AD and AD is reachable via network.

Prerequisites

Knowledge about Active Directory and terms such as Forest, Global Catalog etc... are preferred to understand this post

Searching in particular AD Forest

Here the input is the search string and the name of the AD Forest

public static IEnumerable<ADMember> GetUsersByADForestName(string searchString, string nameOFADForest)
{
    DirectoryContext rootDomainContext = new DirectoryContext(DirectoryContextType.Forest, nameOFADForest);
    Forest forest = Forest.GetForest(rootDomainContext);
    return GetUsersFromForest(searchString, forest);
}

As seen above, the Forest object is created using DirectoryContext which is created using the name of AD Forest.

The GetUsersFromForest() method is given below which does the search

private static IEnumerable<ADMember> GetUsersFromForest(string searchString, Forest forest)
{
    GlobalCatalog catalog = forest.FindGlobalCatalog();
    using (DirectorySearcher directorySearcher = catalog.GetDirectorySearcher())
    {
        //Use different options based on need. Below one is for searchnig names.
        directorySearcher.Filter = $"(&(objectCategory=person)(objectClass=user)(|(sn={searchString}*)(givenName={searchString}*)(samAccountName={searchString}*)))";

        SearchResultCollection src = directorySearcher.FindAll();

        foreach (SearchResult sr in src)
        {
            yield return new ADMember
            {
                Title = GetPropertyValueFromSearchResult(sr, "title"),
                FirstName = GetPropertyValueFromSearchResult(sr, "givenName"),
                MiddleName = GetPropertyValueFromSearchResult(sr, "middleName"),
                LastName = GetPropertyValueFromSearchResult(sr, "sn"),
                Phone = GetPropertyValueFromSearchResult(sr, "phone"),
                Email = GetPropertyValueFromSearchResult(sr, "mail"),
                DisplayName = GetPropertyValueFromSearchResult(sr, "name"),
                UserName = GetPropertyValueFromSearchResult(sr, "samAccountName")
            };
            
        }
    }
}

Hope the above code snippet is self explanatory. The ADMember is just a class with required properties. Below goes the GetPropertyValueFromSearchResult method.

private static string GetPropertyValueFromSearchResult(SearchResult searchResult, string property)
{
    return searchResult.Properties[property].Count > 0 ? searchResult.Properties[property][0].ToString() : string.Empty;
}

The assembly references needed are given below
1. System.DirectoryServices.dll
2. System.DirectoryServices.AccountManagement.dll

Searching in current AD Forest and its trusted Forests

There could be scenarios where we need to search the current AD Forest and any other forests trusted to it. Below goes the code for it.

private static IEnumerable<ADMember> GetUsersByADForestNameAndItsTrustedForests(string searchString)
{
    searchString = EscapeForSearchFilter(searchString);
    List<ADMember> userListFromActiveDirectory = new List<ADMember>();

    var currentForest = Forest.GetCurrentForest();
    userListFromActiveDirectory.AddRange(GetUsersFromForest(searchString, currentForest));

    IEnumerable<ADMember> userListFromGlobalCatalog = GetUsersFromTrustedForests(searchString);
    userListFromActiveDirectory.AddRange(userListFromGlobalCatalog);

    return userListFromActiveDirectory;
}
private static IEnumerable<ADMember> GetUsersFromTrustedForests(string searchString)
{
    var forest = Forest.GetCurrentForest();
    List<ADMember> userInfo = new List<ADMember>();
    var relations = forest.GetAllTrustRelationships().Cast<TrustRelationshipInformation>();
    var filteredRelations = relations.Where(IsTheTrustValid);
    Parallel.ForEach(filteredRelations, (TrustRelationshipInformation trust) =>
    {
        Trace($"TrustedRelation. Source {trust.SourceName}, TargetName {trust.TargetName},{trust.TrustDirection},{trust.TrustType}");
        try
        {
            DirectoryContext rootDomainContext = new DirectoryContext(DirectoryContextType.Forest, trust.TargetName);
            Forest trustedForest = Forest.GetForest(rootDomainContext);
            var userDetails = GetUsersFromForest(searchString, trustedForest);
            if (userDetails.Any())
            {
                userInfo.AddRange(userDetails);
            }
        }
        catch (Exception ex)
        {
            Trace($" Searching exception {ex.Message} for TrustedRelation. Source {trust.SourceName}, Destination {trust.TargetName}.  Continuing...");
        }
    });
    return userInfo;
}
private static bool IsTheTrustValid(TrustRelationshipInformation trust)
{
    return (trust.TrustDirection == TrustDirection.Bidirectional || trust.TrustDirection == TrustDirection.Outbound)
        && trust.TrustType == TrustType.Forest;
}

All the 3 methods are present in the above snippet which helps to filter the trust relation and to create object of Forest from TrustRelation object. 

Please note the code snippet is for demonstration purpose only. It has to be tweaked for production especially when we are dealing with parallelism.

No comments: