From 32021dcfd8c5d5f70d12279dc5b1afee7f0d73d4 Mon Sep 17 00:00:00 2001 From: Meik Date: Fri, 13 Mar 2026 14:36:23 +0100 Subject: [PATCH] Allow regex reuse for NTFS ensure groups --- LiamNtfs/C4IT.LIAM.Ntfs.cs | 16 ++- LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs | 8 +- LiamNtfs/C4IT_IAM_SET/SecurityGroup.cs | 107 ++++++++++++++++++- 3 files changed, 123 insertions(+), 8 deletions(-) diff --git a/LiamNtfs/C4IT.LIAM.Ntfs.cs b/LiamNtfs/C4IT.LIAM.Ntfs.cs index 8a6dd74..cfd50ec 100644 --- a/LiamNtfs/C4IT.LIAM.Ntfs.cs +++ b/LiamNtfs/C4IT.LIAM.Ntfs.cs @@ -563,7 +563,8 @@ namespace C4IT.LIAM groupReadTag = GetRequiredCustomTag("Filesystem_GroupReadTag"), groupTraverseTag = GetRequiredCustomTag("Filesystem_GroupTraverseTag"), groupDLTag = requiresDomainLocalTag ? GetRequiredCustomTag("Filesystem_GroupDomainLocalTag") : string.Empty, - groupGTag = GetRequiredCustomTag("Filesystem_GroupGlobalTag") + groupGTag = GetRequiredCustomTag("Filesystem_GroupGlobalTag"), + allowExistingGroupWildcardMatch = IsAdditionalConfigurationEnabled("EnsureNtfsPermissionGroupsAllowRegexMatch") }; foreach (var template in BuildSecurityGroupTemplates()) @@ -572,6 +573,19 @@ namespace C4IT.LIAM return engine; } + private bool IsAdditionalConfigurationEnabled(string key) + { + if (AdditionalConfiguration == null || string.IsNullOrWhiteSpace(key)) + return false; + + if (!AdditionalConfiguration.TryGetValue(key, out var rawValue) || string.IsNullOrWhiteSpace(rawValue)) + return false; + + return rawValue.Equals("true", StringComparison.OrdinalIgnoreCase) + || rawValue.Equals("1", StringComparison.OrdinalIgnoreCase) + || rawValue.Equals("yes", StringComparison.OrdinalIgnoreCase); + } + private IEnumerable BuildSecurityGroupTemplates() { var templates = new List(); diff --git a/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs b/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs index e315c5f..43198a5 100644 --- a/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs +++ b/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs @@ -51,6 +51,7 @@ namespace C4IT_IAM_SET public ICollection ownerUserSids; public ICollection readerUserSids; public ICollection writerUserSids; + public bool allowExistingGroupWildcardMatch; public int ReadACLPermission = 0x200A9; public int WriteACLPermission = 0x301BF; @@ -144,6 +145,7 @@ namespace C4IT_IAM_SET newSecurityGroups.username = username; newSecurityGroups.domainName = domainName; newSecurityGroups.password = password; + newSecurityGroups.AllowExistingGroupWildcardMatch = allowExistingGroupWildcardMatch; try { // ImpersonationHelper.Impersonate(domainName, username, new NetworkCredential("", password).Password, delegate @@ -274,7 +276,8 @@ namespace C4IT_IAM_SET { username = username, domainName = domainName, - password = password + password = password, + AllowExistingGroupWildcardMatch = allowExistingGroupWildcardMatch }; } @@ -909,9 +912,8 @@ namespace C4IT_IAM_SET else users = null; - var groupAlreadyExists = newSecurityGroups.GroupAllreadyExisting(newSecurityGroups.IAM_SecurityGroups[i].Name.ToUpper()); newSecurityGroups.EnsureADGroup(groupOUPath, newSecurityGroups.IAM_SecurityGroups[i], users); - if (groupAlreadyExists) + if (newSecurityGroups.IAM_SecurityGroups[i].ReusedExistingEntry) resultToken.reusedGroups.Add(newSecurityGroups.IAM_SecurityGroups[i].Name); else resultToken.createdGroups.Add(newSecurityGroups.IAM_SecurityGroups[i].Name); diff --git a/LiamNtfs/C4IT_IAM_SET/SecurityGroup.cs b/LiamNtfs/C4IT_IAM_SET/SecurityGroup.cs index 81aff60..2164b0f 100644 --- a/LiamNtfs/C4IT_IAM_SET/SecurityGroup.cs +++ b/LiamNtfs/C4IT_IAM_SET/SecurityGroup.cs @@ -23,6 +23,7 @@ namespace C4IT_IAM_Engine public string domainName; public string username; public SecureString password; + public bool AllowExistingGroupWildcardMatch; public List IAM_SecurityGroups; public string rootUID; @@ -208,6 +209,7 @@ namespace C4IT_IAM_Engine { Name = ownerGlobal.NamingTemplate, description = ownerGlobal.DescriptionTemplate, + WildcardPattern = ownerGlobal.WildcardTemplate, technicalName = "CN=" + ownerGlobal.NamingTemplate + "," + ouPath, targetTyp = (int)IAM_TargetType.FileSystem, @@ -221,6 +223,7 @@ namespace C4IT_IAM_Engine { Name = writeGlobal.NamingTemplate, description = writeGlobal.DescriptionTemplate, + WildcardPattern = writeGlobal.WildcardTemplate, technicalName = "CN=" + writeGlobal.NamingTemplate + "," + ouPath, targetTyp = (int)IAM_TargetType.FileSystem, @@ -234,6 +237,7 @@ namespace C4IT_IAM_Engine { Name = readGlobal.NamingTemplate, description = readGlobal.DescriptionTemplate, + WildcardPattern = readGlobal.WildcardTemplate, technicalName = "CN=" + readGlobal.NamingTemplate + "," + ouPath, targetTyp = (int)IAM_TargetType.FileSystem, @@ -251,6 +255,7 @@ namespace C4IT_IAM_Engine { Name = ownerDL.NamingTemplate, description = ownerDL.DescriptionTemplate, + WildcardPattern = ownerDL.WildcardTemplate, technicalName = "CN=" + ownerDL.NamingTemplate + "," + ouPath, targetTyp = (int)IAM_TargetType.FileSystem, @@ -265,6 +270,7 @@ namespace C4IT_IAM_Engine { Name = writeDL.NamingTemplate, description = writeDL.DescriptionTemplate, + WildcardPattern = writeDL.WildcardTemplate, technicalName = "CN=" + writeDL.NamingTemplate + "," + ouPath, targetTyp = (int)IAM_TargetType.FileSystem, @@ -279,6 +285,7 @@ namespace C4IT_IAM_Engine { Name = readDL.NamingTemplate, description = readDL.DescriptionTemplate, + WildcardPattern = readDL.WildcardTemplate, technicalName = "CN=" + readDL.NamingTemplate + "," + ouPath, targetTyp = (int)IAM_TargetType.FileSystem, @@ -293,6 +300,7 @@ namespace C4IT_IAM_Engine secGroup.description = secGroup.description.ReplaceLoopTag(0); secGroup.Name = secGroup.Name.ReplaceLoopTag(loop); secGroup.technicalName = secGroup.technicalName.ReplaceLoopTag(loop); + secGroup.WildcardPattern = secGroup.WildcardPattern.ReplaceLoopTag(loop); DefaultLogger.LogEntry(LogLevels.Debug, $"Security group generated: {secGroup.technicalName}"); } } @@ -398,6 +406,92 @@ namespace C4IT_IAM_Engine return groupEntry; } + private DirectoryEntry FindGroupEntryByWildcard(string ouPath, string wildcardPattern) + { + if (string.IsNullOrWhiteSpace(wildcardPattern)) + return null; + + Regex wildcardRegex; + try + { + wildcardRegex = new Regex(wildcardPattern, RegexOptions.IgnoreCase); + } + catch (Exception E) + { + cLogManager.DefaultLogger.LogException(E); + return null; + } + + var basePath = "LDAP://" + domainName; + if (!string.IsNullOrWhiteSpace(ouPath)) + basePath += "/" + ouPath; + + DirectoryEntry entry = new DirectoryEntry + { + Path = basePath, + Username = username, + Password = new NetworkCredential("", password).Password, + AuthenticationType = AuthenticationTypes.Secure | AuthenticationTypes.Sealing + }; + + DirectorySearcher search = new DirectorySearcher(entry) + { + Filter = "(objectClass=group)" + }; + search.PageSize = 100000; + search.PropertiesToLoad.Add("sAMAccountName"); + search.PropertiesToLoad.Add("distinguishedName"); + + string matchedName = null; + string matchedDistinguishedName = null; + var matchCount = 0; + + foreach (SearchResult result in search.FindAll()) + { + if (!result.Properties.Contains("sAMAccountName") || result.Properties["sAMAccountName"].Count == 0) + continue; + + var samAccountName = result.Properties["sAMAccountName"][0]?.ToString(); + if (string.IsNullOrWhiteSpace(samAccountName) || !wildcardRegex.IsMatch(samAccountName)) + continue; + + matchCount++; + if (matchCount > 1) + { + DefaultLogger.LogEntry(LogLevels.Warning, $"Multiple AD groups matched wildcard '{wildcardPattern}' in '{basePath}'. Regex-based reuse is skipped."); + search.Dispose(); + entry.Dispose(); + return null; + } + + matchedName = samAccountName; + matchedDistinguishedName = result.Properties.Contains("distinguishedName") && result.Properties["distinguishedName"].Count > 0 + ? result.Properties["distinguishedName"][0]?.ToString() + : null; + } + + search.Dispose(); + entry.Dispose(); + + if (string.IsNullOrWhiteSpace(matchedDistinguishedName)) + return null; + + DefaultLogger.LogEntry(LogLevels.Debug, $"Reusing existing AD group '{matchedName}' via wildcard '{wildcardPattern}'."); + return new DirectoryEntry("LDAP://" + domainName + "/" + matchedDistinguishedName, username, new NetworkCredential("", password).Password, AuthenticationTypes.Secure | AuthenticationTypes.Sealing); + } + + private void ApplyExistingGroup(IAM_SecurityGroup secGroup, DirectoryEntry existingGroup) + { + secGroup.ReusedExistingEntry = true; + secGroup.UID = getSID(existingGroup); + + if (existingGroup.Properties.Contains("sAMAccountName") && existingGroup.Properties["sAMAccountName"].Count > 0) + secGroup.Name = existingGroup.Properties["sAMAccountName"][0]?.ToString(); + + if (existingGroup.Properties.Contains("distinguishedName") && existingGroup.Properties["distinguishedName"].Count > 0) + secGroup.technicalName = existingGroup.Properties["distinguishedName"][0]?.ToString(); + } + private static bool HasMember(PropertyValueCollection members, string distinguishedName) { foreach (var member in members) @@ -450,13 +544,16 @@ namespace C4IT_IAM_Engine LogMethodBegin(MethodBase.GetCurrentMethod()); try { + secGroup.ReusedExistingEntry = false; var existingGroup = FindGroupEntry(secGroup.Name); + if (existingGroup == null && AllowExistingGroupWildcardMatch) + existingGroup = FindGroupEntryByWildcard(ouPath, secGroup.WildcardPattern); + if (existingGroup == null) return CreateADGroup(ouPath, secGroup, users); AddMissingMembers(existingGroup, secGroup, users); - var objectid = getSID(existingGroup); - secGroup.UID = objectid; + ApplyExistingGroup(secGroup, existingGroup); return existingGroup; } catch (Exception E) @@ -475,6 +572,7 @@ namespace C4IT_IAM_Engine LogMethodBegin(MethodBase.GetCurrentMethod()); try { + secGroup.ReusedExistingEntry = false; if (!GroupAllreadyExisting(secGroup.Name.ToUpper())) { @@ -518,9 +616,8 @@ namespace C4IT_IAM_Engine DirectoryEntry e = FindGroupEntry(secGroup.Name); if (e == null) return null; - var objectid = getSID(e); - secGroup.UID = objectid; AddMissingMembers(e, secGroup, users); + ApplyExistingGroup(secGroup, e); return e; } return null; @@ -588,6 +685,8 @@ namespace C4IT_IAM_Engine public string UID; public string Parent = ""; public string description; + public string WildcardPattern; + public bool ReusedExistingEntry; public List memberGroups; public string Name; public string technicalName;