1
\$\begingroup\$

I wrote a method for a wpf app in .net core to get all users and their group by name if they are in a group from my active directory domaine.

public static void getGroupsWithUsers() {
    String currentDomain = Domain.GetCurrentDomain().ToString();

    using (PrincipalContext context = new PrincipalContext(ContextType.Domain, currentDomain))
    {
        using (PrincipalSearcher searcher = new PrincipalSearcher(new UserPrincipal(context)))
        {

            foreach (var result in searcher.FindAll())
            {
                DirectoryEntry de = result.GetUnderlyingObject() as DirectoryEntry;
                var SID = WindowsIdentity.GetCurrent().User.ToString();


                // find a user
                UserPrincipal user = UserPrincipal.FindByIdentity(context, de.Properties["samAccountName"].Value.ToString());

                if (user != null)
                {
                    // get the user's groups
                    var groups = user.GetAuthorizationGroups();

                    foreach (GroupPrincipal group in groups)
                    {
                        Console.WriteLine("User: " + user + " is in Group: " + group);
                    }


                }
            }
        }
    }

} 

The method takes 1.5 to 1.8 seconds for a small amount of data and Im afraid that the execution time takes forever for real world data amounts.

\$\endgroup\$
1
\$\begingroup\$

I suspect you'd better off using a DirectorySearcher, but I don't know Active Directory services are supported by .NET Core.

Here is how I did it in .NET Framework (4.7.2).

Classes to hold the data I needed:

public class Account
{
    private Account(string name)
    {
        Name = name;
    }

    public Account(string name, List<DataCombination> groupData)
        : this(name)
    {
        GroupData = groupData;
    }

    public string Name { get; }

    public List<DataCombination> GroupData { get; }
}

public class DataCombination
{
    public string GroupName { get; set; }

    public string Account { get; set; }
}

The code that queries AD:

        var searchResults = new List<SearchResult>();

        using (var directoryEntry = new DirectoryEntry(directoryEntryPath))
        {
            directoryEntry.RefreshCache();

            using (var directorySearcher = new DirectorySearcher(directoryEntry))
            {
                directorySearcher.SearchRoot = directoryEntry;
                directorySearcher.SearchScope = SearchScope.Subtree;
                directorySearcher.PageSize = 1000;
                directorySearcher.Filter = filter;

                directorySearcher.PropertiesToLoad.Clear();
                directorySearcher.PropertiesToLoad.Add(PropertyName.Name);
                directorySearcher.PropertiesToLoad.Add(PropertyName.MemberOf);

                searchResults.AddRange(directorySearcher.FindAll().Cast<SearchResult>());
            }
        }

        var accounts = searchResults
            .Select(searchResult => _accountFactory.Execute(searchResult.Properties))
            .OrderBy(x => x.Name)
            .ToList();

        return accounts;
  • directoryEntryPath is an LDAP URI, starts with "LDAP://".
  • filter is used to filter using a list of user names and is filled by this method:

    public static string Execute(IEnumerable<string> userNames)
    {
        var filter = new StringBuilder();
        filter.Append("(&(objectClass=user)(|");
    
        foreach (var userName in userNames)
        {
            filter.Append($"(sAMAccountName={userName})");
        }
    
        filter.Append("))");
    
        return filter.ToString();
    }
    
  • PropertyName is a class containing const strings:

    internal static class PropertyName
    {
        // https://msdn.microsoft.com/en-us/library/windows/desktop/ms675090.aspx
        public const string Name = "name";
        public const string LockoutTime = "lockoutTime";
        public const string UserAccountControl = "userAccountControl";
        public const string MemberOf = "memberOf";
        public const string DisplayName = "displayName";
    }
    

This is the _accountFactory which converts the data returned by the DirectorySearcher into my classes:

internal class GroupAccountFactory : BaseDirectorySearcher, IAccountFactory
{
    public Account Execute(ResultPropertyCollection properties)
    {
        var accountName = GetValue<string>(properties, PropertyName.Name);

        var groupData = GetGroupData(properties, accountName);

        return new Account(accountName, groupData);
    }

    private List<DataCombination> GetGroupData(ResultPropertyCollection properties, string accountName)
    {
        var groupNames = new List<string>();

        foreach (var propertyValue in properties[PropertyName.MemberOf].Cast<string>())
        {
            groupNames.AddRange(propertyValue
                .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
                .Select(part => part.Split('='))
                .Select(x => new
                {
                    Key = x[0],
                    Value = x[1],
                })
                .Where(x => x.Key == LdapDirectory.CommonName)
                .Select(x => x.Value));
        }

        return DataCombinationCollectionCreator.Execute(new HashSet<string>(groupNames), accountName);
    }
}

Its base class:

internal abstract class BaseDirectorySearcher
{
    protected T GetValue<T>(ResultPropertyCollection properties, string propertyName)
    {
        var value = PropertyValueRetriever.GetObject(properties, propertyName);
        if (value == null)
        {
            return default(T);
        }

        return (T)value;
    }
}

And that one uses:

internal class PropertyValueRetriever
{
    public static string GetString(ResultPropertyCollection properties, string propertyName)
    {
        var value = GetObject(properties, propertyName);

        return (string)value;
    }

    public static object GetObject(ResultPropertyCollection properties, string propertyName)
    {
        if (properties.Contains(propertyName)
            && properties[propertyName].Count > 0)
        {
            return properties[propertyName][0];
        }

        return null;
    }
}

You might have noticed this class:

internal class LdapDirectory
{
    public const string CommonName = "CN";
    public const string OrganizationalUnit = "OU";
    public const string DomainComponent = "DC";
}

And finally:

internal class DataCombinationCollectionCreator
{
    public static List<DataCombination> Execute(ICollection<string> groupNames, string accountName)
    {
        var groupData = new List<DataCombination>();

        foreach (var groupName in groupNames)
        {
            groupData.Add(CreateDataCombination(groupName, accountName));
        }

        return groupData;
    }

    private static DataCombination CreateDataCombination(string groupName, string accountName)
    {
        return new DataCombination
        {
            GroupName = groupName,
            Account = accountName
        };
    }
}

(Note that I have stripped away some of the extra logic that is present in my code which only applies for my purposes; thus the whole use of DataCombinationCollectionCreator might be overkill for you.)

It's all fairly self-explanatory. Whether it performs better than your code, I don't know: AD operations can be notoriously slow.

|improve this answer|||||
\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.