From fc8e9070118ef83646c08b537ad36f63df3ffb7f Mon Sep 17 00:00:00 2001 From: Meik Date: Fri, 8 May 2026 21:25:00 +0200 Subject: [PATCH] Align NTFS ensure traverse handling --- LiamNtfs/C4IT.LIAM.Ntfs.cs | 31 ++ LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs | 280 +++++++++++++++++- LiamWorkflowActivities/LiamWorkflowRuntime.cs | 2 +- 3 files changed, 297 insertions(+), 16 deletions(-) diff --git a/LiamNtfs/C4IT.LIAM.Ntfs.cs b/LiamNtfs/C4IT.LIAM.Ntfs.cs index 1a6139f..ea5907e 100644 --- a/LiamNtfs/C4IT.LIAM.Ntfs.cs +++ b/LiamNtfs/C4IT.LIAM.Ntfs.cs @@ -55,6 +55,7 @@ namespace C4IT.LIAM public static Guid nftsModuleId = new Guid("77e213a1-6517-ea11-4881-000c2980fd94"); private const string AdditionalConfigurationExcludePathsKey = "NtfsExcludePaths"; private const string AdditionalConfigurationIncludePathsKey = "NtfsIncludePaths"; + private const string AdditionalConfigurationTraverseBoundaryPathKey = "NtfsTraverseBoundaryPath"; public readonly cNtfsBase ntfsBase = new cNtfsBase(); public readonly cActiveDirectoryBase activeDirectoryBase = new cActiveDirectoryBase(); private readonly Dictionary> publishedShareCache = new Dictionary>(StringComparer.OrdinalIgnoreCase); @@ -996,8 +997,10 @@ namespace C4IT.LIAM groupDLTag = requiresDomainLocalTag ? GetRequiredCustomTag("Filesystem_GroupDomainLocalTag") : string.Empty, groupGTag = GetRequiredCustomTag("Filesystem_GroupGlobalTag"), CanManagePermissionsForPath = IsPermissionManagedFolderPath, + CanManageTraversePermissionsForPath = IsTraversePermissionManagedPath, forceStrictAdGroupNames = IsAdditionalConfigurationEnabled("ForceStrictAdGroupNames") }; + engine.traverseBoundaryPath = GetAdditionalConfigurationValue(AdditionalConfigurationTraverseBoundaryPathKey); foreach (var template in BuildSecurityGroupTemplates()) engine.templates.Add(template); @@ -1018,6 +1021,17 @@ namespace C4IT.LIAM || rawValue.Equals("yes", StringComparison.OrdinalIgnoreCase); } + private string GetAdditionalConfigurationValue(string key) + { + if (AdditionalConfiguration == null || string.IsNullOrWhiteSpace(key)) + return string.Empty; + + if (!AdditionalConfiguration.TryGetValue(key, out var rawValue) || string.IsNullOrWhiteSpace(rawValue)) + return string.Empty; + + return rawValue.Trim(); + } + public bool IsPermissionManagedFolderPath(string path) { return IsPermissionManagedPath(path, eNtfsPathKind.Folder); @@ -1042,6 +1056,23 @@ namespace C4IT.LIAM return IsPathWhitelisted(classification, false, out matchingConfigurationKey, out matchingRule); } + private bool IsTraversePermissionManagedPath(string path) + { + if (string.IsNullOrWhiteSpace(GetAdditionalConfigurationValue(AdditionalConfigurationTraverseBoundaryPathKey))) + return IsPermissionManagedFolderPath(path); + + var classification = ClassifyPath(path); + if (classification == null || classification.Kind == eNtfsPathKind.ServerRoot || classification.Kind == eNtfsPathKind.Unknown) + return false; + + string matchingConfigurationKey; + string matchingRule; + if (IsPathBlacklisted(classification, out matchingConfigurationKey, out matchingRule)) + return false; + + return Directory.Exists(path); + } + private static bool IsSupportedPermissionManagedPathKind(cNtfsPathClassification classification, params eNtfsPathKind[] supportedKinds) { if (classification == null || supportedKinds == null || supportedKinds.Length == 0) diff --git a/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs b/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs index 4d753a1..d149044 100644 --- a/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs +++ b/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs @@ -53,6 +53,8 @@ namespace C4IT_IAM_SET public ICollection readerUserSids; public ICollection writerUserSids; public Func CanManagePermissionsForPath; + public Func CanManageTraversePermissionsForPath; + public string traverseBoundaryPath; public bool forceStrictAdGroupNames; public bool WhatIf; @@ -147,6 +149,10 @@ namespace C4IT_IAM_SET DefaultLogger.LogEntry(LogLevels.Info, $"Establishing connection to {baseFolder}, User: {username}, Password: {Helper.MaskAllButLastAndFirst(new NetworkCredential("", password).Password)}"); using (Connection = new cNetworkConnection(baseFolder, username, new NetworkCredential("", password).Password)) { + var traverseBoundaryResult = ValidateTraverseBoundaryForCurrentFolder(); + if (traverseBoundaryResult.resultErrorId != 0) + return traverseBoundaryResult; + var folderCheckResult = checkFolder(); if (folderCheckResult.resultErrorId == 0) { @@ -299,6 +305,39 @@ namespace C4IT_IAM_SET }; } + private ResultToken ValidateTraverseBoundaryForCurrentFolder() + { + var resultToken = new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString()); + resultToken.resultErrorId = 0; + + var boundaryPath = GetNormalizedTraverseBoundaryPath(); + if (string.IsNullOrWhiteSpace(boundaryPath)) + return resultToken; + + var targetParent = new DirectoryInfo(newFolderPath).Parent; + if (targetParent == null) + { + resultToken.resultErrorId = 30009; + resultToken.resultMessage = $"Traverse boundary '{traverseBoundaryPath}' cannot be validated because '{newFolderPath}' has no parent directory."; + return resultToken; + } + + if (!Directory.Exists(boundaryPath)) + { + resultToken.resultErrorId = 30009; + resultToken.resultMessage = $"Traverse boundary '{traverseBoundaryPath}' does not exist or is not reachable."; + return resultToken; + } + + if (!IsSameOrAncestorPath(boundaryPath, targetParent.FullName)) + { + resultToken.resultErrorId = 30009; + resultToken.resultMessage = $"Traverse boundary '{traverseBoundaryPath}' is not a parent path of '{newFolderPath}'."; + } + + return resultToken; + } + public ResultToken ensureDataAreaPermissions(bool ensureTraverseGroups = false) { LogMethodBegin(MethodBase.GetCurrentMethod()); @@ -327,6 +366,10 @@ namespace C4IT_IAM_SET InitializeFolderContext(); + var traverseBoundaryResult = ValidateTraverseBoundaryForCurrentFolder(); + if (traverseBoundaryResult.resultErrorId != 0) + return traverseBoundaryResult; + ensureADGroups(resultToken); resultToken = ensureFolderPermissions(resultToken); @@ -424,6 +467,10 @@ namespace C4IT_IAM_SET var lvl = DataArea.GetRelativePath(parent.FullName, baseFolder).Count(n => n == Path.DirectorySeparatorChar); DefaultLogger.LogEntry(LogLevels.Debug, $"Ebene (lvl): {lvl}"); + var currentTraverseLevel = lvl; + var defaultTraverseLoopIndex = lvl; + var hasTraverseBoundary = !string.IsNullOrWhiteSpace(GetNormalizedTraverseBoundaryPath()); + var processedNearestTraverseParent = false; // Überprüfen der Templates if (templates == null) @@ -472,9 +519,9 @@ namespace C4IT_IAM_SET return resultToken; } - for (int i = lvl; i >= createTraverseGroupLvl; i--) + while (parent != null && (hasTraverseBoundary || defaultTraverseLoopIndex >= createTraverseGroupLvl)) { - DefaultLogger.LogEntry(LogLevels.Debug, $"Verarbeite Ebene {i}."); + DefaultLogger.LogEntry(LogLevels.Debug, $"Verarbeite Ebene {currentTraverseLevel}."); if (parent == null) { @@ -482,14 +529,22 @@ namespace C4IT_IAM_SET break; } - if (CanManagePermissionsForPath != null && !CanManagePermissionsForPath(parent.FullName)) + var canManageTraversePath = CanManageTraversePermissionsForPath ?? CanManagePermissionsForPath; + if (canManageTraversePath != null && !canManageTraversePath(parent.FullName)) { DefaultLogger.LogEntry(LogLevels.Debug, $"Überspringe Traverse-Verarbeitung für nicht verwaltbaren NTFS-Pfad: {parent.FullName}"); + if (IsTraverseBoundaryPath(parent.FullName)) + break; + parent = parent.Parent; if (parent != null) { - lvl = DataArea.GetRelativePath(parent.FullName, baseFolder).Count(n => n == Path.DirectorySeparatorChar); - DefaultLogger.LogEntry(LogLevels.Debug, $"Neue Ebene (lvl) nach Überspringen: {lvl}"); + currentTraverseLevel = hasTraverseBoundary + ? currentTraverseLevel + 1 + : DataArea.GetRelativePath(parent.FullName, baseFolder).Count(n => n == Path.DirectorySeparatorChar); + if (!hasTraverseBoundary) + defaultTraverseLoopIndex--; + DefaultLogger.LogEntry(LogLevels.Debug, $"Neue Ebene (lvl) nach Überspringen: {currentTraverseLevel}"); } else { @@ -530,13 +585,14 @@ namespace C4IT_IAM_SET var folderName = sanitizedSegments.Length > 0 ? sanitizedSegments[sanitizedSegments.Length - 1] : Helper.SanitizePathSegment(Path.GetFileName(parent.FullName.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))); + var traverseTags = GetTraverseReplacementTags(parent.FullName); var boundedTraverseContext = Helper.GetBoundedAdGroupTemplateContext( traverseGroupTemplate.NamingTemplate, true, relativePath, sanitizedSegments, folderName, - null, + traverseTags, Helper.MaxAdGroupNameLength, $"Traverse fuer '{parent.FullName}'"); var boundedTraverseDescriptionContext = Helper.GetBoundedAdGroupTemplateContext( @@ -545,7 +601,7 @@ namespace C4IT_IAM_SET relativePath, sanitizedSegments, folderName, - null, + traverseTags, Helper.MaxAdGroupDescriptionLength, $"Traverse fuer '{parent.FullName}'", "AD-Gruppenbeschreibung"); @@ -555,13 +611,13 @@ namespace C4IT_IAM_SET var adjustedTraverseDescriptionSegments = boundedTraverseDescriptionContext.SanitizedSegments ?? Array.Empty(); var adjustedTraverseDescriptionRelativePath = adjustedTraverseDescriptionSegments.Length > 0 ? string.Join("_", adjustedTraverseDescriptionSegments) : string.Empty; var adjustedTraverseDescriptionFolderName = boundedTraverseDescriptionContext.FolderName; - var traverseNameTemplate = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.NamingTemplate, true, adjustedTraverseRelativePath, adjustedTraverseSegments, adjustedTraverseFolderName); - var traverseDescriptionTemplate = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.DescriptionTemplate, true, adjustedTraverseDescriptionRelativePath, adjustedTraverseDescriptionSegments, adjustedTraverseDescriptionFolderName); + var traverseNameTemplate = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.NamingTemplate, true, adjustedTraverseRelativePath, adjustedTraverseSegments, adjustedTraverseFolderName).ReplaceTags(traverseTags); + var traverseDescriptionTemplate = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.DescriptionTemplate, true, adjustedTraverseDescriptionRelativePath, adjustedTraverseDescriptionSegments, adjustedTraverseDescriptionFolderName).ReplaceTags(traverseTags); string traverseRegex = null; try { - traverseRegex = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.WildcardTemplate, true, adjustedTraverseRelativePath, adjustedTraverseSegments, adjustedTraverseFolderName); + traverseRegex = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.WildcardTemplate, true, adjustedTraverseRelativePath, adjustedTraverseSegments, adjustedTraverseFolderName).ReplaceTags(traverseTags); DefaultLogger.LogEntry(LogLevels.Debug, $"traverseRegex: {traverseRegex}"); } catch (Exception ex) @@ -570,8 +626,12 @@ namespace C4IT_IAM_SET continue; } + var hasTraverseWildcard = !string.IsNullOrWhiteSpace(traverseRegex); foreach (FileSystemAccessRule acl in ACLs) { + if (!hasTraverseWildcard) + break; + var searchString = acl.IdentityReference.Value; var aclSplit = searchString.Split('\\'); if (aclSplit.Length == 2) @@ -600,7 +660,18 @@ namespace C4IT_IAM_SET break; } - if (parentTraverseGroup == null && !string.IsNullOrEmpty(traverseNameTemplate)) + if (parentTraverseGroup == null && hasTraverseWildcard && !forceStrictAdGroupNames) + { + parentTraverseGroup = FindTraverseGroupByWildcard(domainContext, traverseRegex); + if (parentTraverseGroup != null) + { + resultToken.reusedGroups.Add(parentTraverseGroup.Name); + resultToken.ensuredTraverseGroups.Add(parentTraverseGroup.Name); + DefaultLogger.LogEntry(LogLevels.Debug, $"Vorhandene Traverse-Gruppe per Wildcard wiederverwendet: {parentTraverseGroup.Name}"); + } + } + + if (parentTraverseGroup == null && !string.IsNullOrWhiteSpace(traverseNameTemplate)) { for (var loop = 0; loop < 20; loop++) { @@ -616,7 +687,7 @@ namespace C4IT_IAM_SET } } - if (parentTraverseGroup == null && !traverseGroupTemplate.NamingTemplate.Equals(string.Empty)) + if (parentTraverseGroup == null && !string.IsNullOrWhiteSpace(traverseNameTemplate)) { DefaultLogger.LogEntry(LogLevels.Debug, "Erstelle neue TraverseGroup."); if (newSecurityGroups == null) @@ -750,7 +821,7 @@ namespace C4IT_IAM_SET if (parentTraverseGroup != null) { - if (i == lvl) + if (!processedNearestTraverseParent) { DefaultLogger.LogEntry(LogLevels.Debug, "Verarbeite SecurityGroups bei oberster Ebene."); foreach (var currentSecGroup in newSecurityGroups.IAM_SecurityGroups) @@ -773,6 +844,7 @@ namespace C4IT_IAM_SET continue; } traverseGroup = parentTraverseGroup; + processedNearestTraverseParent = true; } else { @@ -821,12 +893,19 @@ namespace C4IT_IAM_SET if (parentTraverseGroup != null && !resultToken.ensuredTraverseGroups.Contains(parentTraverseGroup.Name)) resultToken.ensuredTraverseGroups.Add(parentTraverseGroup.Name); + if (IsTraverseBoundaryPath(parent.FullName)) + break; + // Aktualisiere parent und lvl für die nächste Iteration parent = parent.Parent; if (parent != null) { - lvl = DataArea.GetRelativePath(parent.FullName, baseFolder).Count(n => n == Path.DirectorySeparatorChar); - DefaultLogger.LogEntry(LogLevels.Debug, $"Neue Ebene (lvl) nach Aktualisierung: {lvl}"); + currentTraverseLevel = hasTraverseBoundary + ? currentTraverseLevel + 1 + : DataArea.GetRelativePath(parent.FullName, baseFolder).Count(n => n == Path.DirectorySeparatorChar); + if (!hasTraverseBoundary) + defaultTraverseLoopIndex--; + DefaultLogger.LogEntry(LogLevels.Debug, $"Neue Ebene (lvl) nach Aktualisierung: {currentTraverseLevel}"); } else { @@ -847,6 +926,177 @@ namespace C4IT_IAM_SET } } + private string GetNormalizedTraverseBoundaryPath() + { + return NormalizeDirectoryPath(traverseBoundaryPath); + } + + private bool IsTraverseBoundaryPath(string path) + { + var boundaryPath = GetNormalizedTraverseBoundaryPath(); + return !string.IsNullOrWhiteSpace(boundaryPath) + && PathsEqual(boundaryPath, path); + } + + private Dictionary GetTraverseReplacementTags(string currentPath) + { + var visibleSegments = GetVisibleTraversePathSegments(currentPath); + return new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "TRAVERSE_NAME", Helper.SanitizePathSegment(GetLastPathSegment(currentPath)) }, + { "TRAVERSE_VISIBLEPATH", string.Join("_", visibleSegments.Select(Helper.SanitizePathSegment)) } + }; + } + + private IEnumerable GetVisibleTraversePathSegments(string currentPath) + { + var normalizedCurrentPath = NormalizeDirectoryPath(currentPath); + var boundaryPath = GetNormalizedTraverseBoundaryPath(); + if (string.IsNullOrWhiteSpace(boundaryPath)) + boundaryPath = NormalizeDirectoryPath(baseFolder); + + var visibleRoot = GetParentPath(boundaryPath); + if (string.IsNullOrWhiteSpace(visibleRoot)) + visibleRoot = boundaryPath; + + var currentSegments = SplitPathSegments(normalizedCurrentPath); + var rootSegments = SplitPathSegments(visibleRoot); + if (currentSegments.Length <= rootSegments.Length) + return currentSegments; + + var isRootPrefix = rootSegments + .Select((segment, index) => new { segment, index }) + .All(i => string.Equals(i.segment, currentSegments[i.index], StringComparison.OrdinalIgnoreCase)); + return isRootPrefix + ? currentSegments.Skip(rootSegments.Length) + : currentSegments; + } + + private GroupPrincipal FindTraverseGroupByWildcard(PrincipalContext domainContext, string wildcardPattern) + { + if (domainContext == null || 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(groupOUPath)) + basePath += "/" + groupOUPath; + + 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("objectSid"); + + string matchedSid = null; + string matchedName = 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 traverse wildcard '{wildcardPattern}' in '{basePath}'. Regex-based reuse is skipped."); + search.Dispose(); + entry.Dispose(); + return null; + } + + matchedName = samAccountName; + matchedSid = result.Properties.Contains("objectSid") && result.Properties["objectSid"].Count > 0 + ? new SecurityIdentifier((byte[])result.Properties["objectSid"][0], 0).Value + : null; + } + + search.Dispose(); + entry.Dispose(); + + if (string.IsNullOrWhiteSpace(matchedSid)) + return null; + + DefaultLogger.LogEntry(LogLevels.Debug, $"Reusing existing traverse AD group '{matchedName}' via wildcard '{wildcardPattern}'."); + return GroupPrincipal.FindByIdentity(domainContext, IdentityType.Sid, matchedSid); + } + + private static string NormalizeDirectoryPath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return string.Empty; + + var normalized = path.Trim().Replace('/', '\\').TrimEnd('\\'); + if (normalized.StartsWith(@"\\", StringComparison.Ordinal)) + return @"\\" + string.Join("\\", SplitPathSegments(normalized)); + + return normalized; + } + + private static bool PathsEqual(string left, string right) + { + return string.Equals(NormalizeDirectoryPath(left), NormalizeDirectoryPath(right), StringComparison.OrdinalIgnoreCase); + } + + private static bool IsSameOrAncestorPath(string ancestorPath, string path) + { + var ancestor = NormalizeDirectoryPath(ancestorPath); + var current = NormalizeDirectoryPath(path); + return string.Equals(ancestor, current, StringComparison.OrdinalIgnoreCase) + || current.StartsWith(ancestor + "\\", StringComparison.OrdinalIgnoreCase); + } + + private static string GetParentPath(string path) + { + var segments = SplitPathSegments(path); + if (segments.Length <= 1) + return string.Empty; + + if (NormalizeDirectoryPath(path).StartsWith(@"\\", StringComparison.Ordinal)) + return @"\\" + string.Join("\\", segments.Take(segments.Length - 1)); + + return string.Join("\\", segments.Take(segments.Length - 1)); + } + + private static string GetLastPathSegment(string path) + { + var segments = SplitPathSegments(path); + return segments.Length == 0 ? string.Empty : segments[segments.Length - 1]; + } + + private static string[] SplitPathSegments(string path) + { + return (path ?? string.Empty) + .Trim() + .Replace('/', '\\') + .Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); + } + private bool TryEnsureGlobalGroupMembershipWithRetry(PrincipalContext domainContext, GroupPrincipal parentTraverseGroup, IAM_SecurityGroup currentSecGroup) { if (domainContext == null || parentTraverseGroup == null || currentSecGroup == null || string.IsNullOrWhiteSpace(currentSecGroup.UID)) diff --git a/LiamWorkflowActivities/LiamWorkflowRuntime.cs b/LiamWorkflowActivities/LiamWorkflowRuntime.cs index 3d05cf1..0cc5205 100644 --- a/LiamWorkflowActivities/LiamWorkflowRuntime.cs +++ b/LiamWorkflowActivities/LiamWorkflowRuntime.cs @@ -447,7 +447,7 @@ namespace LiamWorkflowActivities null, null, allowSharePathEnsure, - false, + ntfsArea is cLiamNtfsFolder, simulateOnly); if (ensureResult == null) {