From 2b460ccc1a1f730305b1486983522ac6460cdfd0 Mon Sep 17 00:00:00 2001 From: Meik Date: Fri, 8 May 2026 21:45:36 +0200 Subject: [PATCH] Add configurable NTFS group name formatting --- LiamNtfs/C4IT.LIAM.Ntfs.cs | 19 ++- LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs | 53 ++++++--- LiamNtfs/C4IT_IAM_SET/Helper.cs | 109 ++++++++++++++---- LiamNtfs/C4IT_IAM_SET/SecurityGroup.cs | 71 +++++++----- ...nalConfiguration_BlackWhitelist_Konzept.md | 31 ++++- ...FS_Massenverarbeitung_ADGruppen_Konzept.md | 1 + 6 files changed, 215 insertions(+), 69 deletions(-) diff --git a/LiamNtfs/C4IT.LIAM.Ntfs.cs b/LiamNtfs/C4IT.LIAM.Ntfs.cs index ea5907e..7e4d0e7 100644 --- a/LiamNtfs/C4IT.LIAM.Ntfs.cs +++ b/LiamNtfs/C4IT.LIAM.Ntfs.cs @@ -56,6 +56,8 @@ namespace C4IT.LIAM private const string AdditionalConfigurationExcludePathsKey = "NtfsExcludePaths"; private const string AdditionalConfigurationIncludePathsKey = "NtfsIncludePaths"; private const string AdditionalConfigurationTraverseBoundaryPathKey = "NtfsTraverseBoundaryPath"; + private const string AdditionalConfigurationGroupNameSanitizeReplacementKey = "NtfsGroupNameSanitizeReplacement"; + private const string AdditionalConfigurationPreserveAdGroupNameCaseKey = "PreserveNtfsAdGroupNameCase"; public readonly cNtfsBase ntfsBase = new cNtfsBase(); public readonly cActiveDirectoryBase activeDirectoryBase = new cActiveDirectoryBase(); private readonly Dictionary> publishedShareCache = new Dictionary>(StringComparer.OrdinalIgnoreCase); @@ -998,7 +1000,11 @@ namespace C4IT.LIAM groupGTag = GetRequiredCustomTag("Filesystem_GroupGlobalTag"), CanManagePermissionsForPath = IsPermissionManagedFolderPath, CanManageTraversePermissionsForPath = IsTraversePermissionManagedPath, - forceStrictAdGroupNames = IsAdditionalConfigurationEnabled("ForceStrictAdGroupNames") + forceStrictAdGroupNames = IsAdditionalConfigurationEnabled("ForceStrictAdGroupNames"), + groupNameSanitizeReplacement = GetAdditionalConfigurationValueOrDefault( + AdditionalConfigurationGroupNameSanitizeReplacementKey, + Helper.DefaultGroupNameSanitizeReplacement), + preserveAdGroupNameCase = IsAdditionalConfigurationEnabled(AdditionalConfigurationPreserveAdGroupNameCaseKey) }; engine.traverseBoundaryPath = GetAdditionalConfigurationValue(AdditionalConfigurationTraverseBoundaryPathKey); @@ -1032,6 +1038,17 @@ namespace C4IT.LIAM return rawValue.Trim(); } + private string GetAdditionalConfigurationValueOrDefault(string key, string defaultValue) + { + if (AdditionalConfiguration == null || string.IsNullOrWhiteSpace(key)) + return defaultValue; + + if (!AdditionalConfiguration.TryGetValue(key, out var rawValue)) + return defaultValue; + + return rawValue == null ? string.Empty : rawValue.Trim(); + } + public bool IsPermissionManagedFolderPath(string path) { return IsPermissionManagedPath(path, eNtfsPathKind.Folder); diff --git a/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs b/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs index 5c90cb9..0920f17 100644 --- a/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs +++ b/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs @@ -56,6 +56,8 @@ namespace C4IT_IAM_SET public Func CanManageTraversePermissionsForPath; public string traverseBoundaryPath; public bool forceStrictAdGroupNames; + public string groupNameSanitizeReplacement = Helper.DefaultGroupNameSanitizeReplacement; + public bool preserveAdGroupNameCase; public bool WhatIf; public int ReadACLPermission = 0x200A9; @@ -301,7 +303,8 @@ namespace C4IT_IAM_SET username = username, domainName = domainName, password = password, - ForceStrictAdGroupNames = forceStrictAdGroupNames + ForceStrictAdGroupNames = forceStrictAdGroupNames, + PreserveAdGroupNameCase = preserveAdGroupNameCase }; } @@ -579,14 +582,14 @@ namespace C4IT_IAM_SET DefaultLogger.LogEntry(LogLevels.Debug, $"relativePath vor Normalisierung: {relativePathRaw}"); var relativePathSegments = relativePathRaw.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); - var sanitizedSegments = relativePathSegments.Select(Helper.SanitizePathSegment).ToArray(); - var relativePath = sanitizedSegments.Length > 0 ? string.Join("_", sanitizedSegments) : string.Empty; + var sanitizedSegments = relativePathSegments.Select(i => Helper.SanitizePathSegment(i, groupNameSanitizeReplacement)).ToArray(); + var relativePath = sanitizedSegments.Length > 0 ? Helper.JoinSanitizedPathSegments(sanitizedSegments, groupNameSanitizeReplacement) : string.Empty; DefaultLogger.LogEntry(LogLevels.Debug, $"relativePath nach Normalisierung: {relativePath}"); var folderName = sanitizedSegments.Length > 0 ? sanitizedSegments[sanitizedSegments.Length - 1] - : Helper.SanitizePathSegment(Path.GetFileName(parent.FullName.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))); + : Helper.SanitizePathSegment(Path.GetFileName(parent.FullName.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)), groupNameSanitizeReplacement); var traverseTags = GetTraverseReplacementTags(parent.FullName); - var rootContext = Helper.GetRootPathTemplateContext(baseFolder); + var rootContext = Helper.GetRootPathTemplateContext(baseFolder, groupNameSanitizeReplacement); var boundedTraverseContext = Helper.GetBoundedAdGroupTemplateContext( traverseGroupTemplate.NamingTemplate, true, @@ -597,7 +600,9 @@ namespace C4IT_IAM_SET Helper.MaxAdGroupNameLength, $"Traverse fuer '{parent.FullName}'", "AD-Gruppenname", - rootContext); + rootContext, + preserveAdGroupNameCase, + groupNameSanitizeReplacement); var boundedTraverseDescriptionContext = Helper.GetBoundedAdGroupTemplateContext( traverseGroupTemplate.DescriptionTemplate, true, @@ -608,20 +613,28 @@ namespace C4IT_IAM_SET Helper.MaxAdGroupDescriptionLength, $"Traverse fuer '{parent.FullName}'", "AD-Gruppenbeschreibung", - rootContext); + rootContext, + preserveAdGroupNameCase, + groupNameSanitizeReplacement); var adjustedTraverseSegments = boundedTraverseContext.SanitizedSegments ?? Array.Empty(); - var adjustedTraverseRelativePath = adjustedTraverseSegments.Length > 0 ? string.Join("_", adjustedTraverseSegments) : string.Empty; + var adjustedTraverseRelativePath = adjustedTraverseSegments.Length > 0 ? Helper.JoinSanitizedPathSegments(adjustedTraverseSegments, groupNameSanitizeReplacement) : string.Empty; var adjustedTraverseFolderName = boundedTraverseContext.FolderName; var adjustedTraverseDescriptionSegments = boundedTraverseDescriptionContext.SanitizedSegments ?? Array.Empty(); - var adjustedTraverseDescriptionRelativePath = adjustedTraverseDescriptionSegments.Length > 0 ? string.Join("_", adjustedTraverseDescriptionSegments) : string.Empty; + var adjustedTraverseDescriptionRelativePath = adjustedTraverseDescriptionSegments.Length > 0 ? Helper.JoinSanitizedPathSegments(adjustedTraverseDescriptionSegments, groupNameSanitizeReplacement) : string.Empty; var adjustedTraverseDescriptionFolderName = boundedTraverseDescriptionContext.FolderName; - var traverseNameTemplate = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.NamingTemplate, true, adjustedTraverseRelativePath, adjustedTraverseSegments, adjustedTraverseFolderName, rootContext).ReplaceTags(traverseTags); - var traverseDescriptionTemplate = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.DescriptionTemplate, true, adjustedTraverseDescriptionRelativePath, adjustedTraverseDescriptionSegments, adjustedTraverseDescriptionFolderName, rootContext).ReplaceTags(traverseTags); + var traverseNameTemplate = Helper.ApplyAdGroupNameCasing( + Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.NamingTemplate, true, adjustedTraverseRelativePath, adjustedTraverseSegments, adjustedTraverseFolderName, rootContext, groupNameSanitizeReplacement).ReplaceTags(traverseTags), + preserveAdGroupNameCase); + var traverseDescriptionTemplate = Helper.ApplyAdGroupNameCasing( + Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.DescriptionTemplate, true, adjustedTraverseDescriptionRelativePath, adjustedTraverseDescriptionSegments, adjustedTraverseDescriptionFolderName, rootContext, groupNameSanitizeReplacement).ReplaceTags(traverseTags), + preserveAdGroupNameCase); string traverseRegex = null; try { - traverseRegex = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.WildcardTemplate, true, adjustedTraverseRelativePath, adjustedTraverseSegments, adjustedTraverseFolderName, rootContext).ReplaceTags(traverseTags); + traverseRegex = Helper.ApplyAdGroupNameCasing( + Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.WildcardTemplate, true, adjustedTraverseRelativePath, adjustedTraverseSegments, adjustedTraverseFolderName, rootContext, groupNameSanitizeReplacement).ReplaceTags(traverseTags), + preserveAdGroupNameCase); DefaultLogger.LogEntry(LogLevels.Debug, $"traverseRegex: {traverseRegex}"); } catch (Exception ex) @@ -721,7 +734,7 @@ namespace C4IT_IAM_SET DefaultLogger.LogEntry(LogLevels.Error, $"Fehler beim Erstellen von newTraverseGroup: {ex.Message}"); break; } - } while (newSecurityGroups.GroupAllreadyExisting(newTraverseGroup.Name.ToUpper()) && loop < 20); + } while (newSecurityGroups.GroupAllreadyExisting(newTraverseGroup.Name) && loop < 20); if (newTraverseGroup != null) { @@ -947,8 +960,8 @@ namespace C4IT_IAM_SET var visibleSegments = GetVisibleTraversePathSegments(currentPath); return new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "TRAVERSE_NAME", Helper.SanitizePathSegment(GetLastPathSegment(currentPath)) }, - { "TRAVERSE_VISIBLEPATH", string.Join("_", visibleSegments.Select(Helper.SanitizePathSegment)) } + { "TRAVERSE_NAME", Helper.SanitizePathSegment(GetLastPathSegment(currentPath), groupNameSanitizeReplacement) }, + { "TRAVERSE_VISIBLEPATH", Helper.JoinSanitizedPathSegments(visibleSegments.Select(i => Helper.SanitizePathSegment(i, groupNameSanitizeReplacement)), groupNameSanitizeReplacement) } }; } @@ -1319,7 +1332,10 @@ namespace C4IT_IAM_SET ReadACLPermission, WriteACLPermission, OwnerACLPermission, - 0); + 0, + 0, + groupNameSanitizeReplacement, + preserveAdGroupNameCase); List owners = getUserPrincipalBySid(ownerUserSids); List writers = getUserPrincipalBySid(writerUserSids); @@ -1482,7 +1498,10 @@ namespace C4IT_IAM_SET ReadACLPermission, WriteACLPermission, OwnerACLPermission, - existingADGroupCount); + existingADGroupCount, + 0, + groupNameSanitizeReplacement, + preserveAdGroupNameCase); /* if (existingADGroupCount > 0 && !templates.All(t => t.Type == SecurityGroupType.Traverse || Regex.IsMatch(t.NamingTemplate, @"(?{{(?[^}]*)(?LOOP)(?[^{]*)}})"))) { diff --git a/LiamNtfs/C4IT_IAM_SET/Helper.cs b/LiamNtfs/C4IT_IAM_SET/Helper.cs index 33f8c6b..329282d 100644 --- a/LiamNtfs/C4IT_IAM_SET/Helper.cs +++ b/LiamNtfs/C4IT_IAM_SET/Helper.cs @@ -14,6 +14,7 @@ namespace C4IT_IAM_Engine public const int MaxAdGroupNameLength = 64; public const int MaxAdGroupDescriptionLength = 1024; public const int MaxAdGroupLoopDigits = 3; + public const string DefaultGroupNameSanitizeReplacement = "_"; private const int MinLeadingRelativePathSegmentLength = 3; private const int MinSingleLeadingRelativePathSegmentLength = 2; private const int MinLastRelativePathSegmentLength = 12; @@ -34,6 +35,7 @@ namespace C4IT_IAM_Engine public string[] Segments { get; set; } = Array.Empty(); public string Name { get; set; } = string.Empty; public string Path { get; set; } = string.Empty; + public string PathSegmentSeparator { get; set; } = DefaultGroupNameSanitizeReplacement; } public static string ReplaceLoopTag(this string str, int loop) @@ -49,10 +51,15 @@ namespace C4IT_IAM_Engine } public static string ApplyTemplatePlaceholders(string templateValue, bool allowRelativePath, string defaultRelativePath, string[] sanitizedSegments, string folderName) { - return ApplyTemplatePlaceholders(templateValue, allowRelativePath, defaultRelativePath, sanitizedSegments, folderName, null); + return ApplyTemplatePlaceholders(templateValue, allowRelativePath, defaultRelativePath, sanitizedSegments, folderName, null, DefaultGroupNameSanitizeReplacement); } public static string ApplyTemplatePlaceholders(string templateValue, bool allowRelativePath, string defaultRelativePath, string[] sanitizedSegments, string folderName, RootPathTemplateContext rootContext) + { + return ApplyTemplatePlaceholders(templateValue, allowRelativePath, defaultRelativePath, sanitizedSegments, folderName, rootContext, DefaultGroupNameSanitizeReplacement); + } + + public static string ApplyTemplatePlaceholders(string templateValue, bool allowRelativePath, string defaultRelativePath, string[] sanitizedSegments, string folderName, RootPathTemplateContext rootContext, string pathSegmentSeparator) { if (templateValue == null) return string.Empty; @@ -75,7 +82,7 @@ namespace C4IT_IAM_Engine var segmentCount = Math.Min(sanitizedSegments.Length, segmentIndex + 1); var skip = sanitizedSegments.Length - segmentCount; - return string.Join("_", sanitizedSegments.Skip(skip)); + return JoinSanitizedPathSegments(sanitizedSegments.Skip(skip), pathSegmentSeparator); }, RegexOptions.IgnoreCase); } @@ -83,22 +90,28 @@ namespace C4IT_IAM_Engine } public static RootPathTemplateContext GetRootPathTemplateContext(string rootPath) + { + return GetRootPathTemplateContext(rootPath, DefaultGroupNameSanitizeReplacement); + } + + public static RootPathTemplateContext GetRootPathTemplateContext(string rootPath, string groupNameSanitizeReplacement) { var segments = SplitPathSegments(rootPath); if (segments.Length == 0) return new RootPathTemplateContext(); var isUncPath = (rootPath ?? string.Empty).Trim().Replace('/', '\\').StartsWith(@"\\", StringComparison.Ordinal); - var server = isUncPath ? SanitizePathSegment(segments[0]) : string.Empty; + var server = isUncPath ? SanitizePathSegment(segments[0], groupNameSanitizeReplacement) : string.Empty; var pathSegments = isUncPath ? segments.Skip(1).ToArray() : segments; - var sanitizedPathSegments = pathSegments.Select(SanitizePathSegment).ToArray(); + var sanitizedPathSegments = pathSegments.Select(i => SanitizePathSegment(i, groupNameSanitizeReplacement)).ToArray(); return new RootPathTemplateContext { Server = server, Segments = sanitizedPathSegments, Name = sanitizedPathSegments.Length == 0 ? string.Empty : sanitizedPathSegments[sanitizedPathSegments.Length - 1], - Path = sanitizedPathSegments.Length == 0 ? string.Empty : string.Join("_", sanitizedPathSegments) + Path = sanitizedPathSegments.Length == 0 ? string.Empty : JoinSanitizedPathSegments(sanitizedPathSegments, groupNameSanitizeReplacement), + PathSegmentSeparator = NormalizeGroupNameSanitizeReplacement(groupNameSanitizeReplacement) }; } @@ -112,13 +125,15 @@ namespace C4IT_IAM_Engine int maxLength, string logContext, string valueLabel = "AD-Gruppenname", - RootPathTemplateContext rootContext = null) + RootPathTemplateContext rootContext = null, + bool preserveCase = false, + string pathSegmentSeparator = DefaultGroupNameSanitizeReplacement) { var effectiveSegments = (sanitizedSegments ?? Array.Empty()).Where(i => i != null).ToArray(); var effectiveFolderName = folderName ?? string.Empty; - var currentRelativePath = GetCurrentRelativePath(effectiveSegments, defaultRelativePath); - var originalValue = MaterializeTemplateValue(templateValue, allowRelativePath, currentRelativePath, effectiveSegments, effectiveFolderName, replacementTags, rootContext); - var measuredValue = MaterializeTemplateValueForLength(templateValue, allowRelativePath, currentRelativePath, effectiveSegments, effectiveFolderName, replacementTags, rootContext); + var currentRelativePath = GetCurrentRelativePath(effectiveSegments, defaultRelativePath, pathSegmentSeparator); + var originalValue = MaterializeTemplateValue(templateValue, allowRelativePath, currentRelativePath, effectiveSegments, effectiveFolderName, replacementTags, rootContext, preserveCase, pathSegmentSeparator); + var measuredValue = MaterializeTemplateValueForLength(templateValue, allowRelativePath, currentRelativePath, effectiveSegments, effectiveFolderName, replacementTags, rootContext, preserveCase, pathSegmentSeparator); var usesRelativePath = allowRelativePath && Regex.IsMatch(templateValue ?? string.Empty, @"{{\s*RELATIVEPATH", RegexOptions.IgnoreCase); var usesName = Regex.IsMatch(templateValue ?? string.Empty, @"{{\s*NAME\s*}}", RegexOptions.IgnoreCase); var strategy = string.Empty; @@ -144,19 +159,21 @@ namespace C4IT_IAM_Engine if (!changed) break; - currentRelativePath = GetCurrentRelativePath(effectiveSegments, defaultRelativePath); - originalValue = MaterializeTemplateValue(templateValue, allowRelativePath, currentRelativePath, effectiveSegments, effectiveFolderName, replacementTags, rootContext); - measuredValue = MaterializeTemplateValueForLength(templateValue, allowRelativePath, currentRelativePath, effectiveSegments, effectiveFolderName, replacementTags, rootContext); + currentRelativePath = GetCurrentRelativePath(effectiveSegments, defaultRelativePath, pathSegmentSeparator); + originalValue = MaterializeTemplateValue(templateValue, allowRelativePath, currentRelativePath, effectiveSegments, effectiveFolderName, replacementTags, rootContext, preserveCase, pathSegmentSeparator); + measuredValue = MaterializeTemplateValueForLength(templateValue, allowRelativePath, currentRelativePath, effectiveSegments, effectiveFolderName, replacementTags, rootContext, preserveCase, pathSegmentSeparator); } var initialValue = MaterializeTemplateValue( templateValue, allowRelativePath, - GetCurrentRelativePath(sanitizedSegments, defaultRelativePath), + GetCurrentRelativePath(sanitizedSegments, defaultRelativePath, pathSegmentSeparator), sanitizedSegments, folderName, replacementTags, - rootContext); + rootContext, + preserveCase, + pathSegmentSeparator); var result = new BoundedTemplateContext { SanitizedSegments = effectiveSegments, @@ -184,11 +201,50 @@ namespace C4IT_IAM_Engine return result; } public static string SanitizePathSegment(string segment) + { + return SanitizePathSegment(segment, DefaultGroupNameSanitizeReplacement); + } + + public static string SanitizePathSegment(string segment, string groupNameSanitizeReplacement) { if (string.IsNullOrEmpty(segment)) return string.Empty; - return Regex.Replace(segment, @"[\s\-]", "_"); + var replacement = NormalizeGroupNameSanitizeReplacement(groupNameSanitizeReplacement); + return Regex.Replace(segment, @"[\s\-]", match => replacement); + } + + public static string NormalizeGroupNameSanitizeReplacement(string replacement) + { + if (replacement == null) + return DefaultGroupNameSanitizeReplacement; + + var trimmed = replacement.Trim(); + if (trimmed.Equals("", StringComparison.OrdinalIgnoreCase) + || trimmed.Equals("empty", StringComparison.OrdinalIgnoreCase) + || trimmed.Equals("none", StringComparison.OrdinalIgnoreCase) + || trimmed.Equals("remove", StringComparison.OrdinalIgnoreCase)) + { + return string.Empty; + } + + return trimmed; + } + + public static string JoinSanitizedPathSegments(IEnumerable sanitizedSegments, string groupNameSanitizeReplacement) + { + if (sanitizedSegments == null) + return string.Empty; + + return string.Join(NormalizeGroupNameSanitizeReplacement(groupNameSanitizeReplacement), sanitizedSegments); + } + + public static string ApplyAdGroupNameCasing(string value, bool preserveCase) + { + if (value == null) + return string.Empty; + + return preserveCase ? value : value.ToUpper(); } public static void CreatePathWithWriteAccess(string FilePath) { @@ -219,11 +275,14 @@ namespace C4IT_IAM_Engine string[] sanitizedSegments, string folderName, IDictionary replacementTags, - RootPathTemplateContext rootContext) + RootPathTemplateContext rootContext, + bool preserveCase, + string pathSegmentSeparator) { - return ApplyTemplatePlaceholders(templateValue, allowRelativePath, defaultRelativePath, sanitizedSegments, folderName, rootContext) - .ReplaceTags(replacementTags) - .ToUpper(); + var materializedValue = ApplyTemplatePlaceholders(templateValue, allowRelativePath, defaultRelativePath, sanitizedSegments, folderName, rootContext, pathSegmentSeparator) + .ReplaceTags(replacementTags); + + return ApplyAdGroupNameCasing(materializedValue, preserveCase); } private static string MaterializeTemplateValueForLength( @@ -233,10 +292,12 @@ namespace C4IT_IAM_Engine string[] sanitizedSegments, string folderName, IDictionary replacementTags, - RootPathTemplateContext rootContext) + RootPathTemplateContext rootContext, + bool preserveCase, + string pathSegmentSeparator) { return NormalizeLoopPlaceholderLength( - MaterializeTemplateValue(templateValue, allowRelativePath, defaultRelativePath, sanitizedSegments, folderName, replacementTags, rootContext)); + MaterializeTemplateValue(templateValue, allowRelativePath, defaultRelativePath, sanitizedSegments, folderName, replacementTags, rootContext, preserveCase, pathSegmentSeparator)); } private static string ApplyRootPathPlaceholders(string templateValue, RootPathTemplateContext rootContext) @@ -257,7 +318,7 @@ namespace C4IT_IAM_Engine return string.Empty; var take = Math.Min(segmentCount, segments.Length); - return take == 0 ? string.Empty : string.Join("_", segments.Skip(segments.Length - take)); + return take == 0 ? string.Empty : JoinSanitizedPathSegments(segments.Skip(segments.Length - take), context.PathSegmentSeparator); }, RegexOptions.IgnoreCase); result = Regex.Replace(result, @"{{\s*ROOT_SEGMENT\s*\(\s*(\d+)\s*\)\s*}}", match => { @@ -296,10 +357,10 @@ namespace C4IT_IAM_Engine .Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); } - private static string GetCurrentRelativePath(string[] sanitizedSegments, string fallbackRelativePath) + private static string GetCurrentRelativePath(string[] sanitizedSegments, string fallbackRelativePath, string pathSegmentSeparator) { if (sanitizedSegments != null && sanitizedSegments.Length > 0) - return string.Join("_", sanitizedSegments); + return JoinSanitizedPathSegments(sanitizedSegments, pathSegmentSeparator); return fallbackRelativePath ?? string.Empty; } diff --git a/LiamNtfs/C4IT_IAM_SET/SecurityGroup.cs b/LiamNtfs/C4IT_IAM_SET/SecurityGroup.cs index 8d2fa36..3e9bb92 100644 --- a/LiamNtfs/C4IT_IAM_SET/SecurityGroup.cs +++ b/LiamNtfs/C4IT_IAM_SET/SecurityGroup.cs @@ -24,6 +24,7 @@ namespace C4IT_IAM_Engine public string username; public SecureString password; public bool ForceStrictAdGroupNames; + public bool PreserveAdGroupNameCase; public List IAM_SecurityGroups; public string rootUID; @@ -53,7 +54,7 @@ namespace C4IT_IAM_Engine }; DirectorySearcher dSearch = new DirectorySearcher(entry) { - Filter = "(&(CN=" + s.Name.ToUpper() + ")(objectClass=group))" + Filter = "(&(CN=" + GetConfiguredGroupName(s.Name) + ")(objectClass=group))" }; dSearch.PageSize = 100000; SearchResultCollection sr = dSearch.FindAll(); @@ -92,7 +93,7 @@ namespace C4IT_IAM_Engine }; DirectorySearcher dSearch = new DirectorySearcher(entry) { - Filter = "(&(CN=" + CN.ToUpper() + ")(objectClass=group))" + Filter = "(&(CN=" + GetConfiguredGroupName(CN) + ")(objectClass=group))" }; dSearch.PageSize = 100000; SearchResultCollection sr = dSearch.FindAll(); @@ -129,7 +130,9 @@ namespace C4IT_IAM_Engine int writeACLPermission, int ownerACLPermission, int loop = 0, - int existingADGroupCount = 0) + int existingADGroupCount = 0, + string groupNameSanitizeReplacement = Helper.DefaultGroupNameSanitizeReplacement, + bool preserveAdGroupNameCase = false) { LogMethodBegin(MethodBase.GetCurrentMethod()); try @@ -145,12 +148,12 @@ namespace C4IT_IAM_Engine var relativePathRaw = DataArea.GetRelativePath(newFolderPath, baseFolder).Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); relativePathRaw = relativePathRaw.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); var relativePathSegments = relativePathRaw.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); - var sanitizedSegments = relativePathSegments.Select(Helper.SanitizePathSegment).ToArray(); - var relativePath = sanitizedSegments.Length > 0 ? string.Join("_", sanitizedSegments) : string.Empty; + var sanitizedSegments = relativePathSegments.Select(i => Helper.SanitizePathSegment(i, groupNameSanitizeReplacement)).ToArray(); + var relativePath = sanitizedSegments.Length > 0 ? Helper.JoinSanitizedPathSegments(sanitizedSegments, groupNameSanitizeReplacement) : string.Empty; var folderName = sanitizedSegments.Length > 0 ? sanitizedSegments[sanitizedSegments.Length - 1] - : Helper.SanitizePathSegment(Path.GetFileName(newFolderPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))); - var rootContext = Helper.GetRootPathTemplateContext(baseFolder); + : Helper.SanitizePathSegment(Path.GetFileName(newFolderPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)), groupNameSanitizeReplacement); + var rootContext = Helper.GetRootPathTemplateContext(baseFolder, groupNameSanitizeReplacement); foreach (var template in resolvedTemplates) { @@ -209,7 +212,9 @@ namespace C4IT_IAM_Engine Helper.MaxAdGroupNameLength, $"{template.Type}/{template.Scope} fuer '{newFolderPath}'", "AD-Gruppenname", - rootContext); + rootContext, + preserveAdGroupNameCase, + groupNameSanitizeReplacement); var boundedDescriptionContext = Helper.GetBoundedAdGroupTemplateContext( template.DescriptionTemplate, @@ -221,27 +226,32 @@ namespace C4IT_IAM_Engine Helper.MaxAdGroupDescriptionLength, $"{template.Type}/{template.Scope} fuer '{newFolderPath}'", "AD-Gruppenbeschreibung", - rootContext); + rootContext, + preserveAdGroupNameCase, + groupNameSanitizeReplacement); var adjustedNameSegments = boundedNameContext.SanitizedSegments ?? Array.Empty(); - var adjustedNameRelativePath = adjustedNameSegments.Length > 0 ? string.Join("_", adjustedNameSegments) : string.Empty; + var adjustedNameRelativePath = adjustedNameSegments.Length > 0 ? Helper.JoinSanitizedPathSegments(adjustedNameSegments, groupNameSanitizeReplacement) : string.Empty; var adjustedNameFolderName = boundedNameContext.FolderName; var adjustedDescriptionSegments = boundedDescriptionContext.SanitizedSegments ?? Array.Empty(); - var adjustedDescriptionRelativePath = adjustedDescriptionSegments.Length > 0 ? string.Join("_", adjustedDescriptionSegments) : string.Empty; + var adjustedDescriptionRelativePath = adjustedDescriptionSegments.Length > 0 ? Helper.JoinSanitizedPathSegments(adjustedDescriptionSegments, groupNameSanitizeReplacement) : string.Empty; var adjustedDescriptionFolderName = boundedDescriptionContext.FolderName; - template.NamingTemplate = Helper.ApplyTemplatePlaceholders(template.NamingTemplate, template.Type != SecurityGroupType.Traverse, adjustedNameRelativePath, adjustedNameSegments, adjustedNameFolderName, rootContext) - .ReplaceTags(customTags).ReplaceTags(tags) - .ToUpper(); + template.NamingTemplate = Helper.ApplyAdGroupNameCasing( + Helper.ApplyTemplatePlaceholders(template.NamingTemplate, template.Type != SecurityGroupType.Traverse, adjustedNameRelativePath, adjustedNameSegments, adjustedNameFolderName, rootContext, groupNameSanitizeReplacement) + .ReplaceTags(customTags).ReplaceTags(tags), + preserveAdGroupNameCase); - template.DescriptionTemplate = Helper.ApplyTemplatePlaceholders(template.DescriptionTemplate, template.Type != SecurityGroupType.Traverse, adjustedDescriptionRelativePath, adjustedDescriptionSegments, adjustedDescriptionFolderName, rootContext) - .ReplaceTags(customTags).ReplaceTags(tags) - .ToUpper(); + template.DescriptionTemplate = Helper.ApplyAdGroupNameCasing( + Helper.ApplyTemplatePlaceholders(template.DescriptionTemplate, template.Type != SecurityGroupType.Traverse, adjustedDescriptionRelativePath, adjustedDescriptionSegments, adjustedDescriptionFolderName, rootContext, groupNameSanitizeReplacement) + .ReplaceTags(customTags).ReplaceTags(tags), + preserveAdGroupNameCase); - template.WildcardTemplate = Helper.ApplyTemplatePlaceholders(template.WildcardTemplate, template.Type != SecurityGroupType.Traverse, adjustedNameRelativePath, adjustedNameSegments, adjustedNameFolderName, rootContext) - .ReplaceTags(customTags).ReplaceTags(tags) - .ToUpper(); + template.WildcardTemplate = Helper.ApplyAdGroupNameCasing( + Helper.ApplyTemplatePlaceholders(template.WildcardTemplate, template.Type != SecurityGroupType.Traverse, adjustedNameRelativePath, adjustedNameSegments, adjustedNameFolderName, rootContext, groupNameSanitizeReplacement) + .ReplaceTags(customTags).ReplaceTags(tags), + preserveAdGroupNameCase); } @@ -429,7 +439,7 @@ namespace C4IT_IAM_Engine DirectorySearcher search = new DirectorySearcher(entry) { - Filter = "(&(objectClass=group)(sAMAccountName=" + groupName.ToUpper() + "))" + Filter = "(&(objectClass=group)(sAMAccountName=" + GetConfiguredGroupName(groupName) + "))" }; search.PageSize = 100000; @@ -718,13 +728,17 @@ namespace C4IT_IAM_Engine try { secGroup.CreatedNewEntry = false; - if (!GroupAllreadyExisting(secGroup.Name.ToUpper())) + var groupName = GetConfiguredGroupName(secGroup.Name); + secGroup.Name = groupName; + secGroup.technicalName = "CN=" + groupName + "," + ouPath; + + if (!GroupAllreadyExisting(groupName)) { DirectoryEntry entry = new DirectoryEntry("LDAP://" + domainName + "/" + ouPath, username, new NetworkCredential("", password).Password, AuthenticationTypes.Secure | AuthenticationTypes.Sealing); - DefaultLogger.LogEntry(LogLevels.Debug, $"Creating ad entry with CN / sAmAccountName: {secGroup.Name.ToUpper()}"); - DirectoryEntry group = entry.Children.Add("CN=" + secGroup.Name.ToUpper(), "group"); - group.Properties["sAmAccountName"].Value = secGroup.Name.ToUpper(); + DefaultLogger.LogEntry(LogLevels.Debug, $"Creating ad entry with CN / sAmAccountName: {groupName}"); + DirectoryEntry group = entry.Children.Add("CN=" + groupName, "group"); + group.Properties["sAmAccountName"].Value = groupName; if (users != null && secGroup.Scope == GroupScope.Global) { foreach (var user in users) @@ -749,7 +763,7 @@ namespace C4IT_IAM_Engine } group.CommitChanges(); - DirectoryEntry ent = new DirectoryEntry("LDAP://" + domainName + "/" + "CN =" + secGroup.Name.ToUpper() + "," + ouPath, username, new NetworkCredential("", password).Password, AuthenticationTypes.Secure | AuthenticationTypes.Sealing); + DirectoryEntry ent = new DirectoryEntry("LDAP://" + domainName + "/" + "CN=" + groupName + "," + ouPath, username, new NetworkCredential("", password).Password, AuthenticationTypes.Secure | AuthenticationTypes.Sealing); var objectid = SecurityGroups.getSID(ent); DefaultLogger.LogEntry(LogLevels.Debug, $"Security group created in ad: {secGroup.technicalName}"); @@ -778,6 +792,11 @@ namespace C4IT_IAM_Engine LogMethodEnd(MethodBase.GetCurrentMethod()); } } + + private string GetConfiguredGroupName(string groupName) + { + return Helper.ApplyAdGroupNameCasing(groupName, PreserveAdGroupNameCase); + } } public enum GroupScopeValues : int { diff --git a/Sonstiges/LIAM_NTFS_AdditionalConfiguration_BlackWhitelist_Konzept.md b/Sonstiges/LIAM_NTFS_AdditionalConfiguration_BlackWhitelist_Konzept.md index 89d4e89..eb1e248 100644 --- a/Sonstiges/LIAM_NTFS_AdditionalConfiguration_BlackWhitelist_Konzept.md +++ b/Sonstiges/LIAM_NTFS_AdditionalConfiguration_BlackWhitelist_Konzept.md @@ -203,7 +203,7 @@ stehen folgende Root-Platzhalter zur Verfuegung: - `{{ROOT_SEGMENT(0)}}`: erstes Root-Segment nach dem Server, z.B. `file_shares` - `{{ROOT_SEGMENT(1)}}`: zweites Root-Segment nach dem Server, z.B. `share2` -Root-Segmente werden wie Ordnersegmente sanitisiert. Leerzeichen und Bindestriche werden zu `_`. Nicht vorhandene `ROOT_SEGMENT(n)`-Werte werden zu einem leeren String. Wenn `ROOT_PATH(n)` mehr Segmente anfordert als vorhanden sind, werden alle vorhandenen Root-Segmente verwendet. +Root-Segmente werden wie Ordnersegmente sanitisiert. Leerzeichen und Bindestriche werden standardmaessig zu `_`. Nicht vorhandene `ROOT_SEGMENT(n)`-Werte werden zu einem leeren String. Wenn `ROOT_PATH(n)` mehr Segmente anfordert als vorhanden sind, werden alle vorhandenen Root-Segmente verwendet. Beispiel: @@ -231,6 +231,35 @@ ACL_FILE_SHARES_SHARE2.TEST33_O Die bestehenden Platzhalter `{{NAME}}`, `{{RELATIVEPATH}}`, `{{TRAVERSE_NAME}}` und `{{TRAVERSE_VISIBLEPATH}}` bleiben unveraendert. +### 11. Konfigurierbares Sanitizing und Gross-/Kleinschreibung fuer NTFS-Gruppennamen + +Die Normalisierung der dynamischen Pfadbestandteile wird ueber `AdditionalConfiguration` gesteuert. Die Werte kommen wie `EnsureNtfsPermissionGroups` aus `C4IT_GCC_DataArea_Collector_AdditionalAttributes`. + +`NtfsGroupNameSanitizeReplacement` steuert das Ersatz-/Trennzeichen fuer dynamische Pfadbestandteile: + +- nicht gesetzt: bisheriges Verhalten, Leerzeichen und Bindestriche werden durch `_` ersetzt und Pfadsegmente werden mit `_` verbunden +- gesetzt auf z.B. `.`: Leerzeichen und Bindestriche werden durch `.` ersetzt und Pfadsegmente werden mit `.` verbunden +- gesetzt auf einen leeren Wert, ``, `empty`, `none` oder `remove`: Leerzeichen/Bindestriche werden entfernt und Pfadsegmente ohne Trennzeichen verbunden + +Die Einstellung wirkt auf `{{NAME}}`, `{{RELATIVEPATH}}`, `{{ROOT_*}}`, `{{TRAVERSE_NAME}}` und `{{TRAVERSE_VISIBLEPATH}}`. Sie aendert nicht die statischen Zeichen, die direkt im Naming Template stehen. Soll z.B. zwischen Root und Ordner immer ein Punkt stehen, bleibt der Punkt Bestandteil des Templates. + +Beispiel: + +```text +RootPath=\\SRVWSM001.imagoverum.com\file_shares\share2 +Zielpfad=\\SRVWSM001.imagoverum.com\file_shares\share2\test-33 +NamingTemplate={{ADGroupPrefix}}_{{ROOT_NAME}}.{{NAME}}{{GROUPTYPEPOSTFIX}} +NtfsGroupNameSanitizeReplacement= +``` + +ergibt bei deaktivierter automatischer Grossschreibung: + +```text +ACL_share2.test33_O +``` + +`PreserveNtfsAdGroupNameCase=1` unterbindet die bisher automatische Grossschreibung der erzeugten AD-Gruppennamen. Ohne diesen Schalter bleibt das bisherige Verhalten erhalten und die generierten CN-/sAMAccountName-Werte werden in Grossbuchstaben erzeugt. + ## Matching-Regeln Empfohlene Semantik: diff --git a/Sonstiges/LIAM_NTFS_Massenverarbeitung_ADGruppen_Konzept.md b/Sonstiges/LIAM_NTFS_Massenverarbeitung_ADGruppen_Konzept.md index 20a13c5..ee3980e 100644 --- a/Sonstiges/LIAM_NTFS_Massenverarbeitung_ADGruppen_Konzept.md +++ b/Sonstiges/LIAM_NTFS_Massenverarbeitung_ADGruppen_Konzept.md @@ -39,6 +39,7 @@ Das bedeutet: - kein Caching ueber den gesamten Lauf - Owner-/Write-/Read-Gruppen sowie Traverse-Gruppen werden analog zur Ordner-Neuanlage sichergestellt - eine optionale Traverse-Grenze kann ueber `NtfsTraverseBoundaryPath` aus `AdditionalConfiguration` gesetzt werden +- das Sanitizing dynamischer Pfadbestandteile kann ueber `NtfsGroupNameSanitizeReplacement` angepasst werden; `PreserveNtfsAdGroupNameCase=1` unterbindet die automatische Grossschreibung neuer AD-Gruppennamen ### 2. Create-/Ensure-Pfad