Add configurable NTFS group name formatting

This commit is contained in:
Meik
2026-05-08 21:45:36 +02:00
parent b9edd16cab
commit 2b460ccc1a
6 changed files with 215 additions and 69 deletions

View File

@@ -56,6 +56,8 @@ namespace C4IT.LIAM
private const string AdditionalConfigurationExcludePathsKey = "NtfsExcludePaths"; private const string AdditionalConfigurationExcludePathsKey = "NtfsExcludePaths";
private const string AdditionalConfigurationIncludePathsKey = "NtfsIncludePaths"; private const string AdditionalConfigurationIncludePathsKey = "NtfsIncludePaths";
private const string AdditionalConfigurationTraverseBoundaryPathKey = "NtfsTraverseBoundaryPath"; private const string AdditionalConfigurationTraverseBoundaryPathKey = "NtfsTraverseBoundaryPath";
private const string AdditionalConfigurationGroupNameSanitizeReplacementKey = "NtfsGroupNameSanitizeReplacement";
private const string AdditionalConfigurationPreserveAdGroupNameCaseKey = "PreserveNtfsAdGroupNameCase";
public readonly cNtfsBase ntfsBase = new cNtfsBase(); public readonly cNtfsBase ntfsBase = new cNtfsBase();
public readonly cActiveDirectoryBase activeDirectoryBase = new cActiveDirectoryBase(); public readonly cActiveDirectoryBase activeDirectoryBase = new cActiveDirectoryBase();
private readonly Dictionary<string, HashSet<string>> publishedShareCache = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, HashSet<string>> publishedShareCache = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
@@ -998,7 +1000,11 @@ namespace C4IT.LIAM
groupGTag = GetRequiredCustomTag("Filesystem_GroupGlobalTag"), groupGTag = GetRequiredCustomTag("Filesystem_GroupGlobalTag"),
CanManagePermissionsForPath = IsPermissionManagedFolderPath, CanManagePermissionsForPath = IsPermissionManagedFolderPath,
CanManageTraversePermissionsForPath = IsTraversePermissionManagedPath, CanManageTraversePermissionsForPath = IsTraversePermissionManagedPath,
forceStrictAdGroupNames = IsAdditionalConfigurationEnabled("ForceStrictAdGroupNames") forceStrictAdGroupNames = IsAdditionalConfigurationEnabled("ForceStrictAdGroupNames"),
groupNameSanitizeReplacement = GetAdditionalConfigurationValueOrDefault(
AdditionalConfigurationGroupNameSanitizeReplacementKey,
Helper.DefaultGroupNameSanitizeReplacement),
preserveAdGroupNameCase = IsAdditionalConfigurationEnabled(AdditionalConfigurationPreserveAdGroupNameCaseKey)
}; };
engine.traverseBoundaryPath = GetAdditionalConfigurationValue(AdditionalConfigurationTraverseBoundaryPathKey); engine.traverseBoundaryPath = GetAdditionalConfigurationValue(AdditionalConfigurationTraverseBoundaryPathKey);
@@ -1032,6 +1038,17 @@ namespace C4IT.LIAM
return rawValue.Trim(); 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) public bool IsPermissionManagedFolderPath(string path)
{ {
return IsPermissionManagedPath(path, eNtfsPathKind.Folder); return IsPermissionManagedPath(path, eNtfsPathKind.Folder);

View File

@@ -56,6 +56,8 @@ namespace C4IT_IAM_SET
public Func<string, bool> CanManageTraversePermissionsForPath; public Func<string, bool> CanManageTraversePermissionsForPath;
public string traverseBoundaryPath; public string traverseBoundaryPath;
public bool forceStrictAdGroupNames; public bool forceStrictAdGroupNames;
public string groupNameSanitizeReplacement = Helper.DefaultGroupNameSanitizeReplacement;
public bool preserveAdGroupNameCase;
public bool WhatIf; public bool WhatIf;
public int ReadACLPermission = 0x200A9; public int ReadACLPermission = 0x200A9;
@@ -301,7 +303,8 @@ namespace C4IT_IAM_SET
username = username, username = username,
domainName = domainName, domainName = domainName,
password = password, password = password,
ForceStrictAdGroupNames = forceStrictAdGroupNames ForceStrictAdGroupNames = forceStrictAdGroupNames,
PreserveAdGroupNameCase = preserveAdGroupNameCase
}; };
} }
@@ -579,14 +582,14 @@ namespace C4IT_IAM_SET
DefaultLogger.LogEntry(LogLevels.Debug, $"relativePath vor Normalisierung: {relativePathRaw}"); DefaultLogger.LogEntry(LogLevels.Debug, $"relativePath vor Normalisierung: {relativePathRaw}");
var relativePathSegments = relativePathRaw.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); var relativePathSegments = relativePathRaw.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
var sanitizedSegments = relativePathSegments.Select(Helper.SanitizePathSegment).ToArray(); var sanitizedSegments = relativePathSegments.Select(i => Helper.SanitizePathSegment(i, groupNameSanitizeReplacement)).ToArray();
var relativePath = sanitizedSegments.Length > 0 ? string.Join("_", sanitizedSegments) : string.Empty; var relativePath = sanitizedSegments.Length > 0 ? Helper.JoinSanitizedPathSegments(sanitizedSegments, groupNameSanitizeReplacement) : string.Empty;
DefaultLogger.LogEntry(LogLevels.Debug, $"relativePath nach Normalisierung: {relativePath}"); DefaultLogger.LogEntry(LogLevels.Debug, $"relativePath nach Normalisierung: {relativePath}");
var folderName = sanitizedSegments.Length > 0 var folderName = sanitizedSegments.Length > 0
? sanitizedSegments[sanitizedSegments.Length - 1] ? 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 traverseTags = GetTraverseReplacementTags(parent.FullName);
var rootContext = Helper.GetRootPathTemplateContext(baseFolder); var rootContext = Helper.GetRootPathTemplateContext(baseFolder, groupNameSanitizeReplacement);
var boundedTraverseContext = Helper.GetBoundedAdGroupTemplateContext( var boundedTraverseContext = Helper.GetBoundedAdGroupTemplateContext(
traverseGroupTemplate.NamingTemplate, traverseGroupTemplate.NamingTemplate,
true, true,
@@ -597,7 +600,9 @@ namespace C4IT_IAM_SET
Helper.MaxAdGroupNameLength, Helper.MaxAdGroupNameLength,
$"Traverse fuer '{parent.FullName}'", $"Traverse fuer '{parent.FullName}'",
"AD-Gruppenname", "AD-Gruppenname",
rootContext); rootContext,
preserveAdGroupNameCase,
groupNameSanitizeReplacement);
var boundedTraverseDescriptionContext = Helper.GetBoundedAdGroupTemplateContext( var boundedTraverseDescriptionContext = Helper.GetBoundedAdGroupTemplateContext(
traverseGroupTemplate.DescriptionTemplate, traverseGroupTemplate.DescriptionTemplate,
true, true,
@@ -608,20 +613,28 @@ namespace C4IT_IAM_SET
Helper.MaxAdGroupDescriptionLength, Helper.MaxAdGroupDescriptionLength,
$"Traverse fuer '{parent.FullName}'", $"Traverse fuer '{parent.FullName}'",
"AD-Gruppenbeschreibung", "AD-Gruppenbeschreibung",
rootContext); rootContext,
preserveAdGroupNameCase,
groupNameSanitizeReplacement);
var adjustedTraverseSegments = boundedTraverseContext.SanitizedSegments ?? Array.Empty<string>(); var adjustedTraverseSegments = boundedTraverseContext.SanitizedSegments ?? Array.Empty<string>();
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 adjustedTraverseFolderName = boundedTraverseContext.FolderName;
var adjustedTraverseDescriptionSegments = boundedTraverseDescriptionContext.SanitizedSegments ?? Array.Empty<string>(); var adjustedTraverseDescriptionSegments = boundedTraverseDescriptionContext.SanitizedSegments ?? Array.Empty<string>();
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 adjustedTraverseDescriptionFolderName = boundedTraverseDescriptionContext.FolderName;
var traverseNameTemplate = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.NamingTemplate, true, adjustedTraverseRelativePath, adjustedTraverseSegments, adjustedTraverseFolderName, rootContext).ReplaceTags(traverseTags); var traverseNameTemplate = Helper.ApplyAdGroupNameCasing(
var traverseDescriptionTemplate = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.DescriptionTemplate, true, adjustedTraverseDescriptionRelativePath, adjustedTraverseDescriptionSegments, adjustedTraverseDescriptionFolderName, rootContext).ReplaceTags(traverseTags); 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; string traverseRegex = null;
try 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}"); DefaultLogger.LogEntry(LogLevels.Debug, $"traverseRegex: {traverseRegex}");
} }
catch (Exception ex) catch (Exception ex)
@@ -721,7 +734,7 @@ namespace C4IT_IAM_SET
DefaultLogger.LogEntry(LogLevels.Error, $"Fehler beim Erstellen von newTraverseGroup: {ex.Message}"); DefaultLogger.LogEntry(LogLevels.Error, $"Fehler beim Erstellen von newTraverseGroup: {ex.Message}");
break; break;
} }
} while (newSecurityGroups.GroupAllreadyExisting(newTraverseGroup.Name.ToUpper()) && loop < 20); } while (newSecurityGroups.GroupAllreadyExisting(newTraverseGroup.Name) && loop < 20);
if (newTraverseGroup != null) if (newTraverseGroup != null)
{ {
@@ -947,8 +960,8 @@ namespace C4IT_IAM_SET
var visibleSegments = GetVisibleTraversePathSegments(currentPath); var visibleSegments = GetVisibleTraversePathSegments(currentPath);
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{ {
{ "TRAVERSE_NAME", Helper.SanitizePathSegment(GetLastPathSegment(currentPath)) }, { "TRAVERSE_NAME", Helper.SanitizePathSegment(GetLastPathSegment(currentPath), groupNameSanitizeReplacement) },
{ "TRAVERSE_VISIBLEPATH", string.Join("_", visibleSegments.Select(Helper.SanitizePathSegment)) } { "TRAVERSE_VISIBLEPATH", Helper.JoinSanitizedPathSegments(visibleSegments.Select(i => Helper.SanitizePathSegment(i, groupNameSanitizeReplacement)), groupNameSanitizeReplacement) }
}; };
} }
@@ -1319,7 +1332,10 @@ namespace C4IT_IAM_SET
ReadACLPermission, ReadACLPermission,
WriteACLPermission, WriteACLPermission,
OwnerACLPermission, OwnerACLPermission,
0); 0,
0,
groupNameSanitizeReplacement,
preserveAdGroupNameCase);
List<UserPrincipal> owners = getUserPrincipalBySid(ownerUserSids); List<UserPrincipal> owners = getUserPrincipalBySid(ownerUserSids);
List<UserPrincipal> writers = getUserPrincipalBySid(writerUserSids); List<UserPrincipal> writers = getUserPrincipalBySid(writerUserSids);
@@ -1482,7 +1498,10 @@ namespace C4IT_IAM_SET
ReadACLPermission, ReadACLPermission,
WriteACLPermission, WriteACLPermission,
OwnerACLPermission, OwnerACLPermission,
existingADGroupCount); existingADGroupCount,
0,
groupNameSanitizeReplacement,
preserveAdGroupNameCase);
/* /*
if (existingADGroupCount > 0 && !templates.All(t => t.Type == SecurityGroupType.Traverse || Regex.IsMatch(t.NamingTemplate, @"(?<loopTag>{{(?<prefix>[^}]*)(?<loop>LOOP)(?<postfix>[^{]*)}})"))) if (existingADGroupCount > 0 && !templates.All(t => t.Type == SecurityGroupType.Traverse || Regex.IsMatch(t.NamingTemplate, @"(?<loopTag>{{(?<prefix>[^}]*)(?<loop>LOOP)(?<postfix>[^{]*)}})")))
{ {

View File

@@ -14,6 +14,7 @@ namespace C4IT_IAM_Engine
public const int MaxAdGroupNameLength = 64; public const int MaxAdGroupNameLength = 64;
public const int MaxAdGroupDescriptionLength = 1024; public const int MaxAdGroupDescriptionLength = 1024;
public const int MaxAdGroupLoopDigits = 3; public const int MaxAdGroupLoopDigits = 3;
public const string DefaultGroupNameSanitizeReplacement = "_";
private const int MinLeadingRelativePathSegmentLength = 3; private const int MinLeadingRelativePathSegmentLength = 3;
private const int MinSingleLeadingRelativePathSegmentLength = 2; private const int MinSingleLeadingRelativePathSegmentLength = 2;
private const int MinLastRelativePathSegmentLength = 12; private const int MinLastRelativePathSegmentLength = 12;
@@ -34,6 +35,7 @@ namespace C4IT_IAM_Engine
public string[] Segments { get; set; } = Array.Empty<string>(); public string[] Segments { get; set; } = Array.Empty<string>();
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string Path { 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) 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) 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) 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) if (templateValue == null)
return string.Empty; return string.Empty;
@@ -75,7 +82,7 @@ namespace C4IT_IAM_Engine
var segmentCount = Math.Min(sanitizedSegments.Length, segmentIndex + 1); var segmentCount = Math.Min(sanitizedSegments.Length, segmentIndex + 1);
var skip = sanitizedSegments.Length - segmentCount; var skip = sanitizedSegments.Length - segmentCount;
return string.Join("_", sanitizedSegments.Skip(skip)); return JoinSanitizedPathSegments(sanitizedSegments.Skip(skip), pathSegmentSeparator);
}, RegexOptions.IgnoreCase); }, RegexOptions.IgnoreCase);
} }
@@ -83,22 +90,28 @@ namespace C4IT_IAM_Engine
} }
public static RootPathTemplateContext GetRootPathTemplateContext(string rootPath) public static RootPathTemplateContext GetRootPathTemplateContext(string rootPath)
{
return GetRootPathTemplateContext(rootPath, DefaultGroupNameSanitizeReplacement);
}
public static RootPathTemplateContext GetRootPathTemplateContext(string rootPath, string groupNameSanitizeReplacement)
{ {
var segments = SplitPathSegments(rootPath); var segments = SplitPathSegments(rootPath);
if (segments.Length == 0) if (segments.Length == 0)
return new RootPathTemplateContext(); return new RootPathTemplateContext();
var isUncPath = (rootPath ?? string.Empty).Trim().Replace('/', '\\').StartsWith(@"\\", StringComparison.Ordinal); 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 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 return new RootPathTemplateContext
{ {
Server = server, Server = server,
Segments = sanitizedPathSegments, Segments = sanitizedPathSegments,
Name = sanitizedPathSegments.Length == 0 ? string.Empty : sanitizedPathSegments[sanitizedPathSegments.Length - 1], 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, int maxLength,
string logContext, string logContext,
string valueLabel = "AD-Gruppenname", string valueLabel = "AD-Gruppenname",
RootPathTemplateContext rootContext = null) RootPathTemplateContext rootContext = null,
bool preserveCase = false,
string pathSegmentSeparator = DefaultGroupNameSanitizeReplacement)
{ {
var effectiveSegments = (sanitizedSegments ?? Array.Empty<string>()).Where(i => i != null).ToArray(); var effectiveSegments = (sanitizedSegments ?? Array.Empty<string>()).Where(i => i != null).ToArray();
var effectiveFolderName = folderName ?? string.Empty; var effectiveFolderName = folderName ?? string.Empty;
var currentRelativePath = GetCurrentRelativePath(effectiveSegments, defaultRelativePath); var currentRelativePath = GetCurrentRelativePath(effectiveSegments, defaultRelativePath, pathSegmentSeparator);
var originalValue = MaterializeTemplateValue(templateValue, allowRelativePath, currentRelativePath, effectiveSegments, effectiveFolderName, replacementTags, rootContext); var originalValue = MaterializeTemplateValue(templateValue, allowRelativePath, currentRelativePath, effectiveSegments, effectiveFolderName, replacementTags, rootContext, preserveCase, pathSegmentSeparator);
var measuredValue = MaterializeTemplateValueForLength(templateValue, allowRelativePath, currentRelativePath, effectiveSegments, effectiveFolderName, replacementTags, rootContext); 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 usesRelativePath = allowRelativePath && Regex.IsMatch(templateValue ?? string.Empty, @"{{\s*RELATIVEPATH", RegexOptions.IgnoreCase);
var usesName = Regex.IsMatch(templateValue ?? string.Empty, @"{{\s*NAME\s*}}", RegexOptions.IgnoreCase); var usesName = Regex.IsMatch(templateValue ?? string.Empty, @"{{\s*NAME\s*}}", RegexOptions.IgnoreCase);
var strategy = string.Empty; var strategy = string.Empty;
@@ -144,19 +159,21 @@ namespace C4IT_IAM_Engine
if (!changed) if (!changed)
break; break;
currentRelativePath = GetCurrentRelativePath(effectiveSegments, defaultRelativePath); currentRelativePath = GetCurrentRelativePath(effectiveSegments, defaultRelativePath, pathSegmentSeparator);
originalValue = MaterializeTemplateValue(templateValue, allowRelativePath, currentRelativePath, effectiveSegments, effectiveFolderName, replacementTags, rootContext); originalValue = MaterializeTemplateValue(templateValue, allowRelativePath, currentRelativePath, effectiveSegments, effectiveFolderName, replacementTags, rootContext, preserveCase, pathSegmentSeparator);
measuredValue = MaterializeTemplateValueForLength(templateValue, allowRelativePath, currentRelativePath, effectiveSegments, effectiveFolderName, replacementTags, rootContext); measuredValue = MaterializeTemplateValueForLength(templateValue, allowRelativePath, currentRelativePath, effectiveSegments, effectiveFolderName, replacementTags, rootContext, preserveCase, pathSegmentSeparator);
} }
var initialValue = MaterializeTemplateValue( var initialValue = MaterializeTemplateValue(
templateValue, templateValue,
allowRelativePath, allowRelativePath,
GetCurrentRelativePath(sanitizedSegments, defaultRelativePath), GetCurrentRelativePath(sanitizedSegments, defaultRelativePath, pathSegmentSeparator),
sanitizedSegments, sanitizedSegments,
folderName, folderName,
replacementTags, replacementTags,
rootContext); rootContext,
preserveCase,
pathSegmentSeparator);
var result = new BoundedTemplateContext var result = new BoundedTemplateContext
{ {
SanitizedSegments = effectiveSegments, SanitizedSegments = effectiveSegments,
@@ -184,11 +201,50 @@ namespace C4IT_IAM_Engine
return result; return result;
} }
public static string SanitizePathSegment(string segment) public static string SanitizePathSegment(string segment)
{
return SanitizePathSegment(segment, DefaultGroupNameSanitizeReplacement);
}
public static string SanitizePathSegment(string segment, string groupNameSanitizeReplacement)
{ {
if (string.IsNullOrEmpty(segment)) if (string.IsNullOrEmpty(segment))
return string.Empty; 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("<empty>", 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<string> 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) public static void CreatePathWithWriteAccess(string FilePath)
{ {
@@ -219,11 +275,14 @@ namespace C4IT_IAM_Engine
string[] sanitizedSegments, string[] sanitizedSegments,
string folderName, string folderName,
IDictionary<string, string> replacementTags, IDictionary<string, string> replacementTags,
RootPathTemplateContext rootContext) RootPathTemplateContext rootContext,
bool preserveCase,
string pathSegmentSeparator)
{ {
return ApplyTemplatePlaceholders(templateValue, allowRelativePath, defaultRelativePath, sanitizedSegments, folderName, rootContext) var materializedValue = ApplyTemplatePlaceholders(templateValue, allowRelativePath, defaultRelativePath, sanitizedSegments, folderName, rootContext, pathSegmentSeparator)
.ReplaceTags(replacementTags) .ReplaceTags(replacementTags);
.ToUpper();
return ApplyAdGroupNameCasing(materializedValue, preserveCase);
} }
private static string MaterializeTemplateValueForLength( private static string MaterializeTemplateValueForLength(
@@ -233,10 +292,12 @@ namespace C4IT_IAM_Engine
string[] sanitizedSegments, string[] sanitizedSegments,
string folderName, string folderName,
IDictionary<string, string> replacementTags, IDictionary<string, string> replacementTags,
RootPathTemplateContext rootContext) RootPathTemplateContext rootContext,
bool preserveCase,
string pathSegmentSeparator)
{ {
return NormalizeLoopPlaceholderLength( 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) private static string ApplyRootPathPlaceholders(string templateValue, RootPathTemplateContext rootContext)
@@ -257,7 +318,7 @@ namespace C4IT_IAM_Engine
return string.Empty; return string.Empty;
var take = Math.Min(segmentCount, segments.Length); 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); }, RegexOptions.IgnoreCase);
result = Regex.Replace(result, @"{{\s*ROOT_SEGMENT\s*\(\s*(\d+)\s*\)\s*}}", match => 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); .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) if (sanitizedSegments != null && sanitizedSegments.Length > 0)
return string.Join("_", sanitizedSegments); return JoinSanitizedPathSegments(sanitizedSegments, pathSegmentSeparator);
return fallbackRelativePath ?? string.Empty; return fallbackRelativePath ?? string.Empty;
} }

View File

@@ -24,6 +24,7 @@ namespace C4IT_IAM_Engine
public string username; public string username;
public SecureString password; public SecureString password;
public bool ForceStrictAdGroupNames; public bool ForceStrictAdGroupNames;
public bool PreserveAdGroupNameCase;
public List<IAM_SecurityGroup> IAM_SecurityGroups; public List<IAM_SecurityGroup> IAM_SecurityGroups;
public string rootUID; public string rootUID;
@@ -53,7 +54,7 @@ namespace C4IT_IAM_Engine
}; };
DirectorySearcher dSearch = new DirectorySearcher(entry) DirectorySearcher dSearch = new DirectorySearcher(entry)
{ {
Filter = "(&(CN=" + s.Name.ToUpper() + ")(objectClass=group))" Filter = "(&(CN=" + GetConfiguredGroupName(s.Name) + ")(objectClass=group))"
}; };
dSearch.PageSize = 100000; dSearch.PageSize = 100000;
SearchResultCollection sr = dSearch.FindAll(); SearchResultCollection sr = dSearch.FindAll();
@@ -92,7 +93,7 @@ namespace C4IT_IAM_Engine
}; };
DirectorySearcher dSearch = new DirectorySearcher(entry) DirectorySearcher dSearch = new DirectorySearcher(entry)
{ {
Filter = "(&(CN=" + CN.ToUpper() + ")(objectClass=group))" Filter = "(&(CN=" + GetConfiguredGroupName(CN) + ")(objectClass=group))"
}; };
dSearch.PageSize = 100000; dSearch.PageSize = 100000;
SearchResultCollection sr = dSearch.FindAll(); SearchResultCollection sr = dSearch.FindAll();
@@ -129,7 +130,9 @@ namespace C4IT_IAM_Engine
int writeACLPermission, int writeACLPermission,
int ownerACLPermission, int ownerACLPermission,
int loop = 0, int loop = 0,
int existingADGroupCount = 0) int existingADGroupCount = 0,
string groupNameSanitizeReplacement = Helper.DefaultGroupNameSanitizeReplacement,
bool preserveAdGroupNameCase = false)
{ {
LogMethodBegin(MethodBase.GetCurrentMethod()); LogMethodBegin(MethodBase.GetCurrentMethod());
try try
@@ -145,12 +148,12 @@ namespace C4IT_IAM_Engine
var relativePathRaw = DataArea.GetRelativePath(newFolderPath, baseFolder).Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); var relativePathRaw = DataArea.GetRelativePath(newFolderPath, baseFolder).Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
relativePathRaw = relativePathRaw.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); relativePathRaw = relativePathRaw.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
var relativePathSegments = relativePathRaw.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); var relativePathSegments = relativePathRaw.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
var sanitizedSegments = relativePathSegments.Select(Helper.SanitizePathSegment).ToArray(); var sanitizedSegments = relativePathSegments.Select(i => Helper.SanitizePathSegment(i, groupNameSanitizeReplacement)).ToArray();
var relativePath = sanitizedSegments.Length > 0 ? string.Join("_", sanitizedSegments) : string.Empty; var relativePath = sanitizedSegments.Length > 0 ? Helper.JoinSanitizedPathSegments(sanitizedSegments, groupNameSanitizeReplacement) : string.Empty;
var folderName = sanitizedSegments.Length > 0 var folderName = sanitizedSegments.Length > 0
? sanitizedSegments[sanitizedSegments.Length - 1] ? sanitizedSegments[sanitizedSegments.Length - 1]
: Helper.SanitizePathSegment(Path.GetFileName(newFolderPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))); : Helper.SanitizePathSegment(Path.GetFileName(newFolderPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)), groupNameSanitizeReplacement);
var rootContext = Helper.GetRootPathTemplateContext(baseFolder); var rootContext = Helper.GetRootPathTemplateContext(baseFolder, groupNameSanitizeReplacement);
foreach (var template in resolvedTemplates) foreach (var template in resolvedTemplates)
{ {
@@ -209,7 +212,9 @@ namespace C4IT_IAM_Engine
Helper.MaxAdGroupNameLength, Helper.MaxAdGroupNameLength,
$"{template.Type}/{template.Scope} fuer '{newFolderPath}'", $"{template.Type}/{template.Scope} fuer '{newFolderPath}'",
"AD-Gruppenname", "AD-Gruppenname",
rootContext); rootContext,
preserveAdGroupNameCase,
groupNameSanitizeReplacement);
var boundedDescriptionContext = Helper.GetBoundedAdGroupTemplateContext( var boundedDescriptionContext = Helper.GetBoundedAdGroupTemplateContext(
template.DescriptionTemplate, template.DescriptionTemplate,
@@ -221,27 +226,32 @@ namespace C4IT_IAM_Engine
Helper.MaxAdGroupDescriptionLength, Helper.MaxAdGroupDescriptionLength,
$"{template.Type}/{template.Scope} fuer '{newFolderPath}'", $"{template.Type}/{template.Scope} fuer '{newFolderPath}'",
"AD-Gruppenbeschreibung", "AD-Gruppenbeschreibung",
rootContext); rootContext,
preserveAdGroupNameCase,
groupNameSanitizeReplacement);
var adjustedNameSegments = boundedNameContext.SanitizedSegments ?? Array.Empty<string>(); var adjustedNameSegments = boundedNameContext.SanitizedSegments ?? Array.Empty<string>();
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 adjustedNameFolderName = boundedNameContext.FolderName;
var adjustedDescriptionSegments = boundedDescriptionContext.SanitizedSegments ?? Array.Empty<string>(); var adjustedDescriptionSegments = boundedDescriptionContext.SanitizedSegments ?? Array.Empty<string>();
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; var adjustedDescriptionFolderName = boundedDescriptionContext.FolderName;
template.NamingTemplate = Helper.ApplyTemplatePlaceholders(template.NamingTemplate, template.Type != SecurityGroupType.Traverse, adjustedNameRelativePath, adjustedNameSegments, adjustedNameFolderName, rootContext) template.NamingTemplate = Helper.ApplyAdGroupNameCasing(
.ReplaceTags(customTags).ReplaceTags(tags) Helper.ApplyTemplatePlaceholders(template.NamingTemplate, template.Type != SecurityGroupType.Traverse, adjustedNameRelativePath, adjustedNameSegments, adjustedNameFolderName, rootContext, groupNameSanitizeReplacement)
.ToUpper(); .ReplaceTags(customTags).ReplaceTags(tags),
preserveAdGroupNameCase);
template.DescriptionTemplate = Helper.ApplyTemplatePlaceholders(template.DescriptionTemplate, template.Type != SecurityGroupType.Traverse, adjustedDescriptionRelativePath, adjustedDescriptionSegments, adjustedDescriptionFolderName, rootContext) template.DescriptionTemplate = Helper.ApplyAdGroupNameCasing(
.ReplaceTags(customTags).ReplaceTags(tags) Helper.ApplyTemplatePlaceholders(template.DescriptionTemplate, template.Type != SecurityGroupType.Traverse, adjustedDescriptionRelativePath, adjustedDescriptionSegments, adjustedDescriptionFolderName, rootContext, groupNameSanitizeReplacement)
.ToUpper(); .ReplaceTags(customTags).ReplaceTags(tags),
preserveAdGroupNameCase);
template.WildcardTemplate = Helper.ApplyTemplatePlaceholders(template.WildcardTemplate, template.Type != SecurityGroupType.Traverse, adjustedNameRelativePath, adjustedNameSegments, adjustedNameFolderName, rootContext) template.WildcardTemplate = Helper.ApplyAdGroupNameCasing(
.ReplaceTags(customTags).ReplaceTags(tags) Helper.ApplyTemplatePlaceholders(template.WildcardTemplate, template.Type != SecurityGroupType.Traverse, adjustedNameRelativePath, adjustedNameSegments, adjustedNameFolderName, rootContext, groupNameSanitizeReplacement)
.ToUpper(); .ReplaceTags(customTags).ReplaceTags(tags),
preserveAdGroupNameCase);
} }
@@ -429,7 +439,7 @@ namespace C4IT_IAM_Engine
DirectorySearcher search = new DirectorySearcher(entry) DirectorySearcher search = new DirectorySearcher(entry)
{ {
Filter = "(&(objectClass=group)(sAMAccountName=" + groupName.ToUpper() + "))" Filter = "(&(objectClass=group)(sAMAccountName=" + GetConfiguredGroupName(groupName) + "))"
}; };
search.PageSize = 100000; search.PageSize = 100000;
@@ -718,13 +728,17 @@ namespace C4IT_IAM_Engine
try try
{ {
secGroup.CreatedNewEntry = false; 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); 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()}"); DefaultLogger.LogEntry(LogLevels.Debug, $"Creating ad entry with CN / sAmAccountName: {groupName}");
DirectoryEntry group = entry.Children.Add("CN=" + secGroup.Name.ToUpper(), "group"); DirectoryEntry group = entry.Children.Add("CN=" + groupName, "group");
group.Properties["sAmAccountName"].Value = secGroup.Name.ToUpper(); group.Properties["sAmAccountName"].Value = groupName;
if (users != null && secGroup.Scope == GroupScope.Global) if (users != null && secGroup.Scope == GroupScope.Global)
{ {
foreach (var user in users) foreach (var user in users)
@@ -749,7 +763,7 @@ namespace C4IT_IAM_Engine
} }
group.CommitChanges(); 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); var objectid = SecurityGroups.getSID(ent);
DefaultLogger.LogEntry(LogLevels.Debug, $"Security group created in ad: {secGroup.technicalName}"); DefaultLogger.LogEntry(LogLevels.Debug, $"Security group created in ad: {secGroup.technicalName}");
@@ -778,6 +792,11 @@ namespace C4IT_IAM_Engine
LogMethodEnd(MethodBase.GetCurrentMethod()); LogMethodEnd(MethodBase.GetCurrentMethod());
} }
} }
private string GetConfiguredGroupName(string groupName)
{
return Helper.ApplyAdGroupNameCasing(groupName, PreserveAdGroupNameCase);
}
} }
public enum GroupScopeValues : int public enum GroupScopeValues : int
{ {

View File

@@ -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(0)}}`: erstes Root-Segment nach dem Server, z.B. `file_shares`
- `{{ROOT_SEGMENT(1)}}`: zweites Root-Segment nach dem Server, z.B. `share2` - `{{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: Beispiel:
@@ -231,6 +231,35 @@ ACL_FILE_SHARES_SHARE2.TEST33_O
Die bestehenden Platzhalter `{{NAME}}`, `{{RELATIVEPATH}}`, `{{TRAVERSE_NAME}}` und `{{TRAVERSE_VISIBLEPATH}}` bleiben unveraendert. 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>`, `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 ## Matching-Regeln
Empfohlene Semantik: Empfohlene Semantik:

View File

@@ -39,6 +39,7 @@ Das bedeutet:
- kein Caching ueber den gesamten Lauf - kein Caching ueber den gesamten Lauf
- Owner-/Write-/Read-Gruppen sowie Traverse-Gruppen werden analog zur Ordner-Neuanlage sichergestellt - Owner-/Write-/Read-Gruppen sowie Traverse-Gruppen werden analog zur Ordner-Neuanlage sichergestellt
- eine optionale Traverse-Grenze kann ueber `NtfsTraverseBoundaryPath` aus `AdditionalConfiguration` gesetzt werden - 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 ### 2. Create-/Ensure-Pfad