Align NTFS ensure traverse handling

This commit is contained in:
Meik
2026-05-08 21:25:00 +02:00
parent 898ed7dd8e
commit fc8e907011
3 changed files with 297 additions and 16 deletions

View File

@@ -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<string, HashSet<string>> publishedShareCache = new Dictionary<string, HashSet<string>>(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)

View File

@@ -53,6 +53,8 @@ namespace C4IT_IAM_SET
public ICollection<string> readerUserSids;
public ICollection<string> writerUserSids;
public Func<string, bool> CanManagePermissionsForPath;
public Func<string, bool> 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<string>();
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<string, string> GetTraverseReplacementTags(string currentPath)
{
var visibleSegments = GetVisibleTraversePathSegments(currentPath);
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "TRAVERSE_NAME", Helper.SanitizePathSegment(GetLastPathSegment(currentPath)) },
{ "TRAVERSE_VISIBLEPATH", string.Join("_", visibleSegments.Select(Helper.SanitizePathSegment)) }
};
}
private IEnumerable<string> 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))