From f2deb29ecfdc003962c03b0c46a422b167bf2fde Mon Sep 17 00:00:00 2001 From: "Clauss, Adam S" Date: Sun, 15 Sep 2024 13:51:18 -0500 Subject: [PATCH] Implemented a new Generic LDAP user directory that avoids assumptions tied to an Active Directory system and allows customization to work with both AD and non-AD systems. Modifications to some existing methods was done to add parameters with default values. Default values should persist the current preexisting behaviors while allowing them to be customized by the Generic LDAP user directory. --- .../DirectoryServicesLdapClient.cs | 38 +- ...nericLdapUserDirectory.GenericLdapGroup.cs | 88 ++++ ...enericLdapUserDirectory.GenericLdapUser.cs | 58 +++ .../GenericLdap/GenericLdapUserDirectory.cs | 410 ++++++++++++++++++ .../UserDirectories/LdapClientEntry.cs | 9 +- .../UserDirectories/NovellLdapClient.cs | 26 +- 6 files changed, 616 insertions(+), 13 deletions(-) create mode 100644 InedoCore/InedoExtension/UserDirectories/GenericLdap/GenericLdapUserDirectory.GenericLdapGroup.cs create mode 100644 InedoCore/InedoExtension/UserDirectories/GenericLdap/GenericLdapUserDirectory.GenericLdapUser.cs create mode 100644 InedoCore/InedoExtension/UserDirectories/GenericLdap/GenericLdapUserDirectory.cs diff --git a/InedoCore/InedoExtension/UserDirectories/DirectoryServicesLdapClient.cs b/InedoCore/InedoExtension/UserDirectories/DirectoryServicesLdapClient.cs index 67ea469..394add1 100644 --- a/InedoCore/InedoExtension/UserDirectories/DirectoryServicesLdapClient.cs +++ b/InedoCore/InedoExtension/UserDirectories/DirectoryServicesLdapClient.cs @@ -6,10 +6,24 @@ namespace Inedo.Extensions.UserDirectories; internal sealed class DirectoryServicesLdapClient : LdapClient { private LdapConnection connection; + private readonly AuthType? authType; + private readonly string[] attributes; + + /// + public DirectoryServicesLdapClient(AuthType? authType = null, string[] attributes = null) + { + this.authType = authType; + this.attributes = attributes; + } public override void Connect(string server, int? port, bool ldaps, bool bypassSslCertificate) { this.connection = new LdapConnection(new LdapDirectoryIdentifier(server, port ?? (ldaps ? 636 : 389))); + if (authType != null) + { + connection.AuthType = authType.Value; + } + if (ldaps) { this.connection.SessionOptions.SecureSocketLayer = true; @@ -25,6 +39,11 @@ public override void Bind(NetworkCredential credentials) public override IEnumerable Search(string distinguishedName, string filter, LdapClientSearchScope scope) { var request = new SearchRequest(distinguishedName, filter, (SearchScope)scope); + if (attributes != null) + { + request.Attributes.AddRange(attributes); + } + var response = this.connection.SendRequest(request); if (response is SearchResponse sr) @@ -55,9 +74,8 @@ public override string GetPropertyValue(string propertyName) return propertyCollection[0]?.ToString() ?? string.Empty; } - public override ISet ExtractGroupNames(string memberOfPropertyName = null) + public override ISet ExtractGroupNames(string memberOfPropertyName = "memberof", string groupNamePropertyName = "CN", bool includeDomainPath = false) { - Logger.Log(MessageLevel.Debug, "Begin ExtractGroupNames", "AD User Directory"); var groups = new HashSet(StringComparer.OrdinalIgnoreCase); try @@ -86,12 +104,20 @@ public override ISet ExtractGroupNames(string memberOfPropertyName = nul if (!string.IsNullOrWhiteSpace(memberOf)) { var groupNames = from part in memberOf.Split(',') - where part.StartsWith("CN=", StringComparison.OrdinalIgnoreCase) - let name = part["CN=".Length..] + where part.StartsWith($"{groupNamePropertyName}=", StringComparison.OrdinalIgnoreCase) + let name = part[$"{groupNamePropertyName}=".Length..] where !string.IsNullOrWhiteSpace(name) select name; - - groups.UnionWith(groupNames); + foreach (var groupName in groupNames) + { + string groupNameToAdd = groupName; + if (includeDomainPath) + { + groupNameToAdd = $"{groupName}@{GetDomainPath(memberOf)}"; + } + + groups.Add(groupNameToAdd); + } } } } diff --git a/InedoCore/InedoExtension/UserDirectories/GenericLdap/GenericLdapUserDirectory.GenericLdapGroup.cs b/InedoCore/InedoExtension/UserDirectories/GenericLdap/GenericLdapUserDirectory.GenericLdapGroup.cs new file mode 100644 index 0000000..fbbf57e --- /dev/null +++ b/InedoCore/InedoExtension/UserDirectories/GenericLdap/GenericLdapUserDirectory.GenericLdapGroup.cs @@ -0,0 +1,88 @@ +using Inedo.Extensibility.UserDirectories; + +namespace Inedo.Extensions.UserDirectories.GenericLdap; + +public partial class GenericLdapUserDirectory +{ + private sealed class GenericLdapGroup : IUserDirectoryGroup, IEquatable + { + private readonly GroupId groupId; + private readonly GenericLdapUserDirectory directory; + private readonly HashSet isMemberOfGroupCache = new(StringComparer.OrdinalIgnoreCase); + + public Lazy> Groups { get; } + + public GenericLdapGroup(GenericLdapUserDirectory directory, LdapClientEntry entry) + { + this.directory = directory; + groupId = new GroupId(entry.GetPropertyValue(directory.GroupNameAttributeName), entry.GetDomainPath()); + groupId.DistinguishedName = entry.DistinguishedName; + + Groups = new Lazy>(() => GetGroups(directory, entry), LazyThreadSafetyMode.ExecutionAndPublication); + } + + public string Name => groupId.ToFullyQualifiedName(); + public string DisplayName => groupId.Principal; + + public bool IsMemberOfGroup(string groupName) + { + Logger.Log(MessageLevel.Debug, "Begin GenericLdapGroup IsMemberOfGroup", "Generic LDAP User Directory"); + ArgumentNullException.ThrowIfNull(groupName); + if (isMemberOfGroupCache.Contains(groupName)) + { + Logger.Log(MessageLevel.Debug, "End GenericLdapGroup IsMemberOfGroup", "Generic LDAP User Directory"); + return true; + } + + if (Groups.Value.Contains(groupName)) + { + Logger.Log(MessageLevel.Debug, "End GenericLdapGroup IsMemberOfGroup", "Generic LDAP User Directory"); + isMemberOfGroupCache.Add(groupName); + return true; + } + + Logger.Log(MessageLevel.Debug, "End GenericLdapGroup IsMemberOfGroup", "Generic LDAP User Directory"); + return false; + } + + public IEnumerable GetMemberUsers() + { + Logger.Log(MessageLevel.Debug, "Begin GenericLdapGroup GetMembers", "Generic LDAP User Directory"); + if (directory.GroupSearchType != GroupSearchType.RecursiveSearchActiveDirectory) + { + var memberUsers = directory.FindUsers($"({directory.GroupMemberOfAttributeName}={groupId.DistinguishedName})"); + foreach (var memberUser in memberUsers) + { + yield return memberUser; + } + + if (directory.GroupSearchType == GroupSearchType.RecursiveSearch) + { + var memberGroups = directory.FindGroups($"({directory.GroupMemberOfAttributeName}={groupId.DistinguishedName})").Cast(); + foreach (var memberGroup in memberGroups) + { + foreach (var memberUser in memberGroup.GetMemberUsers()) + { + yield return memberUser; + } + } + } + } + else + { + foreach (var userEntry in directory.FindUsers($"memberOf:1.2.840.113556.1.4.1941:={groupId.DistinguishedName}")) + { + yield return userEntry; + } + } + Logger.Log(MessageLevel.Debug, "End GenericLdapGroup GetMembers", "Generic LDAP User Directory"); + } + + public bool Equals(GenericLdapGroup other) => groupId.Equals(other?.groupId); + public bool Equals(IUserDirectoryGroup other) => Equals(other as GenericLdapGroup); + public bool Equals(IUserDirectoryPrincipal other) => Equals(other as GenericLdapGroup); + public override bool Equals(object obj) => Equals(obj as GenericLdapGroup); + public override int GetHashCode() => groupId.GetHashCode(); + public override string ToString() => groupId.Principal; + } +} \ No newline at end of file diff --git a/InedoCore/InedoExtension/UserDirectories/GenericLdap/GenericLdapUserDirectory.GenericLdapUser.cs b/InedoCore/InedoExtension/UserDirectories/GenericLdap/GenericLdapUserDirectory.GenericLdapUser.cs new file mode 100644 index 0000000..20f3e70 --- /dev/null +++ b/InedoCore/InedoExtension/UserDirectories/GenericLdap/GenericLdapUserDirectory.GenericLdapUser.cs @@ -0,0 +1,58 @@ +using Inedo.Extensibility.UserDirectories; + +namespace Inedo.Extensions.UserDirectories.GenericLdap; + +public partial class GenericLdapUserDirectory +{ + internal sealed class GenericLdapUser : IUserDirectoryUser, IEquatable + { + private readonly GenericLdapUserDirectory directory; + private readonly UserId userId; + private readonly HashSet isMemberOfGroupCache = new(StringComparer.OrdinalIgnoreCase); + + public Lazy> Groups { get; } + + public GenericLdapUser(GenericLdapUserDirectory directory, LdapClientEntry entry) + { + this.directory = directory; + userId = new UserId(entry.GetPropertyValue(directory.UserNameAttributeName), entry.GetDomainPath()); + userId.DistinguishedName = entry.DistinguishedName; + DisplayName = AH.CoalesceString(entry.GetPropertyValue(directory.UserDisplayNameAttributeName), userId.Principal); + EmailAddress = entry.GetPropertyValue(directory.EmailAddressAttributeName); + DistinguishedName = entry.DistinguishedName; + Groups = new Lazy>(() => GetGroups(directory, entry), LazyThreadSafetyMode.ExecutionAndPublication); + } + + string IUserDirectoryPrincipal.Name => userId.ToFullyQualifiedName(); + public string EmailAddress { get; } + public string DisplayName { get; } + public string DistinguishedName { get; } + + public bool IsMemberOfGroup(string groupName) + { + Logger.Log(MessageLevel.Debug, "Begin GenericLdapUser IsMemberOfGroup", "Generic LDAP User Directory"); + ArgumentNullException.ThrowIfNull(groupName); + if (isMemberOfGroupCache.Contains(groupName)) + { + Logger.Log(MessageLevel.Debug, "End GenericLdapUser IsMemberOfGroup", "Generic LDAP User Directory"); + return true; + } + + if (Groups.Value.Contains(groupName)) + { + Logger.Log(MessageLevel.Debug, "End GenericLdapUser IsMemberOfGroup", "Generic LDAP User Directory"); + isMemberOfGroupCache.Add(groupName); + return true; + } + + Logger.Log(MessageLevel.Debug, "End GenericLdapUser IsMemberOfGroup", "Generic LDAP User Directory"); + return false; + } + + public bool Equals(GenericLdapUser other) => userId.Equals(other?.userId); + public bool Equals(IUserDirectoryUser other) => Equals(other as GenericLdapUser); + public bool Equals(IUserDirectoryPrincipal other) => Equals(other as GenericLdapUser); + public override bool Equals(object obj) => Equals(obj as GenericLdapUser); + public override int GetHashCode() => userId.GetHashCode(); + } +} \ No newline at end of file diff --git a/InedoCore/InedoExtension/UserDirectories/GenericLdap/GenericLdapUserDirectory.cs b/InedoCore/InedoExtension/UserDirectories/GenericLdap/GenericLdapUserDirectory.cs new file mode 100644 index 0000000..b59284b --- /dev/null +++ b/InedoCore/InedoExtension/UserDirectories/GenericLdap/GenericLdapUserDirectory.cs @@ -0,0 +1,410 @@ +using System.DirectoryServices.Protocols; +using System.Net; +using System.Security; +using Inedo.Extensibility.UserDirectories; +using Inedo.Serialization; + +namespace Inedo.Extensions.UserDirectories.GenericLdap +{ + [DisplayName("Generic LDAP Directory")] + [Description("Queries an LDAP directory for users and group membership.")] + public partial class GenericLdapUserDirectory : UserDirectory + { + #region Configuration Properties + + #region Connection + + [Persistent] + [Category("Connection")] + [DisplayName("LDAP hostname")] + [PlaceholderText("Use Domain")] + [Description("Specify the host name or IP address of the domain controller.")] + public string Hostname { get; set; } + + [Persistent] + [Category("Connection")] + [DisplayName("LDAP Port Override")] + [Description("This will override the port used to connect to LDAP or LDAPS. If this is not set, then port 389 is used for LDAP and 636 is used for LDAPS.")] + [PlaceholderText("Use default port")] + public string Port { get; set; } + + [Persistent] + [Category("Connection")] + [DisplayName("LDAP Connection")] + [DefaultValue(LdapConnectionType.Ldap)] + [Description("Connect via LDAP, LDAP over SSL, or LDAP over SSL and bypass certificate errors.")] + public LdapConnectionType LdapConnection { get; set; } + + [Persistent] + [Category("Connection")] + [DisplayName("Bind Username")] + [Description("User name for LDAP credentials that have READ access to the domain")] + public string BindUsername { get; set; } + + [Persistent(Encrypted = true)] + [Category("Connection")] + [DisplayName("Bind Password")] + [Description("BindPassword for LDAP credentials that have READ access to the domain")] + public SecureString BindPassword { get; set; } + + [Persistent] + [Category("Connection")] + [DisplayName("Base DN")] + [PlaceholderText("Leave blank to search the root of the directory.")] + [Description("Base DN to use when searcing for users and groups.")] + public string BaseDN { get; set; } + + #endregion + + #region User Search + + [Persistent] + [Category("User Search")] + [DisplayName("Users LDAP Filter")] + [DefaultValue("(objectCategory=user)")] + [PlaceholderText("(objectCategory=user)")] + public string UsersFilterBase { get; set; } = "(objectCategory=user)"; + + [Persistent] + [Category("User Search")] + [DisplayName("User Name Attribute")] + [DefaultValue("sAMAccountName")] + [PlaceholderText("sAMAccountName")] + public string UserNameAttributeName { get; set; } = "sAMAccountName"; + + [Persistent] + [Category("User Search")] + [DisplayName("User Display Name Attribute")] + [DefaultValue("displayName")] + [PlaceholderText("displayName")] + public string UserDisplayNameAttributeName { get; set; } = "displayName"; + + [Persistent] + [Category("User Search")] + [DisplayName("Email Attribute")] + [DefaultValue("mail")] + [PlaceholderText("mail")] + public string EmailAddressAttributeName { get; set; } = "mail"; + + #endregion + + #region Group Search + + [Persistent] + [Category("Group Search")] + [DisplayName("Groups LDAP Filter")] + [DefaultValue("(objectCategory=group)")] + [PlaceholderText("(objectCategory=group)")] + public string GroupsFilterBase { get; set; } = "(objectCategory=group)"; + + [Persistent] + [Category("Group Search")] + [DisplayName("Group Name Attribute")] + [DefaultValue("name")] + [PlaceholderText("name")] + public string GroupNameAttributeName { get; set; } = "name"; + + [Persistent] + [Category("Group Search")] + [DisplayName("Search Group Method")] + [DefaultValue(GroupSearchType.NoRecursion)] + [Description("Choose to recursively check group memberships or only check for the groups that a user is directly a member of. This may cause reduced performance.")] + public GroupSearchType GroupSearchType { get; set; } + + [Persistent] + [Category("Group Search")] + [DisplayName("Group Membership Property Value")] + [DefaultValue("memberof")] + [PlaceholderText("memberof")] + [Description("This property will only be used when \"No Recursion\" or \"Recursive Search (LDAP/Non-Active Directory)\" is set for the group search type. When the group search type is \"Recursive Search (Active Directory Only)\" a special Active Directory query is used to find groups.")] + public string GroupMemberOfAttributeName { get; set; } = "memberof"; + + private string[] RequiredAttributes + { + get + { + return new[] + { + UserNameAttributeName, UserDisplayNameAttributeName, EmailAddressAttributeName, + GroupNameAttributeName, GroupMemberOfAttributeName + }; + } + } + + #endregion + + #endregion + + #region Overrides of UserDirectory + + /// + public override IUserDirectoryUser TryGetUser(string userName) + { + // Username might be fully qualified: user@domain + try + { + if (TryParseFullyQualifiedPrincipalName(userName, out string user, out _)) + { + userName = user; + } + + var result = FindUsers(userName).FirstOrDefault(); + return result; + } + catch + { + return null; + } + } + + /// + public override IUserDirectoryUser TryGetAndValidateUser(string userName, string password) + { + var user = TryGetUser(userName) as GenericLdapUser; + if (user == null) + { + return null; + } + + try + { + using var connection = CreateConnectionAndBindUser(user.DistinguishedName, password); + } + catch + { + return null; + } + + return user; + } + + /// + public override IUserDirectoryGroup TryGetGroup(string groupName) + { + try + { + if (TryParseFullyQualifiedPrincipalName(groupName, out string group, out _)) + { + groupName = group; + } + + var result = FindGroups(groupName).FirstOrDefault(); + return result; + } + catch + { + return null; + } + } + + /// + public override IEnumerable GetGroupMembers(string groupName) + { + var group = (GenericLdapGroup)TryGetGroup(groupName); + return group?.GetMemberUsers()?.ToList() ?? []; + } + + /// + public override IEnumerable FindPrincipals(string searchTerm) + { + // We want any groups that also match the search terms plus any users that match the search terms + return FindUsers(searchTerm).Cast().Union(FindGroups(searchTerm)); + } + + /// + public override IEnumerable FindUsers(string searchTerm) + { + if (string.IsNullOrEmpty(searchTerm)) + { + return []; + } + + string filter = UsersFilterBase; + // If the passed-in search term contains an = sign, assume it is already a filter (ex: the AD recursive group member search) and append it as-is + if (searchTerm.Contains("=")) + { + filter = CombineFiltersAnd(filter, searchTerm); + } + else + { + filter = CombineFiltersAnd(filter, $"{UserNameAttributeName}={searchTerm}*"); + } + + var results = Search(BaseDN, filter); + return results.Select(LdapClientEntryAsUser); + } + + /// + public override IEnumerable FindGroups(string searchTerm) + { + if (string.IsNullOrEmpty(searchTerm)) + { + return []; + } + + string filter = GroupsFilterBase; + + // If the passed-in search term contains an = sign, assume it is already a filter (ex: the AD recursive group member search) and append it as-is + if (searchTerm.Contains("=")) + { + filter = CombineFiltersAnd(filter, searchTerm); + } + else + { + filter = CombineFiltersAnd(filter, $"{GroupNameAttributeName}={searchTerm}*"); + } + + var results = Search(BaseDN, filter); + return results.Select(LdapClientEntryAsGroup); + } + + #endregion + + #region Private Methods + + private LdapClient GetClient() + { + return OperatingSystem.IsWindows() + ? new DirectoryServicesLdapClient(AuthType.Basic, RequiredAttributes) + : new NovellLdapClient(RequiredAttributes); + } + + private static bool TryParseFullyQualifiedPrincipalName(string fullyQualifiedName, out string principalName, out string domainName) + { + // Username here may be fully qualified: user@domain + string[] parts = fullyQualifiedName.Split('@'); + if (parts.Length < 2) + { + principalName = null; + domainName = null; + return false; + } + + // Everything after the last @ is considered the domain, everything before that is the name + // Allow for multiple @ signs (allowing for cases where the user itself is an email address - a@gmail.com@domain.com) + domainName = parts[parts.Length - 1]; + principalName = string.Join('@', parts.SkipLast(1)); + return true; + + } + + private static string CombineFiltersAnd(string baseFilter, string additionalFilter) + { + string newFilter = baseFilter; + if (additionalFilter != null) + { + if (baseFilter.First() != '(') + { + baseFilter = $"({baseFilter})"; + } + + if (additionalFilter.First() != '(') + { + additionalFilter = $"({additionalFilter})"; + } + newFilter = $"(&{baseFilter}{additionalFilter})"; + } + + return newFilter; + } + + private IEnumerable Search(string baseDN, string filter) + { + this.LogInformation($"Generic LDAP search for filter \"{filter}\" in base DN \"baseDN\"..."); + using var connection = CreateConnectionAndBindUser(); + var entries = connection.Search(baseDN, filter, LdapClientSearchScope.Subtree); + return entries.ToList(); + } + + private LdapClient CreateConnectionAndBindUser(string username = null, string password = null) + { + NetworkCredential credential = null; + if (username != null && password != null) + { + credential = new NetworkCredential(username, password); + } + else if (BindUsername != null && BindPassword != null) + { + credential = new NetworkCredential(BindUsername, BindPassword); + } + + return CreateConnectionAndBindUser(credential); + } + + private LdapClient CreateConnectionAndBindUser(NetworkCredential credential) + { + LdapClient conn = null; + + try + { + conn = GetClient(); + conn.Connect(AH.NullIf(Hostname, string.Empty), int.TryParse(Port, out var port) ? port : null, LdapConnection != LdapConnectionType.Ldap, LdapConnection == LdapConnectionType.LdapsWithBypass); + if (credential != null) + { + conn.Bind(credential); + } + + return conn; + } + catch + { + conn?.Dispose(); + throw; + } + } + + private GenericLdapUser LdapClientEntryAsUser(LdapClientEntry entry) + { + var user = new GenericLdapUser(this, entry); + return user; + } + + private GenericLdapGroup LdapClientEntryAsGroup(LdapClientEntry entry) + { + var group = new GenericLdapGroup(this, entry); + return group; + } + + private static ISet GetGroups(GenericLdapUserDirectory directory, LdapClientEntry entry) + { + var groups = new List(); + //Old Group searching way + if (directory.GroupSearchType != GroupSearchType.RecursiveSearchActiveDirectory) + { + var parentGroupNames = entry.ExtractGroupNames(directory.GroupMemberOfAttributeName, directory.GroupNameAttributeName, true); + foreach (var childGroupName in parentGroupNames) + { + groups.Add(childGroupName); + } + + groups.Sort(); + if (directory.GroupSearchType == GroupSearchType.RecursiveSearch) + { + foreach (var parentGroupName in parentGroupNames) + { + var parentGroup = (GenericLdapGroup)directory.TryGetGroup(parentGroupName); + if (parentGroup != null) + { + foreach (string childMemberGroup in parentGroup.Groups.Value) + { + groups.Add(childMemberGroup); + } + } + } + } + } + // New AD-only way + else + { + foreach (var group in directory.FindGroups($"member:1.2.840.113556.1.4.1941:={entry.DistinguishedName}")) + { + groups.Add(group.Name); + } + } + + return new HashSet(groups); + } + + #endregion + } +} diff --git a/InedoCore/InedoExtension/UserDirectories/LdapClientEntry.cs b/InedoCore/InedoExtension/UserDirectories/LdapClientEntry.cs index 49d9da3..dcd82ea 100644 --- a/InedoCore/InedoExtension/UserDirectories/LdapClientEntry.cs +++ b/InedoCore/InedoExtension/UserDirectories/LdapClientEntry.cs @@ -13,11 +13,16 @@ protected LdapClientEntry() public abstract string DistinguishedName { get; } public abstract string GetPropertyValue(string propertyName); - public abstract ISet ExtractGroupNames(string memberOfPropertyName = null); + public abstract ISet ExtractGroupNames(string memberOfPropertyName = "memberof", string groupNamePropertyName = "CN", bool includeDomainPath = false); public string GetDomainPath() + { + return GetDomainPath(this.DistinguishedName); + } + + public static string GetDomainPath(string distinguishedName) { return string.Join(".", - from p in this.DistinguishedName.Split(',') + from p in distinguishedName.Split(',') where p.StartsWith("DC=", StringComparison.OrdinalIgnoreCase) select p.Substring("DC=".Length) ); diff --git a/InedoCore/InedoExtension/UserDirectories/NovellLdapClient.cs b/InedoCore/InedoExtension/UserDirectories/NovellLdapClient.cs index 000b6a0..b64124e 100644 --- a/InedoCore/InedoExtension/UserDirectories/NovellLdapClient.cs +++ b/InedoCore/InedoExtension/UserDirectories/NovellLdapClient.cs @@ -12,8 +12,15 @@ namespace Inedo.Extensions.UserDirectories { internal sealed class NovellLdapClient : LdapClient { + private readonly string[] attributes; private LdapConnection connection; + /// + public NovellLdapClient(string[] attributes = null) + { + this.attributes = attributes; + } + public override void Connect(string server, int? port, bool ldaps, bool bypassSslCertificate) { this.connection = new LdapConnection(); @@ -36,7 +43,7 @@ public override void Bind(NetworkCredential credentials) } public override IEnumerable Search(string distinguishedName, string filter, LdapClientSearchScope scope) { - return getResults(this.connection.Search(distinguishedName, (int)scope, filter, null, false, this.connection.SearchConstraints)); + return getResults(this.connection.Search(distinguishedName, (int)scope, filter, attributes, false, connection.SearchConstraints)); static IEnumerable getResults(ILdapSearchResults results) { @@ -109,7 +116,7 @@ public override string GetPropertyValue(string propertyName) } } - public override ISet ExtractGroupNames(string memberOfPropertyName = null) + public override ISet ExtractGroupNames(string memberOfPropertyName = "memberof", string groupNamePropertyName = "CN", bool includeDomainPath = false) { var groups = new HashSet(StringComparer.OrdinalIgnoreCase); try @@ -117,12 +124,21 @@ public override ISet ExtractGroupNames(string memberOfPropertyName = nul foreach (var memberOf in this.entry.GetAttribute(AH.NullIf(memberOfPropertyName, string.Empty) ?? "memberof")?.StringValueArray ?? Array.Empty()) { var groupNames = from part in memberOf.Split(',') - where part.StartsWith("CN=", StringComparison.OrdinalIgnoreCase) - let name = part["CN=".Length..] + where part.StartsWith($"{groupNamePropertyName}=", StringComparison.OrdinalIgnoreCase) + let name = part[$"{groupNamePropertyName}=".Length..] where !string.IsNullOrWhiteSpace(name) select name; - groups.UnionWith(groupNames); + foreach (var groupName in groupNames) + { + string groupNameToAdd = groupName; + if (includeDomainPath) + { + groupNameToAdd = $"{groupName}@{GetDomainPath(memberOf)}"; + } + + groups.Add(groupNameToAdd); + } } } catch