32 Commits

Author SHA1 Message Date
Meik
18a96d3e23 Add classified NTFS path template placeholders 2026-07-16 20:16:03 +02:00
Meik
c1e26ba615 Safeguard NTFS AD group reuse 2026-07-16 15:43:00 +02:00
Meik
c4efc41c4b Prevent NTFS traverse ensure loop 2026-07-16 13:34:53 +02:00
Meik
daec5adfb5 Use shared LIAM logger for NTFS engine fallback 2026-07-16 13:14:29 +02:00
Meik
dd0ebd3d6d Avoid repeated NTFS engine logger initialization 2026-07-16 13:11:40 +02:00
Meik
0b1dbd7925 Add NTFS automatic ensure timeout diagnostics 2026-07-16 12:42:37 +02:00
Meik
e7fb041ff5 Centralize NTFS ACL permission defaults 2026-07-16 12:37:56 +02:00
Meik
5dfccca237 Use configured NTFS ACL permissions 2026-07-10 11:23:52 +02:00
Meik
f9133b3549 Improve automatic NTFS ensure diagnostics 2026-07-10 10:42:37 +02:00
Meik
10c487572d Default NTFS traverse boundary to root path 2026-07-10 09:56:00 +02:00
Meik
e9bed32334 Bump LIAM version to 3.3.5 2026-07-10 09:18:49 +02:00
Meik
af5f0784b8 Create NTFS traverse groups lazily 2026-07-10 09:02:32 +02:00
Meik
077bac99ae Avoid own permission groups in traverse group 2026-07-10 08:46:08 +02:00
Meik
2f1ab27ce9 Allow child permission groups in parent traverse groups 2026-07-10 00:03:29 +02:00
Meik
b0fd1d0c75 Fix NTFS traverse group reuse 2026-07-09 23:52:19 +02:00
Meik
b9cd74c5a9 Log LIAM assembly identities and cap traverse depth 2026-07-09 23:30:07 +02:00
Meik
e8b138f9c3 Apply NTFS level filters to data area creation 2026-07-09 22:53:47 +02:00
Meik
3ca1a41816 Add NTFS ensure level filters 2026-07-09 22:43:41 +02:00
Meik
e23397c2ac Bump assembly version to 3.3.4 2026-07-09 22:36:05 +02:00
Meik
fb9494063b Update diagnostics NTFS ensure actions 2026-06-18 21:48:40 +02:00
Meik
fd818f9eb2 Document automatic NTFS traverse ensure 2026-06-18 21:34:32 +02:00
Meik
2217a393be Add automatic NTFS traverse ensure 2026-06-18 21:32:13 +02:00
Meik
6abe0a690b Continue automatic NTFS ensure after failures 2026-06-18 21:05:56 +02:00
Meik
fe059de50d Use LDAP lookup for fixture groups 2026-06-18 16:30:32 +02:00
Meik
60518a801f Recreate logged NTFS ACL fixture 2026-06-18 16:25:50 +02:00
Meik
b0cfed750c Add LIAM NTFS demo share setup script 2026-06-18 16:19:14 +02:00
Meik
275abe8ebd Add NTFS data area diagnostics logging 2026-06-16 14:47:21 +02:00
Meik
f1fb5dbc1b Limit NTFS group name sanitizing to invalid AD characters 2026-06-16 13:14:14 +02:00
Meik
15533ef005 Bump shared assembly version to 3.3.3 2026-06-16 13:09:51 +02:00
Meik
44954bdd39 Ensure traverse ACLs for NTFS shares 2026-06-16 12:30:45 +02:00
Meik
e0a1fc9595 Document NTFS naming convention placeholders 2026-06-16 11:38:07 +02:00
Meik
8d43d47b82 Bump shared assembly version to 3.3.2 2026-06-16 11:34:29 +02:00
15 changed files with 2832 additions and 393 deletions

View File

@@ -48,6 +48,13 @@ namespace C4IT.LIAM
{
}
public static class cLiamAclPermissionDefaults
{
public const int Read = 0x200A9;
public const int Write = 0x301BF;
public const int Owner = 0x301BF;
}
public class cLiamProviderData : ICloneable
{
@@ -80,6 +87,9 @@ namespace C4IT.LIAM
public string ReadGroupGlobal { get; set; } = "";
public string ReadGroupLocal { get; set; } = "";
public string TraverseGroup { get; set; } = "";
public int ReadACLPermission { get; set; } = cLiamAclPermissionDefaults.Read;
public int WriteACLPermission { get; set; } = cLiamAclPermissionDefaults.Write;
public int OwnerACLPermission { get; set; } = cLiamAclPermissionDefaults.Owner;
public List<cLiamNamingConvention> NamingConventions { get; set; } = new List<cLiamNamingConvention>();
public Dictionary<string, string> CustomTags { get; set; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
@@ -98,6 +108,9 @@ namespace C4IT.LIAM
To.GroupFilter = From.GroupFilter;
To.GroupRegEx = From.GroupRegEx;
To.GroupPath = From.GroupPath;
To.ReadACLPermission = From.ReadACLPermission;
To.WriteACLPermission = From.WriteACLPermission;
To.OwnerACLPermission = From.OwnerACLPermission;
To.CustomTags = From.CustomTags;
To.AdditionalConfiguration = From.AdditionalConfiguration;
To.CustomTags = From.CustomTags;

File diff suppressed because it is too large Load Diff

View File

@@ -17,15 +17,15 @@ using C4IT.Logging;
using static C4IT.Logging.cLogManager;
using System.Reflection;
using System.Threading;
using C4IT_IAM_GET;
using C4IT.LIAM;
namespace C4IT_IAM_SET
{
public class DataArea_FileSystem
{
public const string constApplicationDataPath = "%ProgramData%\\Consulting4IT GmbH\\LIAM";
public string domainName;
public string effectiveDomainController;
public string username;
@@ -57,13 +57,16 @@ namespace C4IT_IAM_SET
public Func<string, bool> CanManageTraversePermissionsForPath;
public string traverseBoundaryPath;
public bool forceStrictAdGroupNames;
public SecurityGroupReuseMode adGroupReuseMode = SecurityGroupReuseMode.Safe;
public bool adGroupMarkerBackfill;
public string groupNameSanitizeReplacement = Helper.DefaultGroupNameSanitizeReplacement;
public bool preserveAdGroupNameCase;
public bool WhatIf;
public Func<string, Helper.RootPathTemplateContext> GetTemplateContextForPath;
public int ReadACLPermission = 0x200A9;
public int WriteACLPermission = 0x301BF;
public int OwnerACLPermission = 0x1F01FF;
public int ReadACLPermission = cLiamAclPermissionDefaults.Read;
public int WriteACLPermission = cLiamAclPermissionDefaults.Write;
public int OwnerACLPermission = cLiamAclPermissionDefaults.Owner;
public string ConfigID;
@@ -74,20 +77,32 @@ namespace C4IT_IAM_SET
public List<IAM_SecurityGroupTemplate> templates;
public int createTraverseGroupLvl = 0;
private static int engineFileLoggerInitialized;
public DataArea_FileSystem()
{
var logDirectory = Environment.ExpandEnvironmentVariables(constApplicationDataPath);
Helper.CreatePathWithWriteAccess(logDirectory);
var LogPath = Path.Combine(logDirectory, "Logs");
cLogManagerFile.CreateInstance(Path.Combine(LogPath, "LIAM.log"));
EnsureEngineFileLoggerInitialized();
DefaultLogger.LogEntry(LogLevels.Info, "=================================================");
DefaultLogger.LogEntry(LogLevels.Info, $"LIAM engine v{Assembly.GetExecutingAssembly().GetName().Version} started");
if (DefaultLogger != null)
{
DefaultLogger.LogEntry(LogLevels.Info, "=================================================");
DefaultLogger.LogEntry(LogLevels.Info, $"LIAM engine v{Assembly.GetExecutingAssembly().GetName().Version} started");
}
templates = new List<IAM_SecurityGroupTemplate>();
}
private static void EnsureEngineFileLoggerInitialized()
{
if (DefaultLogger != null)
return;
if (Interlocked.Exchange(ref engineFileLoggerInitialized, 1) != 0)
return;
cLogManagerFile.CreateInstance(LocalMachine: true, A: Assembly.GetExecutingAssembly());
}
private string GetAdServer()
{
return string.IsNullOrWhiteSpace(effectiveDomainController) ? domainName : effectiveDomainController;
@@ -311,10 +326,18 @@ namespace C4IT_IAM_SET
effectiveDomainController = effectiveDomainController,
password = password,
ForceStrictAdGroupNames = forceStrictAdGroupNames,
PreserveAdGroupNameCase = preserveAdGroupNameCase
PreserveAdGroupNameCase = preserveAdGroupNameCase,
ReuseMode = adGroupReuseMode,
MarkerBackfill = adGroupMarkerBackfill
};
}
private Helper.RootPathTemplateContext GetEffectiveTemplateContext(string path)
{
return GetTemplateContextForPath?.Invoke(path)
?? Helper.GetRootPathTemplateContext(baseFolder, groupNameSanitizeReplacement);
}
private ResultToken ValidateTraverseBoundaryForCurrentFolder()
{
var resultToken = new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString());
@@ -324,11 +347,11 @@ namespace C4IT_IAM_SET
if (string.IsNullOrWhiteSpace(boundaryPath))
return resultToken;
var targetParent = new DirectoryInfo(newFolderPath).Parent;
if (targetParent == null)
var targetPath = NormalizeDirectoryPath(newFolderPath);
if (string.IsNullOrWhiteSpace(targetPath))
{
resultToken.resultErrorId = 30009;
resultToken.resultMessage = $"Traverse boundary '{traverseBoundaryPath}' cannot be validated because '{newFolderPath}' has no parent directory.";
resultToken.resultMessage = $"Traverse boundary '{traverseBoundaryPath}' cannot be validated because the target path is empty.";
return resultToken;
}
@@ -339,16 +362,144 @@ namespace C4IT_IAM_SET
return resultToken;
}
if (!IsSameOrAncestorPath(boundaryPath, targetParent.FullName))
if (!IsSameOrAncestorPath(boundaryPath, targetPath))
{
resultToken.resultErrorId = 30009;
resultToken.resultMessage = $"Traverse boundary '{traverseBoundaryPath}' is not a parent path of '{newFolderPath}'.";
resultToken.resultMessage = $"Traverse boundary '{traverseBoundaryPath}' is not the target path or a parent path of '{newFolderPath}'.";
}
return resultToken;
}
public ResultToken ensureDataAreaPermissions(bool ensureTraverseGroups = false)
{
LogMethodBegin(MethodBase.GetCurrentMethod());
var stopwatch = Stopwatch.StartNew();
try
{
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions start. Stage=Start Path='{newFolderPath}', BaseFolder='{baseFolder}', EnsureTraverseGroups={ensureTraverseGroups}, WhatIf={WhatIf}");
var resultToken = checkRequiredVariablesForEnsure();
if (resultToken.resultErrorId != 0)
return resultToken;
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage. Stage=NetworkConnection Path='{newFolderPath}', BaseFolder='{baseFolder}'");
if (Connection != null)
Connection.Dispose();
using (Connection = new cNetworkConnection(baseFolder, username, new NetworkCredential("", password).Password))
{
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage finished. Stage=NetworkConnection Path='{newFolderPath}', Elapsed='{stopwatch.Elapsed}'");
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage. Stage=DirectoryExists Path='{newFolderPath}'");
if (!Directory.Exists(newFolderPath))
{
resultToken.resultErrorId = 30203;
resultToken.resultMessage = "Verzeichnis existiert nicht";
return resultToken;
}
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage finished. Stage=DirectoryExists Path='{newFolderPath}', Elapsed='{stopwatch.Elapsed}'");
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage. Stage=InitializeFolderContext Path='{newFolderPath}'");
var parentDirectory = Directory.GetParent(newFolderPath);
if (string.IsNullOrWhiteSpace(newFolderParent))
newFolderParent = parentDirectory?.FullName;
InitializeFolderContext();
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage finished. Stage=InitializeFolderContext Path='{newFolderPath}', ParentPath='{newFolderParent}', Elapsed='{stopwatch.Elapsed}'");
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage. Stage=ValidateTraverseBoundary Path='{newFolderPath}'");
var traverseBoundaryResult = ValidateTraverseBoundaryForCurrentFolder();
if (traverseBoundaryResult.resultErrorId != 0)
return traverseBoundaryResult;
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage finished. Stage=ValidateTraverseBoundary Path='{newFolderPath}', Elapsed='{stopwatch.Elapsed}'");
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage. Stage=EnsureADGroups Path='{newFolderPath}'");
ensureADGroups(resultToken);
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage finished. Stage=EnsureADGroups Path='{newFolderPath}', CreatedGroups={resultToken.createdGroups.Count}, ReusedGroups={resultToken.reusedGroups.Count}, Warnings={resultToken.warnings.Count}, Elapsed='{stopwatch.Elapsed}'");
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage. Stage=EnsureFolderPermissions Path='{newFolderPath}'");
resultToken = ensureFolderPermissions(resultToken);
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage finished. Stage=EnsureFolderPermissions Path='{newFolderPath}', ResultErrorId={resultToken.resultErrorId}, AddedAcls={resultToken.addedAclEntries.Count}, SkippedAcls={resultToken.skippedAclEntries.Count}, Elapsed='{stopwatch.Elapsed}'");
if (resultToken.resultErrorId != 0)
return resultToken;
if (ensureTraverseGroups)
{
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage. Stage=SetTraversePermissions Path='{newFolderPath}'");
var traverseResult = SetTraversePermissions();
if (traverseResult != null)
{
resultToken.createdGroups.AddRange(traverseResult.createdGroups);
resultToken.reusedGroups.AddRange(traverseResult.reusedGroups);
resultToken.addedAclEntries.AddRange(traverseResult.addedAclEntries);
resultToken.skippedAclEntries.AddRange(traverseResult.skippedAclEntries);
resultToken.ensuredTraverseGroups.AddRange(traverseResult.ensuredTraverseGroups);
resultToken.warnings.AddRange(traverseResult.warnings);
if (traverseResult.resultErrorId != 0)
{
resultToken.resultErrorId = traverseResult.resultErrorId;
resultToken.resultMessage = traverseResult.resultMessage;
return resultToken;
}
}
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage finished. Stage=SetTraversePermissions Path='{newFolderPath}', TraverseGroups={resultToken.ensuredTraverseGroups.Count}, AddedAcls={resultToken.addedAclEntries.Count}, Elapsed='{stopwatch.Elapsed}'");
}
resultToken.resultMessage = WhatIf
? "Gruppen- und ACL-Vorschau erfolgreich erstellt"
: "Gruppen und ACLs erfolgreich sichergestellt";
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions finished. Stage=Finished Path='{newFolderPath}', ResultErrorId={resultToken.resultErrorId}, ResultMessage='{resultToken.resultMessage}', Elapsed='{stopwatch.Elapsed}'");
return resultToken;
}
}
catch (Exception E)
{
cLogManager.DefaultLogger.LogException(E);
throw;
}
finally
{
LogMethodEnd(MethodBase.GetCurrentMethod());
}
}
public ResultToken ensureTraversePermissionsOnly()
{
LogMethodBegin(MethodBase.GetCurrentMethod());
@@ -380,42 +531,15 @@ namespace C4IT_IAM_SET
if (traverseBoundaryResult.resultErrorId != 0)
return traverseBoundaryResult;
ensureADGroups(resultToken);
resultToken = ensureFolderPermissions(resultToken);
if (resultToken.resultErrorId != 0)
return resultToken;
if (ensureTraverseGroups)
EnsureExistingPermissionGroupsForTraverse(resultToken);
resultToken = MergeResultTokens(resultToken, SetTraversePermissions(true));
if (resultToken.resultErrorId == 0)
{
if (WhatIf)
{
resultToken.warnings.Add("Traverse group preview is not supported in WhatIf mode for automatic DataArea ensure.");
resultToken.resultMessage = "Gruppen- und ACL-Vorschau erfolgreich erstellt";
return resultToken;
}
var traverseResult = SetTraversePermissions();
if (traverseResult != null)
{
resultToken.createdGroups.AddRange(traverseResult.createdGroups);
resultToken.reusedGroups.AddRange(traverseResult.reusedGroups);
resultToken.addedAclEntries.AddRange(traverseResult.addedAclEntries);
resultToken.skippedAclEntries.AddRange(traverseResult.skippedAclEntries);
resultToken.ensuredTraverseGroups.AddRange(traverseResult.ensuredTraverseGroups);
resultToken.warnings.AddRange(traverseResult.warnings);
if (traverseResult.resultErrorId != 0)
{
resultToken.resultErrorId = traverseResult.resultErrorId;
resultToken.resultMessage = traverseResult.resultMessage;
return resultToken;
}
}
resultToken.resultMessage = WhatIf
? "Traverse-Gruppen- und ACL-Vorschau erfolgreich erstellt"
: "Traverse-Gruppen und ACLs erfolgreich sichergestellt";
}
resultToken.resultMessage = WhatIf
? "Gruppen- und ACL-Vorschau erfolgreich erstellt"
: "Gruppen und ACLs erfolgreich sichergestellt";
return resultToken;
}
}
@@ -430,7 +554,51 @@ namespace C4IT_IAM_SET
}
}
private ResultToken SetTraversePermissions()
private void EnsureExistingPermissionGroupsForTraverse(ResultToken resultToken)
{
newSecurityGroups.IAM_SecurityGroups.Clear();
newSecurityGroups.GenerateNewSecurityGroups(baseFolder,
newDataArea.IAM_Folders[0].technicalName,
groupPrefix,
groupOUPath,
groupPermissionStrategy,
groupTraverseTag,
groupReadTag,
groupWriteTag,
groupOwnerTag,
groupDLTag,
groupGTag,
groupCustomTags,
templates,
ReadACLPermission,
WriteACLPermission,
OwnerACLPermission,
0,
0,
groupNameSanitizeReplacement,
preserveAdGroupNameCase,
GetEffectiveTemplateContext(newDataArea.IAM_Folders[0].technicalName));
var existingGroups = new List<IAM_SecurityGroup>();
foreach (var securityGroup in newSecurityGroups.IAM_SecurityGroups)
{
var existingGroup = newSecurityGroups.PreviewADGroup(groupOUPath, securityGroup, newDataArea.IAM_Folders[0].technicalName);
if (existingGroup == null || string.IsNullOrWhiteSpace(securityGroup.UID))
{
if (securityGroup.Scope == GroupScope.Global)
resultToken.warnings.Add($"Traverse-only: LIAM-Berechtigungsgruppe '{securityGroup.Name}' fehlt und wird nicht angelegt.");
continue;
}
resultToken.reusedGroups.Add(securityGroup.Name);
existingGroups.Add(securityGroup);
}
newSecurityGroups.IAM_SecurityGroups.Clear();
newSecurityGroups.IAM_SecurityGroups.AddRange(existingGroups);
}
private ResultToken SetTraversePermissions(bool includeCurrentFolder = false)
{
LogMethodBegin(MethodBase.GetCurrentMethod());
@@ -467,20 +635,21 @@ namespace C4IT_IAM_SET
DirectoryInfo newDir = new DirectoryInfo(newDataArea.IAM_Folders[0].technicalName);
DefaultLogger.LogEntry(LogLevels.Debug, $"Neues Verzeichnis: {newDir.FullName}");
DirectoryInfo parent = newDir.Parent;
DirectoryInfo parent = includeCurrentFolder ? newDir : newDir.Parent;
if (parent == null)
{
DefaultLogger.LogEntry(LogLevels.Error, "Parent-Verzeichnis ist null.");
DefaultLogger.LogEntry(LogLevels.Error, "Traverse-Startverzeichnis ist null.");
return resultToken;
}
DefaultLogger.LogEntry(LogLevels.Debug, $"Parent-Verzeichnis: {parent.FullName}");
DefaultLogger.LogEntry(LogLevels.Debug, $"Traverse-Startverzeichnis: {parent.FullName}");
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;
var sourcePath = NormalizeDirectoryPath(newDataArea.IAM_Folders[0].technicalName);
var sourcePermissionGroupsAdded = false;
// Überprüfen der Templates
if (templates == null)
@@ -519,6 +688,10 @@ namespace C4IT_IAM_SET
}
GroupPrincipal traverseGroup = null;
string traverseGroupName = null;
var visitedTraverseStates = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var traverseIterationCount = 0;
const int maxTraverseIterations = 256;
// Überprüfen, ob createTraverseGroupLvl initialisiert ist
DefaultLogger.LogEntry(LogLevels.Debug, $"createTraverseGroupLvl: {createTraverseGroupLvl}");
@@ -539,6 +712,24 @@ namespace C4IT_IAM_SET
break;
}
traverseIterationCount++;
if (traverseIterationCount > maxTraverseIterations)
{
resultToken.resultErrorId = 30200;
resultToken.resultMessage = $"Traverse-Verarbeitung fuer '{newDir.FullName}' wurde nach {maxTraverseIterations} Iterationen abgebrochen.";
DefaultLogger.LogEntry(LogLevels.Error, resultToken.resultMessage);
return resultToken;
}
var traverseState = $"{NormalizeDirectoryPath(parent.FullName)}|{currentTraverseLevel}|{defaultTraverseLoopIndex}";
if (!visitedTraverseStates.Add(traverseState))
{
resultToken.resultErrorId = 30200;
resultToken.resultMessage = $"Traverse-Verarbeitung fuer '{newDir.FullName}' wurde abgebrochen, weil der Pfad '{parent.FullName}' auf Ebene {currentTraverseLevel} erneut verarbeitet werden sollte.";
DefaultLogger.LogEntry(LogLevels.Error, resultToken.resultMessage);
return resultToken;
}
var canManageTraversePath = CanManageTraversePermissionsForPath ?? CanManagePermissionsForPath;
if (canManageTraversePath != null && !canManageTraversePath(parent.FullName))
{
@@ -563,6 +754,29 @@ namespace C4IT_IAM_SET
continue;
}
if (PathsEqual(parent.FullName, sourcePath) && string.IsNullOrWhiteSpace(traverseGroupName))
{
DefaultLogger.LogEntry(LogLevels.Debug, $"Skip own Traverse-Gruppe fuer '{parent.FullName}' because no child permission or traverse group needs it.");
if (IsTraverseBoundaryPath(parent.FullName))
break;
parent = parent.Parent;
if (parent != null)
{
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 Lazy-Skip: {currentTraverseLevel}");
}
else
{
DefaultLogger.LogEntry(LogLevels.Debug, "Parent nach Lazy-Skip ist null.");
}
continue;
}
DefaultLogger.LogEntry(LogLevels.Debug, $"Hole ACL für Ordner: {parent.FullName}");
AuthorizationRuleCollection ACLs = null;
try
@@ -583,6 +797,7 @@ namespace C4IT_IAM_SET
}
GroupPrincipal parentTraverseGroup = null;
string parentTraverseGroupName = null;
var parentTraverseAclExists = false;
string relativePathRaw = DataArea.GetRelativePath(parent.FullName, baseFolder).Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
relativePathRaw = relativePathRaw.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
@@ -595,8 +810,8 @@ namespace C4IT_IAM_SET
var folderName = sanitizedSegments.Length > 0
? sanitizedSegments[sanitizedSegments.Length - 1]
: Helper.SanitizePathSegment(Path.GetFileName(parent.FullName.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)), groupNameSanitizeReplacement);
var traverseTags = GetTraverseReplacementTags(parent.FullName);
var rootContext = Helper.GetRootPathTemplateContext(baseFolder, groupNameSanitizeReplacement);
var traverseTags = GetTraverseReplacementTags(parent.FullName, traverseGroupTemplate.Scope);
var rootContext = GetEffectiveTemplateContext(parent.FullName);
var boundedTraverseContext = Helper.GetBoundedAdGroupTemplateContext(
traverseGroupTemplate.NamingTemplate,
true,
@@ -684,16 +899,8 @@ namespace C4IT_IAM_SET
break;
}
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)
parentTraverseGroupName = parentTraverseGroup.Name;
if (parentTraverseGroup == null && !string.IsNullOrWhiteSpace(traverseNameTemplate))
{
@@ -704,6 +911,7 @@ namespace C4IT_IAM_SET
if (parentTraverseGroup == null)
continue;
parentTraverseGroupName = parentTraverseGroup.Name;
resultToken.reusedGroups.Add(candidateName);
resultToken.ensuredTraverseGroups.Add(candidateName);
DefaultLogger.LogEntry(LogLevels.Debug, $"Vorhandene Traverse-Gruppe wiederverwendet: {candidateName}");
@@ -711,6 +919,13 @@ namespace C4IT_IAM_SET
}
}
if (parentTraverseGroup == null && hasTraverseWildcard && !forceStrictAdGroupNames)
{
DefaultLogger.LogEntry(
LogLevels.Debug,
$"Traverse wildcard '{traverseRegex}' is only used for ACL-linked groups on '{parent.FullName}'. Global wildcard reuse is skipped to avoid reusing a traverse group from another path.");
}
if (parentTraverseGroup == null && !string.IsNullOrWhiteSpace(traverseNameTemplate))
{
DefaultLogger.LogEntry(LogLevels.Debug, "Erstelle neue TraverseGroup.");
@@ -731,7 +946,9 @@ namespace C4IT_IAM_SET
Name = traverseNameTemplate.ReplaceLoopTag(loop),
description = traverseDescriptionTemplate.ReplaceLoopTag(loop),
technicalName = "CN=" + traverseNameTemplate.ReplaceLoopTag(loop) + "," + groupOUPath,
Scope = traverseGroupTemplate.Scope
securityGroupType = SecurityGroupType.Traverse,
Scope = traverseGroupTemplate.Scope,
MarkerPath = parent.FullName
};
DefaultLogger.LogEntry(LogLevels.Debug, $"Erstellte TraverseGroup: {newTraverseGroup.Name} (Loop: {loop})");
loop++;
@@ -751,60 +968,58 @@ namespace C4IT_IAM_SET
continue;
}
if (parent.Parent != null)
DefaultLogger.LogEntry(LogLevels.Debug, "Erstelle AD-Gruppe.");
if (WhatIf)
{
DefaultLogger.LogEntry(LogLevels.Debug, "Parent.Parent ist nicht null. Erstelle AD-Gruppe.");
if (WhatIf)
{
resultToken.createdGroups.Add(newTraverseGroup.Name);
resultToken.ensuredTraverseGroups.Add(newTraverseGroup.Name);
resultToken.warnings.Add($"Traverse-Gruppe würde angelegt werden: {newTraverseGroup.Name}");
resultToken.addedAclEntries.Add(newTraverseGroup.Name);
parentTraverseAclExists = true;
}
else
{
try
{
newSecurityGroups.CreateADGroup(groupOUPath, newTraverseGroup, null);
DefaultLogger.LogEntry(LogLevels.Debug, $"AD-Gruppe erstellt: {newTraverseGroup.Name}");
resultToken.createdGroups.Add(newTraverseGroup.Name);
resultToken.ensuredTraverseGroups.Add(newTraverseGroup.Name);
}
catch (Exception ex)
{
DefaultLogger.LogEntry(LogLevels.Error, $"Fehler beim Erstellen der AD-Gruppe: {ex.Message}");
continue;
}
parentTraverseGroup = GroupPrincipal.FindByIdentity(domainContext, newTraverseGroup.Name);
if (parentTraverseGroup == null)
{
DefaultLogger.LogEntry(LogLevels.Error, $"parentTraverseGroup konnte nach Erstellung der Gruppe nicht gefunden werden: {newTraverseGroup.Name}");
continue;
}
try
{
var accesscontrol = parent.GetAccessControl();
accesscontrol.AddAccessRule(new FileSystemAccessRule(parentTraverseGroup.Sid,
FileSystemRights.Read, InheritanceFlags.None, PropagationFlags.None,
AccessControlType.Allow));
DefaultLogger.LogEntry(LogLevels.Debug, $"Setze Traverse-ACL auf: {parent.FullName} für {parentTraverseGroup.DistinguishedName}");
parent.SetAccessControl(accesscontrol);
resultToken.addedAclEntries.Add(parentTraverseGroup.Name);
parentTraverseAclExists = true;
}
catch (Exception ex)
{
DefaultLogger.LogEntry(LogLevels.Error, $"Fehler beim Setzen der ACL: {ex.Message}");
continue;
}
}
parentTraverseGroupName = newTraverseGroup.Name;
resultToken.createdGroups.Add(newTraverseGroup.Name);
resultToken.ensuredTraverseGroups.Add(newTraverseGroup.Name);
resultToken.warnings.Add($"Traverse-Gruppe würde angelegt werden: {newTraverseGroup.Name}");
resultToken.addedAclEntries.Add(newTraverseGroup.Name);
parentTraverseAclExists = true;
}
else
{
DefaultLogger.LogEntry(LogLevels.Debug, "Parent.Parent ist null. Traverse-ACL kann nicht gesetzt werden.");
try
{
newSecurityGroups.CreateADGroup(groupOUPath, newTraverseGroup, null);
DefaultLogger.LogEntry(LogLevels.Debug, $"AD-Gruppe erstellt: {newTraverseGroup.Name}");
resultToken.createdGroups.Add(newTraverseGroup.Name);
resultToken.ensuredTraverseGroups.Add(newTraverseGroup.Name);
}
catch (Exception ex)
{
DefaultLogger.LogEntry(LogLevels.Error, $"Fehler beim Erstellen der AD-Gruppe: {ex.Message}");
continue;
}
parentTraverseGroup = GroupPrincipal.FindByIdentity(domainContext, newTraverseGroup.Name);
if (parentTraverseGroup == null)
{
DefaultLogger.LogEntry(LogLevels.Error, $"parentTraverseGroup konnte nach Erstellung der Gruppe nicht gefunden werden: {newTraverseGroup.Name}");
continue;
}
parentTraverseGroupName = parentTraverseGroup.Name;
try
{
var accesscontrol = parent.GetAccessControl();
accesscontrol.AddAccessRule(new FileSystemAccessRule(parentTraverseGroup.Sid,
FileSystemRights.Read, InheritanceFlags.None, PropagationFlags.None,
AccessControlType.Allow));
DefaultLogger.LogEntry(LogLevels.Debug, $"Setze Traverse-ACL auf: {parent.FullName} für {parentTraverseGroup.DistinguishedName}");
parent.SetAccessControl(accesscontrol);
resultToken.addedAclEntries.Add(parentTraverseGroup.Name);
parentTraverseAclExists = true;
}
catch (Exception ex)
{
DefaultLogger.LogEntry(LogLevels.Error, $"Fehler beim Setzen der ACL: {ex.Message}");
resultToken.resultErrorId = 30200;
resultToken.resultMessage = $"Fehler beim Setzen der Traverse-ACL auf '{parent.FullName}' fuer '{parentTraverseGroupName ?? newTraverseGroup.Name}': {ex.Message}";
return resultToken;
}
}
}
}
@@ -839,66 +1054,84 @@ namespace C4IT_IAM_SET
catch (Exception ex)
{
DefaultLogger.LogEntry(LogLevels.Error, $"Fehler beim Sicherstellen der Traverse-ACL: {ex.Message}");
continue;
resultToken.resultErrorId = 30200;
resultToken.resultMessage = $"Fehler beim Sicherstellen der Traverse-ACL auf '{parent.FullName}' fuer '{parentTraverseGroup.Name}': {ex.Message}";
return resultToken;
}
}
if (parentTraverseGroup != null)
if (parentTraverseGroup != null || !string.IsNullOrWhiteSpace(parentTraverseGroupName))
{
if (!processedNearestTraverseParent)
var displayTraverseGroupName = parentTraverseGroup?.Name ?? parentTraverseGroupName;
var isSourceTraversePath = PathsEqual(parent.FullName, sourcePath);
if (!sourcePermissionGroupsAdded)
{
DefaultLogger.LogEntry(LogLevels.Debug, "Verarbeite SecurityGroups bei oberster Ebene.");
foreach (var currentSecGroup in newSecurityGroups.IAM_SecurityGroups)
if (isSourceTraversePath)
{
if (currentSecGroup == null)
{
DefaultLogger.LogEntry(LogLevels.Error, "currentSecGroup ist null.");
continue;
}
if (currentSecGroup.Scope != GroupScope.Global)
continue;
if (WhatIf)
{
resultToken.warnings.Add($"Traverse-Gruppe '{parentTraverseGroup.Name}' würde Mitglied '{currentSecGroup.Name}' erhalten.");
continue;
}
if (!TryEnsureGlobalGroupMembershipWithRetry(domainContext, parentTraverseGroup, currentSecGroup))
continue;
DefaultLogger.LogEntry(LogLevels.Debug, $"Skip direct permission group membership for own Traverse-Gruppe '{displayTraverseGroupName}' on '{parent.FullName}'.");
}
traverseGroup = parentTraverseGroup;
processedNearestTraverseParent = true;
}
else
{
if (traverseGroup != null && parentTraverseGroup != null)
else
{
try
DefaultLogger.LogEntry(LogLevels.Debug, "Verarbeite SecurityGroups in erster uebergeordneter Traverse-Gruppe.");
foreach (var currentSecGroup in newSecurityGroups.IAM_SecurityGroups)
{
if (!parentTraverseGroup.Members.Contains(traverseGroup))
if (currentSecGroup == null)
{
if (WhatIf)
{
resultToken.warnings.Add($"Traverse-Gruppe '{parentTraverseGroup.Name}' würde verschachtelte Gruppe '{traverseGroup.Name}' erhalten.");
}
else
{
if (!TryEnsureNestedTraverseGroupMembershipWithRetry(parentTraverseGroup, traverseGroup))
continue;
}
DefaultLogger.LogEntry(LogLevels.Error, "currentSecGroup ist null.");
continue;
}
if (currentSecGroup.Scope != GroupScope.Global)
continue;
if (IsSameGroupName(currentSecGroup.Name, displayTraverseGroupName))
{
DefaultLogger.LogEntry(LogLevels.Debug, $"Skip self membership for Traverse-Gruppe '{displayTraverseGroupName}'.");
continue;
}
if (WhatIf)
{
resultToken.warnings.Add($"Traverse-Gruppe '{displayTraverseGroupName}' würde Mitglied '{currentSecGroup.Name}' erhalten.");
continue;
}
if (!TryEnsureGlobalGroupMembershipWithRetry(domainContext, parentTraverseGroup, currentSecGroup))
continue;
}
catch (Exception ex)
{
DefaultLogger.LogEntry(LogLevels.Error, $"Fehler beim Hinzufügen der Traverse-Gruppe: {ex.Message}");
continue;
}
sourcePermissionGroupsAdded = true;
}
}
if (!string.IsNullOrWhiteSpace(traverseGroupName))
{
try
{
if (IsSameGroupName(displayTraverseGroupName, traverseGroupName))
{
DefaultLogger.LogEntry(LogLevels.Debug, $"Skip nested self membership for Traverse-Gruppe '{displayTraverseGroupName}'.");
}
else if (WhatIf)
{
resultToken.warnings.Add($"Traverse-Gruppe '{displayTraverseGroupName}' würde verschachtelte Gruppe '{traverseGroupName}' erhalten.");
}
else if (traverseGroup != null && parentTraverseGroup != null && !parentTraverseGroup.Members.Contains(traverseGroup))
{
if (!TryEnsureNestedTraverseGroupMembershipWithRetry(parentTraverseGroup, traverseGroup))
continue;
}
}
catch (Exception ex)
{
DefaultLogger.LogEntry(LogLevels.Error, $"Fehler beim Hinzufügen der Traverse-Gruppe: {ex.Message}");
continue;
}
}
traverseGroup = parentTraverseGroup;
traverseGroupName = displayTraverseGroupName;
try
{
if (!WhatIf)
if (!WhatIf && parentTraverseGroup != null)
{
parentTraverseGroup.Save();
DefaultLogger.LogEntry(LogLevels.Debug, $"parentTraverseGroup gespeichert: {parentTraverseGroup.Name}");
@@ -914,8 +1147,8 @@ namespace C4IT_IAM_SET
DefaultLogger.LogEntry(LogLevels.Debug, "parentTraverseGroup ist null.");
}
if (parentTraverseGroup != null && !resultToken.ensuredTraverseGroups.Contains(parentTraverseGroup.Name))
resultToken.ensuredTraverseGroups.Add(parentTraverseGroup.Name);
if (!string.IsNullOrWhiteSpace(parentTraverseGroupName) && !resultToken.ensuredTraverseGroups.Contains(parentTraverseGroupName))
resultToken.ensuredTraverseGroups.Add(parentTraverseGroupName);
if (IsTraverseBoundaryPath(parent.FullName))
break;
@@ -962,14 +1195,45 @@ namespace C4IT_IAM_SET
&& PathsEqual(boundaryPath, path);
}
private Dictionary<string, string> GetTraverseReplacementTags(string currentPath)
private Dictionary<string, string> GetTraverseReplacementTags(string currentPath, GroupScope traverseScope)
{
var visibleSegments = GetVisibleTraversePathSegments(currentPath);
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
var tags = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (groupCustomTags != null)
{
{ "TRAVERSE_NAME", Helper.SanitizePathSegment(GetLastPathSegment(currentPath), groupNameSanitizeReplacement) },
{ "TRAVERSE_VISIBLEPATH", Helper.JoinSanitizedPathSegments(visibleSegments.Select(i => Helper.SanitizePathSegment(i, groupNameSanitizeReplacement)), groupNameSanitizeReplacement) }
};
foreach (var customTag in groupCustomTags)
tags[customTag.Key] = customTag.Value;
}
tags["PREFIX"] = groupPrefix;
tags["GROUPTYPEPOSTFIX"] = groupTraverseTag;
tags["SCOPETAG"] = traverseScope == GroupScope.Local ? groupDLTag : groupGTag;
tags["TRAVERSE_NAME"] = Helper.SanitizePathSegment(GetLastPathSegment(currentPath), groupNameSanitizeReplacement);
tags["TRAVERSE_VISIBLEPATH"] = Helper.JoinSanitizedPathSegments(visibleSegments.Select(i => Helper.SanitizePathSegment(i, groupNameSanitizeReplacement)), groupNameSanitizeReplacement);
return tags;
}
private static bool IsSameGroupName(string left, string right)
{
return !string.IsNullOrWhiteSpace(left)
&& !string.IsNullOrWhiteSpace(right)
&& string.Equals(left, right, StringComparison.OrdinalIgnoreCase);
}
private static bool IsSamePrincipal(GroupPrincipal left, GroupPrincipal right)
{
if (left == null || right == null)
return false;
var leftSid = left.Sid?.Value;
var rightSid = right.Sid?.Value;
if (!string.IsNullOrWhiteSpace(leftSid) && !string.IsNullOrWhiteSpace(rightSid))
return string.Equals(leftSid, rightSid, StringComparison.OrdinalIgnoreCase);
if (!string.IsNullOrWhiteSpace(left.DistinguishedName) && !string.IsNullOrWhiteSpace(right.DistinguishedName))
return string.Equals(left.DistinguishedName, right.DistinguishedName, StringComparison.OrdinalIgnoreCase);
return IsSameGroupName(left.SamAccountName ?? left.Name, right.SamAccountName ?? right.Name);
}
private IEnumerable<string> GetVisibleTraversePathSegments(string currentPath)
@@ -1152,6 +1416,12 @@ namespace C4IT_IAM_SET
if (parentTraverseGroup == null || traverseGroup == null)
return false;
if (IsSamePrincipal(parentTraverseGroup, traverseGroup))
{
DefaultLogger.LogEntry(LogLevels.Debug, $"Skip nested self membership for Traverse-Gruppe '{parentTraverseGroup.Name}'.");
return true;
}
return RetryTraverseMembershipAction(
traverseGroup.Name,
true,
@@ -1343,7 +1613,8 @@ namespace C4IT_IAM_SET
0,
0,
groupNameSanitizeReplacement,
preserveAdGroupNameCase);
preserveAdGroupNameCase,
GetEffectiveTemplateContext(newDataArea.IAM_Folders[0].technicalName));
List<UserPrincipal> owners = getUserPrincipalBySid(ownerUserSids);
List<UserPrincipal> writers = getUserPrincipalBySid(writerUserSids);
@@ -1461,11 +1732,11 @@ namespace C4IT_IAM_SET
DirectoryInfo dInfo = new DirectoryInfo(technicalName);
//DirectoryInfo dInfoBaseFolder = new DirectoryInfo(baseFolderTechnicalName);
// Get a DirectorySecurity object that represents the
// Get a DirectorySecurity object that represents the
// current security settings.
DirectorySecurity dSecurity = dInfo.GetAccessControl();
// Add the FileSystemAccessRule to the security settings.
// Add the FileSystemAccessRule to the security settings.
var everyone = new SecurityIdentifier(WellKnownSidType.WorldSid, null);
dSecurity.AddAccessRule(new FileSystemAccessRule(everyone,
@@ -1509,7 +1780,8 @@ namespace C4IT_IAM_SET
existingADGroupCount,
0,
groupNameSanitizeReplacement,
preserveAdGroupNameCase);
preserveAdGroupNameCase,
GetEffectiveTemplateContext(newDataArea.IAM_Folders[0].technicalName));
/*
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 @@ using System.Security.AccessControl;
using System.Security.Principal;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Serialization;
@@ -32,8 +33,6 @@ namespace C4IT_IAM_GET
}
public class DataArea_FileSystem
{
public const string constApplicationDataPath = "%ProgramData%\\Consulting4IT GmbH\\LIAM";
public string domainName;
public string DomainIPAddress;
public string username;
@@ -74,6 +73,7 @@ namespace C4IT_IAM_GET
public string SecGroupXML;
public string DataAreasXML;
private static int engineFileLoggerInitialized;
public SecureString Password { get => password; set => password = value; }
public string StartDir { get => startDir; set => startDir = value; }
@@ -130,15 +130,26 @@ namespace C4IT_IAM_GET
public DataArea_FileSystem()
{
var logDirectory = Environment.ExpandEnvironmentVariables(constApplicationDataPath);
Helper.CreatePathWithWriteAccess(logDirectory);
var LogPath = Path.Combine(logDirectory, "Logs");
cLogManagerFile.CreateInstance(Path.Combine(LogPath, "LIAM.log"));
DefaultLogger.LogEntry(LogLevels.Info, "=================================================");
DefaultLogger.LogEntry(LogLevels.Info, $"LIAM engine v{Assembly.GetExecutingAssembly().GetName().Version} started");
EnsureEngineFileLoggerInitialized();
if (DefaultLogger != null)
{
DefaultLogger.LogEntry(LogLevels.Info, "=================================================");
DefaultLogger.LogEntry(LogLevels.Info, $"LIAM engine v{Assembly.GetExecutingAssembly().GetName().Version} started");
}
}
private static void EnsureEngineFileLoggerInitialized()
{
if (DefaultLogger != null)
return;
if (Interlocked.Exchange(ref engineFileLoggerInitialized, 1) != 0)
return;
cLogManagerFile.CreateInstance(LocalMachine: true, A: Assembly.GetExecutingAssembly());
}
private void FillMatchingGroups()
{
LogMethodBegin(MethodBase.GetCurrentMethod());

View File

@@ -15,7 +15,7 @@ namespace C4IT_IAM_Engine
public const int MaxAdGroupDescriptionLength = 1024;
public const int MaxAdGroupLoopDigits = 3;
public const string DefaultGroupNameSanitizeReplacement = "_";
private const string AdUnsafeGroupNameCharactersPattern = @"[\x00-\x1F\s\-\/\\\[\]:;\|=,\+\*\?<>\@()'""]";
private const string AdUnsafeGroupNameCharactersPattern = @"[\x00-\x1F\x7F\/\\\[\]:;\|=,\+\*\?<>]";
private const int MinLeadingRelativePathSegmentLength = 3;
private const int MinSingleLeadingRelativePathSegmentLength = 2;
private const int MinLastRelativePathSegmentLength = 12;
@@ -33,6 +33,9 @@ namespace C4IT_IAM_Engine
public sealed class RootPathTemplateContext
{
public string Server { get; set; } = string.Empty;
public string ServerName { get; set; } = string.Empty;
public string DfsNamespaceName { get; set; } = string.Empty;
public string ShareName { get; set; } = string.Empty;
public string[] Segments { get; set; } = Array.Empty<string>();
public string Name { get; set; } = string.Empty;
public string Path { get; set; } = string.Empty;
@@ -109,6 +112,8 @@ namespace C4IT_IAM_Engine
return new RootPathTemplateContext
{
Server = server,
ServerName = server,
ShareName = sanitizedPathSegments.Length == 0 ? string.Empty : sanitizedPathSegments[sanitizedPathSegments.Length - 1],
Segments = sanitizedPathSegments,
Name = sanitizedPathSegments.Length == 0 ? string.Empty : sanitizedPathSegments[sanitizedPathSegments.Length - 1],
Path = sanitizedPathSegments.Length == 0 ? string.Empty : JoinSanitizedPathSegments(sanitizedPathSegments, groupNameSanitizeReplacement),
@@ -308,6 +313,9 @@ namespace C4IT_IAM_Engine
var context = rootContext ?? new RootPathTemplateContext();
var result = Regex.Replace(templateValue, @"{{\s*ROOT_SERVER\s*}}", context.Server ?? string.Empty, RegexOptions.IgnoreCase);
result = Regex.Replace(result, @"{{\s*SERVER_NAME\s*}}", context.ServerName ?? context.Server ?? string.Empty, RegexOptions.IgnoreCase);
result = Regex.Replace(result, @"{{\s*DFS_NAMESPACE_NAME\s*}}", context.DfsNamespaceName ?? string.Empty, RegexOptions.IgnoreCase);
result = Regex.Replace(result, @"{{\s*SHARE_NAME\s*}}", context.ShareName ?? string.Empty, RegexOptions.IgnoreCase);
result = Regex.Replace(result, @"{{\s*ROOT_NAME\s*}}", context.Name ?? string.Empty, RegexOptions.IgnoreCase);
result = Regex.Replace(result, @"{{\s*ROOT_PATH(?:\s*\(\s*(\d+)\s*\))?\s*}}", match =>
{

View File

@@ -26,9 +26,13 @@ namespace C4IT_IAM_Engine
public SecureString password;
public bool ForceStrictAdGroupNames;
public bool PreserveAdGroupNameCase;
public SecurityGroupReuseMode ReuseMode = SecurityGroupReuseMode.Safe;
public bool MarkerBackfill;
public List<IAM_SecurityGroup> IAM_SecurityGroups;
public string rootUID;
private const string MarkerAttributeName = "info";
private const string MarkerPrefix = "LIAM;Provider=Ntfs;";
public SecurityGroups()
{
IAM_SecurityGroups = new List<IAM_SecurityGroup>();
@@ -138,7 +142,8 @@ namespace C4IT_IAM_Engine
int loop = 0,
int existingADGroupCount = 0,
string groupNameSanitizeReplacement = Helper.DefaultGroupNameSanitizeReplacement,
bool preserveAdGroupNameCase = false)
bool preserveAdGroupNameCase = false,
Helper.RootPathTemplateContext templateContext = null)
{
LogMethodBegin(MethodBase.GetCurrentMethod());
try
@@ -159,7 +164,7 @@ namespace C4IT_IAM_Engine
var folderName = sanitizedSegments.Length > 0
? sanitizedSegments[sanitizedSegments.Length - 1]
: Helper.SanitizePathSegment(Path.GetFileName(newFolderPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)), groupNameSanitizeReplacement);
var rootContext = Helper.GetRootPathTemplateContext(baseFolder, groupNameSanitizeReplacement);
var rootContext = templateContext ?? Helper.GetRootPathTemplateContext(baseFolder, groupNameSanitizeReplacement);
foreach (var template in resolvedTemplates)
{
@@ -269,9 +274,11 @@ namespace C4IT_IAM_Engine
WildcardPattern = ownerGlobal.WildcardTemplate,
technicalName = "CN=" + ownerGlobal.NamingTemplate + "," + ouPath,
securityGroupType = SecurityGroupType.Owner,
targetTyp = (int)IAM_TargetType.FileSystem,
rights = (FileSystemRights)ownerACLPermission,
Scope = GroupScope.Global
Scope = GroupScope.Global,
MarkerPath = newFolderPath
};
IAM_SecurityGroups.Add(osecGroup);
@@ -283,9 +290,11 @@ namespace C4IT_IAM_Engine
WildcardPattern = writeGlobal.WildcardTemplate,
technicalName = "CN=" + writeGlobal.NamingTemplate + "," + ouPath,
securityGroupType = SecurityGroupType.Write,
targetTyp = (int)IAM_TargetType.FileSystem,
rights = (FileSystemRights)writeACLPermission,
Scope = GroupScope.Global
Scope = GroupScope.Global,
MarkerPath = newFolderPath
};
IAM_SecurityGroups.Add(wsecGroup);
@@ -297,9 +306,11 @@ namespace C4IT_IAM_Engine
WildcardPattern = readGlobal.WildcardTemplate,
technicalName = "CN=" + readGlobal.NamingTemplate + "," + ouPath,
securityGroupType = SecurityGroupType.Read,
targetTyp = (int)IAM_TargetType.FileSystem,
rights = (FileSystemRights)readACLPermission,
Scope = GroupScope.Global
Scope = GroupScope.Global,
MarkerPath = newFolderPath
};
IAM_SecurityGroups.Add(rsecGroup);
@@ -315,9 +326,11 @@ namespace C4IT_IAM_Engine
WildcardPattern = ownerDL.WildcardTemplate,
technicalName = "CN=" + ownerDL.NamingTemplate + "," + ouPath,
securityGroupType = SecurityGroupType.Owner,
targetTyp = (int)IAM_TargetType.FileSystem,
rights = (FileSystemRights)ownerACLPermission,
Scope = GroupScope.Local
Scope = GroupScope.Local,
MarkerPath = newFolderPath
};
osecDLGroup.memberGroups.Add(osecGroup);
IAM_SecurityGroups.Add(osecDLGroup);
@@ -330,9 +343,11 @@ namespace C4IT_IAM_Engine
WildcardPattern = writeDL.WildcardTemplate,
technicalName = "CN=" + writeDL.NamingTemplate + "," + ouPath,
securityGroupType = SecurityGroupType.Write,
targetTyp = (int)IAM_TargetType.FileSystem,
rights = (FileSystemRights)writeACLPermission,
Scope = GroupScope.Local
Scope = GroupScope.Local,
MarkerPath = newFolderPath
};
wsecDLGroup.memberGroups.Add(wsecGroup);
IAM_SecurityGroups.Add(wsecDLGroup);
@@ -345,9 +360,11 @@ namespace C4IT_IAM_Engine
WildcardPattern = readDL.WildcardTemplate,
technicalName = "CN=" + readDL.NamingTemplate + "," + ouPath,
securityGroupType = SecurityGroupType.Read,
targetTyp = (int)IAM_TargetType.FileSystem,
rights = (FileSystemRights)readACLPermission,
Scope = GroupScope.Local
Scope = GroupScope.Local,
MarkerPath = newFolderPath
};
rsecDLGroup.memberGroups.Add(rsecGroup);
IAM_SecurityGroups.Add(rsecDLGroup);
@@ -661,6 +678,168 @@ namespace C4IT_IAM_Engine
group.CommitChanges();
}
private static string NormalizeMarkerPath(string path)
{
if (string.IsNullOrWhiteSpace(path))
return string.Empty;
return path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar).TrimEnd(Path.DirectorySeparatorChar);
}
private static string GetMarkerScope(GroupScope scope)
{
return scope == GroupScope.Local ? "DomainLocal" : "Global";
}
private static string BuildMarker(IAM_SecurityGroup secGroup)
{
return $"{MarkerPrefix}Path={NormalizeMarkerPath(secGroup.MarkerPath)};Role={secGroup.securityGroupType};Scope={GetMarkerScope(secGroup.Scope)}";
}
private static Dictionary<string, string> ParseMarker(string marker)
{
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(marker) || !marker.StartsWith(MarkerPrefix, StringComparison.OrdinalIgnoreCase))
return result;
foreach (var part in marker.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
{
var separatorIndex = part.IndexOf('=');
if (separatorIndex <= 0)
continue;
result[part.Substring(0, separatorIndex).Trim()] = part.Substring(separatorIndex + 1).Trim();
}
return result;
}
private static IEnumerable<string> GetLiamMarkers(string info)
{
if (string.IsNullOrWhiteSpace(info))
return Enumerable.Empty<string>();
return info
.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries)
.Select(i => i.Trim())
.Where(i => i.StartsWith(MarkerPrefix, StringComparison.OrdinalIgnoreCase));
}
private static bool IsMatchingMarker(string marker, IAM_SecurityGroup secGroup)
{
var values = ParseMarker(marker);
if (values.Count == 0)
return false;
return values.TryGetValue("Provider", out var provider)
&& string.Equals(provider, "Ntfs", StringComparison.OrdinalIgnoreCase)
&& values.TryGetValue("Path", out var markerPath)
&& string.Equals(NormalizeMarkerPath(markerPath), NormalizeMarkerPath(secGroup.MarkerPath), StringComparison.OrdinalIgnoreCase)
&& values.TryGetValue("Role", out var role)
&& string.Equals(role, secGroup.securityGroupType.ToString(), StringComparison.OrdinalIgnoreCase)
&& values.TryGetValue("Scope", out var scope)
&& string.Equals(scope, GetMarkerScope(secGroup.Scope), StringComparison.OrdinalIgnoreCase);
}
private bool TryReadInfoAttribute(DirectoryEntry group, out string info)
{
info = string.Empty;
if (group == null)
return false;
try
{
group.RefreshCache(new[] { MarkerAttributeName });
if (group.Properties.Contains(MarkerAttributeName) && group.Properties[MarkerAttributeName].Count > 0)
info = group.Properties[MarkerAttributeName].Value?.ToString() ?? string.Empty;
return true;
}
catch (Exception E)
{
DefaultLogger.LogEntry(LogLevels.Warning, $"AD group marker attribute '{MarkerAttributeName}' cannot be read for '{group.Path}'. Marker-based reuse is not available. {E.Message}");
return false;
}
}
private bool HasMatchingMarker(DirectoryEntry group, IAM_SecurityGroup secGroup)
{
if (!TryReadInfoAttribute(group, out var info))
return false;
return GetLiamMarkers(info).Any(marker => IsMatchingMarker(marker, secGroup));
}
private bool TryWriteMarker(DirectoryEntry group, IAM_SecurityGroup secGroup, bool onlyIfNoLiamMarker)
{
if (group == null)
return false;
if (!TryReadInfoAttribute(group, out var info))
return false;
var existingMarkers = GetLiamMarkers(info).ToList();
if (existingMarkers.Any(marker => IsMatchingMarker(marker, secGroup)))
return true;
if (onlyIfNoLiamMarker && existingMarkers.Count > 0)
{
DefaultLogger.LogEntry(LogLevels.Warning, $"AD group '{secGroup.Name}' already has a LIAM marker, but it does not match path '{secGroup.MarkerPath}' and role '{secGroup.securityGroupType}'. Marker backfill is skipped.");
return false;
}
try
{
var marker = BuildMarker(secGroup);
var newInfo = string.IsNullOrWhiteSpace(info)
? marker
: info.TrimEnd('\r', '\n') + Environment.NewLine + marker;
group.Properties[MarkerAttributeName].Value = newInfo;
group.CommitChanges();
DefaultLogger.LogEntry(LogLevels.Debug, $"AD group marker written to '{secGroup.Name}': {marker}");
return true;
}
catch (Exception E)
{
DefaultLogger.LogEntry(LogLevels.Warning, $"AD group marker attribute '{MarkerAttributeName}' cannot be written for '{secGroup.Name}'. {E.Message}");
return false;
}
}
private DirectoryEntry ValidateExistingGroupReuse(DirectoryEntry existingGroup, IAM_SecurityGroup secGroup, string folderPath, bool linkedFromFolderAcl)
{
if (existingGroup == null)
return null;
if (ReuseMode == SecurityGroupReuseMode.Name)
{
if (!linkedFromFolderAcl && !HasMatchingMarker(existingGroup, secGroup))
DefaultLogger.LogEntry(LogLevels.Warning, $"AD group '{secGroup.Name}' is reused by name because NtfsAdGroupReuseMode=Name is active. This bypasses ACL/marker validation for '{secGroup.MarkerPath}'.");
ApplyExistingGroup(secGroup, existingGroup);
return existingGroup;
}
if (linkedFromFolderAcl)
{
if (MarkerBackfill)
TryWriteMarker(existingGroup, secGroup, true);
ApplyExistingGroup(secGroup, existingGroup);
return existingGroup;
}
if (HasMatchingMarker(existingGroup, secGroup))
{
ApplyExistingGroup(secGroup, existingGroup);
return existingGroup;
}
var message = $"Existing AD group '{secGroup.Name}' was found by name for '{folderPath ?? secGroup.MarkerPath}', but it is not linked on the folder ACL and has no matching LIAM marker. Reuse is blocked in NtfsAdGroupReuseMode=Safe. Set NtfsAdGroupReuseMode=Name to allow legacy name-based reuse.";
DefaultLogger.LogEntry(LogLevels.Warning, message);
throw new InvalidOperationException(message);
}
public DirectoryEntry EnsureADGroup(string ouPath, IAM_SecurityGroup secGroup, List<UserPrincipal> users, string folderPath = null)
{
LogMethodBegin(MethodBase.GetCurrentMethod());
@@ -668,8 +847,12 @@ namespace C4IT_IAM_Engine
{
secGroup.CreatedNewEntry = false;
DirectoryEntry existingGroup = null;
var linkedFromFolderAcl = false;
if (!ForceStrictAdGroupNames)
{
existingGroup = FindGroupEntryFromFolderAcl(folderPath, secGroup.WildcardPattern);
linkedFromFolderAcl = existingGroup != null;
}
if (existingGroup == null)
existingGroup = FindGroupEntry(secGroup.Name);
@@ -680,8 +863,8 @@ namespace C4IT_IAM_Engine
if (existingGroup == null)
return CreateADGroup(ouPath, secGroup, users);
existingGroup = ValidateExistingGroupReuse(existingGroup, secGroup, folderPath, linkedFromFolderAcl);
AddMissingMembers(existingGroup, secGroup, users);
ApplyExistingGroup(secGroup, existingGroup);
return existingGroup;
}
catch (Exception E)
@@ -702,8 +885,12 @@ namespace C4IT_IAM_Engine
{
secGroup.CreatedNewEntry = false;
DirectoryEntry existingGroup = null;
var linkedFromFolderAcl = false;
if (!ForceStrictAdGroupNames)
{
existingGroup = FindGroupEntryFromFolderAcl(folderPath, secGroup.WildcardPattern);
linkedFromFolderAcl = existingGroup != null;
}
if (existingGroup == null)
existingGroup = FindGroupEntry(secGroup.Name);
@@ -714,8 +901,7 @@ namespace C4IT_IAM_Engine
if (existingGroup == null)
return null;
ApplyExistingGroup(secGroup, existingGroup);
return existingGroup;
return ValidateExistingGroupReuse(existingGroup, secGroup, folderPath, linkedFromFolderAcl);
}
catch (Exception E)
{
@@ -738,55 +924,45 @@ namespace C4IT_IAM_Engine
secGroup.Name = groupName;
secGroup.technicalName = "CN=" + groupName + "," + ouPath;
if (!GroupAllreadyExisting(groupName))
if (GroupAllreadyExisting(groupName))
throw new InvalidOperationException($"AD group '{groupName}' already exists. Create mode does not reuse existing groups.");
DirectoryEntry entry = new DirectoryEntry("LDAP://" + GetLdapServer() + "/" + ouPath, username, new NetworkCredential("", password).Password, AuthenticationTypes.Secure | AuthenticationTypes.Sealing);
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)
{
DirectoryEntry entry = new DirectoryEntry("LDAP://" + GetLdapServer() + "/" + ouPath, username, new NetworkCredential("", password).Password, AuthenticationTypes.Secure | AuthenticationTypes.Sealing);
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)
{
foreach (var user in users)
{
DefaultLogger.LogEntry(LogLevels.Debug, $"Adding member: {user.DistinguishedName}");
group.Properties["member"].Add(user.DistinguishedName);
}
DefaultLogger.LogEntry(LogLevels.Debug, $"Adding member: {user.DistinguishedName}");
group.Properties["member"].Add(user.DistinguishedName);
}
if(!String.IsNullOrEmpty(secGroup.description))
{
DefaultLogger.LogEntry(LogLevels.Debug, $"Setting description: {secGroup.description}");
group.Properties["description"].Value = secGroup.description;
}
var groupType = secGroup.Scope == GroupScope.Global ? GroupScopeValues.Global : GroupScopeValues.Local;
DefaultLogger.LogEntry(LogLevels.Debug, $"Setting groupType to: {groupType}");
group.Properties["groupType"].Value = groupType;
if (secGroup.Scope == GroupScope.Local)
foreach (var iGroup in secGroup.memberGroups)
{
DefaultLogger.LogEntry(LogLevels.Debug, $"Adding member: {iGroup.technicalName}");
group.Properties["member"].Add(iGroup.technicalName);
}
group.CommitChanges();
DirectoryEntry ent = new DirectoryEntry("LDAP://" + GetLdapServer() + "/" + "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}");
secGroup.UID = objectid;
secGroup.CreatedNewEntry = true;
return ent;
}
else
if(!String.IsNullOrEmpty(secGroup.description))
{
DirectoryEntry e = FindGroupEntry(secGroup.Name);
if (e == null)
return null;
AddMissingMembers(e, secGroup, users);
ApplyExistingGroup(secGroup, e);
return e;
DefaultLogger.LogEntry(LogLevels.Debug, $"Setting description: {secGroup.description}");
group.Properties["description"].Value = secGroup.description;
}
return null;
var groupType = secGroup.Scope == GroupScope.Global ? GroupScopeValues.Global : GroupScopeValues.Local;
DefaultLogger.LogEntry(LogLevels.Debug, $"Setting groupType to: {groupType}");
group.Properties["groupType"].Value = groupType;
if (secGroup.Scope == GroupScope.Local)
foreach (var iGroup in secGroup.memberGroups)
{
DefaultLogger.LogEntry(LogLevels.Debug, $"Adding member: {iGroup.technicalName}");
group.Properties["member"].Add(iGroup.technicalName);
}
group.CommitChanges();
DirectoryEntry ent = new DirectoryEntry("LDAP://" + GetLdapServer() + "/" + "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}");
secGroup.UID = objectid;
secGroup.CreatedNewEntry = true;
TryWriteMarker(ent, secGroup, false);
return ent;
}
catch (Exception E)
{
@@ -865,11 +1041,17 @@ namespace C4IT_IAM_Engine
public int targetTyp;
public GroupScope Scope;
public FileSystemRights rights;
public string MarkerPath;
public IAM_SecurityGroup()
{
memberGroups = new List<IAM_SecurityGroup>();
}
}
public enum SecurityGroupReuseMode
{
Safe,
Name
}
public enum SecurityGroupType
{
[XmlEnum(Name = "0")]

View File

@@ -25,6 +25,7 @@ namespace LiamNtfs
public PrincipalContext adContext = null;
public Exception LastException { get; private set; } = null;
public string LastErrorMessage { get; private set; } = null;
public cNtfsEnumerationDiagnostics LastEnumerationDiagnostics { get; private set; } = new cNtfsEnumerationDiagnostics();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ResetError()
@@ -169,6 +170,7 @@ namespace LiamNtfs
public async Task<cNtfsCollectionBase> RequestFoldersListAsync(string rootPath, int depth)
{
ResetError();
LastEnumerationDiagnostics = new cNtfsEnumerationDiagnostics(rootPath, depth);
try
{
await Task.Delay(0);
@@ -217,7 +219,9 @@ namespace LiamNtfs
}
catch (Exception E)
{
cLogManager.LogEntry($"Could not enumerate directories under '{rootPath.FullName}': {E.Message}", LogLevels.Warning);
LastEnumerationDiagnostics.EnumerationFailures++;
LastEnumerationDiagnostics.AddFailure(rootPath.FullName, E);
cLogManager.LogEntry($"NTFS enumeration failed under '{rootPath.FullName}'. ReturnedPartialResult=true. Error='{E.Message}'", LogLevels.Warning);
cLogManager.LogException(E, LogLevels.Debug);
return folders;
}
@@ -239,11 +243,14 @@ namespace LiamNtfs
}
catch (Exception E)
{
cLogManager.LogEntry($"Could not read directory metadata for '{directory.FullName}': {E.Message}", LogLevels.Warning);
LastEnumerationDiagnostics.MetadataFailures++;
LastEnumerationDiagnostics.AddFailure(directory.FullName, E);
cLogManager.LogEntry($"NTFS directory metadata read failed for '{directory.FullName}'. DirectorySkipped=true. Error='{E.Message}'", LogLevels.Warning);
cLogManager.LogException(E, LogLevels.Debug);
continue;
}
LastEnumerationDiagnostics.FoldersEnumerated++;
folders.Add(folder);
if (depth <= 0)
continue;
@@ -256,7 +263,9 @@ namespace LiamNtfs
}
catch (Exception E)
{
cLogManager.LogEntry($"Could not scan subtree '{directory.FullName}': {E.Message}", LogLevels.Warning);
LastEnumerationDiagnostics.SubtreeFailures++;
LastEnumerationDiagnostics.AddFailure(directory.FullName, E);
cLogManager.LogEntry($"NTFS subtree scan failed for '{directory.FullName}'. ReturnedPartialResult=true. Error='{E.Message}'", LogLevels.Warning);
cLogManager.LogException(E, LogLevels.Debug);
}
}
@@ -330,6 +339,39 @@ namespace LiamNtfs
}
}
public class cNtfsEnumerationDiagnostics
{
private const int MaxSamples = 10;
public cNtfsEnumerationDiagnostics()
{
}
public cNtfsEnumerationDiagnostics(string rootPath, int depth)
{
RootPath = rootPath ?? string.Empty;
Depth = depth;
}
public string RootPath { get; private set; } = string.Empty;
public int Depth { get; private set; }
public int FoldersEnumerated { get; set; }
public int EnumerationFailures { get; set; }
public int MetadataFailures { get; set; }
public int SubtreeFailures { get; set; }
public List<string> FailureSamples { get; } = new List<string>();
public int TotalFailures => EnumerationFailures + MetadataFailures + SubtreeFailures;
public void AddFailure(string path, Exception exception)
{
if (FailureSamples.Count >= MaxSamples)
return;
FailureSamples.Add($"{path}: {exception?.GetType().Name}: {exception?.Message}");
}
}
public class cNtfsLogonInfo
{

View File

@@ -68,7 +68,10 @@ namespace C4IT.LIAM.Activities
var dataMain = dataProvider.GetDataList(constFragmentNameConfigProviderMain,
"ID, [Expression-ObjectId] as EOID, GCCDomain, GCCTarget, GCCMaxDepth, " +
"GCCgroupLDAPFilter, GCCgroupOUPath, GCCPermissionGroupStrategy, GCCtargetType",
"GCCgroupLDAPFilter, GCCgroupOUPath, GCCPermissionGroupStrategy, GCCtargetType, " +
"OwnerACLPermission.C4IT_EnumValueInt as OwnerACLPermission, " +
"WriteACLPermission.C4IT_EnumValueInt as WriteACLPermission, " +
"ReadACLPermission.C4IT_EnumValueInt as ReadACLPermission",
$"[Expression-ObjectId] = '{configEOID}'");
Guid ConfigClassId = (Guid)dataMain.First()["ID"];

View File

@@ -2,7 +2,9 @@
using System;
using System.Activities;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using System.Security.Cryptography;
using static C4IT.Logging.cLogManager;
using Matrix42.Contracts.Platform.Data;
using Matrix42.ServiceRepository.Contracts.Components;
@@ -120,6 +122,9 @@ namespace LiamWorkflowActivities
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
cLogManager.DefaultLogger.LogAssemblyInfo(Ass);
LogAssemblyIdentity("WorkflowActivities", Ass);
LogAssemblyIdentity("LiamNtfs", typeof(cLiamProviderNtfs).Assembly);
LogAssemblyIdentity("LiamBaseClasses", typeof(cLiamProviderBase).Assembly);
IsInitialized = true;
LogMethodEnd(CM);
}
@@ -127,6 +132,39 @@ namespace LiamWorkflowActivities
catch { };
}
private static void LogAssemblyIdentity(string label, Assembly assembly)
{
if (assembly == null)
return;
var fileVersion = string.IsNullOrWhiteSpace(assembly.Location)
? null
: FileVersionInfo.GetVersionInfo(assembly.Location);
var informationalVersion = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? string.Empty;
LogEntry(
$"LIAM assembly identity: Label='{label}', Name='{assembly.GetName().Name}', AssemblyVersion='{assembly.GetName().Version}', InformationalVersion='{informationalVersion}', FileVersion='{fileVersion?.FileVersion}', ProductVersion='{fileVersion?.ProductVersion}', Sha256='{GetAssemblySha256(assembly)}', Location='{assembly.Location}'",
LogLevels.Info);
}
private static string GetAssemblySha256(Assembly assembly)
{
try
{
if (assembly == null || string.IsNullOrWhiteSpace(assembly.Location) || !File.Exists(assembly.Location))
return string.Empty;
using (var stream = File.OpenRead(assembly.Location))
using (var sha256 = SHA256.Create())
{
return BitConverter.ToString(sha256.ComputeHash(stream)).Replace("-", string.Empty);
}
}
catch
{
return string.Empty;
}
}
public bool LoadLicensingInformation(bool force = false)
{
if (cC4ITLicenseM42ESM.Instance == null || force)
@@ -262,7 +300,10 @@ namespace LiamWorkflowActivities
GroupFilter = cLIAMHelper.getStringFromObject(dataMain["GCCgroupLDAPFilter"]),
GroupPath = cLIAMHelper.getStringFromObject(dataMain["GCCgroupOUPath"]),
GroupStrategy = (eLiamGroupStrategies)cLIAMHelper.getIntFromObject(dataMain["GCCPermissionGroupStrategy"]),
ProviderType = (eLiamProviderTypes)cLIAMHelper.getIntFromObject(dataMain["GCCtargetType"])
ProviderType = (eLiamProviderTypes)cLIAMHelper.getIntFromObject(dataMain["GCCtargetType"]),
OwnerACLPermission = GetAclPermissionFromDataRow(dataMain, "OwnerACLPermission", cLiamAclPermissionDefaults.Owner),
WriteACLPermission = GetAclPermissionFromDataRow(dataMain, "WriteACLPermission", cLiamAclPermissionDefaults.Write),
ReadACLPermission = GetAclPermissionFromDataRow(dataMain, "ReadACLPermission", cLiamAclPermissionDefaults.Read)
};
if (dataAdditional != null)
{
@@ -383,6 +424,34 @@ namespace LiamWorkflowActivities
}
private int GetAclPermissionFromDataRow(DataRow row, string columnName, int defaultValue)
{
if (row == null || string.IsNullOrWhiteSpace(columnName) || !row.Table.Columns.Contains(columnName))
return defaultValue;
var rawValue = cLIAMHelper.getStringFromObject(row[columnName]);
if (string.IsNullOrWhiteSpace(rawValue))
return defaultValue;
var value = rawValue.Trim();
try
{
if (value.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
return Convert.ToInt32(value.Substring(2), 16);
if (int.TryParse(value, out var parsedValue))
return parsedValue;
}
catch (Exception ex)
{
LogEntry($"Could not parse ACL permission config value '{columnName}'='{rawValue}': {ex.Message}. Falling back to default '{defaultValue}'.", LogLevels.Warning);
return defaultValue;
}
LogEntry($"Could not parse ACL permission config value '{columnName}'='{rawValue}'. Falling back to default '{defaultValue}'.", LogLevels.Warning);
return defaultValue;
}
public ProviderCacheEntry getDataProvider(Guid ProviderConfigClassID)
{
var CM = MethodBase.GetCurrentMethod();

View File

@@ -5,6 +5,7 @@ using C4IT_IAM_Engine;
using LiamAD;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
@@ -77,6 +78,9 @@ namespace LiamWorkflowActivities
public static class LiamWorkflowRuntime
{
private const string AutomaticNtfsEnsureTimeoutSecondsKey = "NtfsAutomaticEnsureTimeoutSeconds";
private const int DefaultAutomaticNtfsEnsureTimeoutSeconds = 300;
public static async Task<GetDataAreasOperationResult> GetDataAreasFromProviderAsync(cLiamProviderBase provider, string configurationId = null, bool? simulateConfiguredNtfsPermissionEnsure = null)
{
var result = new GetDataAreasOperationResult();
@@ -421,62 +425,219 @@ namespace LiamWorkflowActivities
var allowFolderEnsure = IsAdditionalConfigurationEnabled(provider, "EnsureNtfsPermissionGroups");
var allowSharePathEnsure = IsAdditionalConfigurationEnabled(provider, "EnsureNtfsPermissionGroupsForShares");
if (!allowFolderEnsure && !allowSharePathEnsure)
var allowTraverseEnsure = IsAdditionalConfigurationEnabled(provider, "EnsureNtfsTraverseGroups");
var ensureTimeoutSeconds = GetAdditionalConfigurationInt32(
provider,
AutomaticNtfsEnsureTimeoutSecondsKey,
DefaultAutomaticNtfsEnsureTimeoutSeconds,
0);
var ensureTimeout = ensureTimeoutSeconds > 0
? TimeSpan.FromSeconds(ensureTimeoutSeconds)
: TimeSpan.Zero;
if (!allowFolderEnsure && !allowSharePathEnsure && !allowTraverseEnsure)
return true;
foreach (var ntfsArea in dataAreas
.Where(dataArea =>
allowFolderEnsure && dataArea is cLiamNtfsFolder
|| allowSharePathEnsure && dataArea is cLiamNtfsShare)
.Cast<cLiamNtfsPermissionDataAreaBase>())
LogEntry(
$"Automatic NTFS ensure configured. PermissionGroups={allowFolderEnsure}, PermissionGroupsForShares={allowSharePathEnsure}, TraverseGroups={allowTraverseEnsure}, DataAreas={dataAreas.Count}, WhatIf={simulateOnly}, TimeoutSeconds={ensureTimeoutSeconds}",
LogLevels.Debug);
if (allowFolderEnsure || allowSharePathEnsure)
{
var folderPath = ntfsArea.TechnicalName;
if (string.IsNullOrWhiteSpace(folderPath))
continue;
if (!Directory.Exists(folderPath))
foreach (var ntfsArea in dataAreas
.Where(dataArea =>
allowFolderEnsure && dataArea is cLiamNtfsFolder
|| allowSharePathEnsure && dataArea is cLiamNtfsShare)
.Cast<cLiamNtfsPermissionDataAreaBase>())
{
LogEntry($"Skipping automatic NTFS permission group ensure for '{folderPath}' because the directory does not exist.", LogLevels.Warning);
continue;
}
var folderPath = ntfsArea.TechnicalName;
if (string.IsNullOrWhiteSpace(folderPath))
continue;
var ensureResult = await ntfsProvider.EnsureMissingPermissionGroupsAsync(
folderPath,
null,
null,
null,
null,
allowSharePathEnsure,
ntfsArea is cLiamNtfsFolder,
simulateOnly);
if (ensureResult == null)
var ensureTraverseInPermissionPhase = !allowTraverseEnsure && (ntfsArea is cLiamNtfsFolder || ntfsArea is cLiamNtfsShare);
LogEntry(
$"Automatic NTFS permission group ensure starting for '{folderPath}'. AllowSharePathEnsure={allowSharePathEnsure}, EnsureTraverseInPermissionPhase={ensureTraverseInPermissionPhase}, WhatIf={simulateOnly}",
LogLevels.Debug);
ResultToken ensureResult;
try
{
ensureResult = await RunAutomaticNtfsEnsureOperationAsync(
folderPath,
"permission group ensure",
() => ntfsProvider.EnsureMissingPermissionGroupsAsync(
folderPath,
null,
null,
null,
null,
allowSharePathEnsure,
ensureTraverseInPermissionPhase,
simulateOnly),
ensureTimeout);
}
catch (Exception ex)
{
LogException(ex);
LogEntry($"Automatic NTFS permission group ensure failed for '{folderPath}' with exception: {ex.Message}", LogLevels.Error);
continue;
}
if (ensureResult == null)
{
var providerMessage = ntfsProvider.GetLastErrorMessage() ?? "Provider returned no result.";
LogEntry(
$"Automatic NTFS permission group ensure skipped for '{folderPath}' because the provider returned no result. ProviderMessage='{providerMessage}'",
LogLevels.Warning);
continue;
}
if (ensureResult.resultErrorId != 0)
{
if (IsAutomaticNtfsEnsureTimeout(ensureResult))
{
SetAutomaticEnsureTimeoutError(result, folderPath, ensureResult, "permission group ensure");
return false;
}
LogEntry(
$"Automatic NTFS permission group ensure skipped for '{folderPath}' after resultErrorId={ensureResult.resultErrorId}. ResultMessage='{ensureResult.resultMessage ?? string.Empty}'",
LogLevels.Warning);
continue;
}
if (simulateOnly)
{
LogAutomaticNtfsEnsurePreviewDebug(folderPath, ensureResult, "permission group ensure");
result.AutomaticEnsurePreview.Add(MapAutomaticEnsurePreview(folderPath, ensureResult));
continue;
}
LogAutomaticNtfsEnsureDebug(folderPath, ensureResult, "permission group ensure");
LogEntry($"Automatic NTFS permission group resolve starting for '{folderPath}'.", LogLevels.Debug);
await ntfsArea.ResolvePermissionGroupsAsync(folderPath);
LogEntry($"Automatic NTFS permission group resolve finished for '{folderPath}'.", LogLevels.Debug);
}
}
if (allowTraverseEnsure)
{
foreach (var ntfsArea in dataAreas
.Where(dataArea => dataArea is cLiamNtfsFolder || dataArea is cLiamNtfsShare)
.Cast<cLiamNtfsPermissionDataAreaBase>())
{
result.ErrorCode = "WF_GET_DATAAREAS_ENSURE_NTFS_GROUPS_FAILED";
result.ErrorMessage = $"Automatic NTFS permission group ensure failed for '{folderPath}' because the provider returned no result.";
return false;
}
var folderPath = ntfsArea.TechnicalName;
if (string.IsNullOrWhiteSpace(folderPath))
continue;
if (ensureResult.resultErrorId != 0)
{
result.ErrorCode = "WF_GET_DATAAREAS_ENSURE_NTFS_GROUPS_FAILED";
result.ErrorMessage = $"Automatic NTFS permission group ensure failed for '{folderPath}': {ensureResult.resultMessage}";
return false;
}
LogEntry(
$"Automatic NTFS traverse group ensure starting for '{folderPath}'. WhatIf={simulateOnly}",
LogLevels.Debug);
if (simulateOnly)
{
LogAutomaticNtfsEnsurePreviewDebug(folderPath, ensureResult);
result.AutomaticEnsurePreview.Add(MapAutomaticEnsurePreview(folderPath, ensureResult));
continue;
}
ResultToken ensureResult;
try
{
ensureResult = await RunAutomaticNtfsEnsureOperationAsync(
folderPath,
"traverse group ensure",
() => ntfsProvider.EnsureTraverseGroupsAsync(
folderPath,
null,
simulateOnly),
ensureTimeout);
}
catch (Exception ex)
{
LogException(ex);
LogEntry($"Automatic NTFS traverse group ensure failed for '{folderPath}' with exception: {ex.Message}", LogLevels.Error);
continue;
}
LogAutomaticNtfsEnsureDebug(folderPath, ensureResult);
await ntfsArea.ResolvePermissionGroupsAsync(folderPath);
if (ensureResult == null)
{
var providerMessage = ntfsProvider.GetLastErrorMessage() ?? "Provider returned no result.";
LogEntry(
$"Automatic NTFS traverse group ensure skipped for '{folderPath}' because the provider returned no result. ProviderMessage='{providerMessage}'",
LogLevels.Warning);
continue;
}
if (ensureResult.resultErrorId != 0)
{
if (IsAutomaticNtfsEnsureTimeout(ensureResult))
{
SetAutomaticEnsureTimeoutError(result, folderPath, ensureResult, "traverse group ensure");
return false;
}
LogEntry(
$"Automatic NTFS traverse group ensure skipped for '{folderPath}' after resultErrorId={ensureResult.resultErrorId}. ResultMessage='{ensureResult.resultMessage ?? string.Empty}'",
LogLevels.Warning);
continue;
}
if (simulateOnly)
{
LogAutomaticNtfsEnsurePreviewDebug(folderPath, ensureResult, "traverse group ensure");
result.AutomaticEnsurePreview.Add(MapAutomaticEnsurePreview(folderPath, ensureResult));
continue;
}
LogAutomaticNtfsEnsureDebug(folderPath, ensureResult, "traverse group ensure");
}
}
return true;
}
private static async Task<ResultToken> RunAutomaticNtfsEnsureOperationAsync(
string folderPath,
string operationLabel,
Func<Task<ResultToken>> operation,
TimeSpan timeout)
{
var operationTask = Task.Run(operation);
#pragma warning disable 4014
// Fire-and-forget observer for exceptions that happen after a timeout.
operationTask.ContinueWith(
task => LogException(task.Exception),
TaskContinuationOptions.OnlyOnFaulted);
#pragma warning restore 4014
if (timeout <= TimeSpan.Zero)
return await operationTask;
var completedTask = await Task.WhenAny(operationTask, Task.Delay(timeout));
if (completedTask == operationTask)
return await operationTask;
var message =
$"Automatic NTFS {operationLabel} timed out for '{folderPath}' after {timeout.TotalSeconds:F0}s. " +
"The workflow will stop automatic NTFS ensure processing for this run. " +
"The underlying NTFS/AD operation may still finish in the worker process.";
LogEntry(message, LogLevels.Error);
return new ResultToken("AutomaticNtfsEnsureTimeout")
{
resultErrorId = 30010,
resultMessage = message
};
}
private static bool IsAutomaticNtfsEnsureTimeout(ResultToken ensureResult)
{
return ensureResult?.resultErrorId == 30010;
}
private static void SetAutomaticEnsureTimeoutError(
GetDataAreasOperationResult result,
string folderPath,
ResultToken ensureResult,
string operationLabel)
{
result.ErrorCode = "WF_GET_DATAAREAS_ENSURE_NTFS_GROUPS_TIMEOUT";
result.ErrorMessage = ensureResult?.resultMessage
?? $"Automatic NTFS {operationLabel} timed out for '{folderPath}'.";
}
private static NtfsAutomaticEnsurePreviewEntry MapAutomaticEnsurePreview(string folderPath, ResultToken ensureResult)
{
return new NtfsAutomaticEnsurePreviewEntry
@@ -493,13 +654,13 @@ namespace LiamWorkflowActivities
};
}
private static void LogAutomaticNtfsEnsurePreviewDebug(string folderPath, ResultToken ensureResult)
private static void LogAutomaticNtfsEnsurePreviewDebug(string folderPath, ResultToken ensureResult, string operationLabel)
{
if (ensureResult == null)
return;
LogEntry(
$"Automatic NTFS permission group ensure preview finished for '{folderPath}'. " +
$"Automatic NTFS {operationLabel} preview finished for '{folderPath}'. " +
$"WouldCreateGroups={ensureResult.createdGroups.Count}, " +
$"WouldReuseGroups={ensureResult.reusedGroups.Count}, " +
$"WouldAddAcls={ensureResult.addedAclEntries.Count}, " +
@@ -510,13 +671,13 @@ namespace LiamWorkflowActivities
LogLevels.Debug);
}
private static void LogAutomaticNtfsEnsureDebug(string folderPath, ResultToken ensureResult)
private static void LogAutomaticNtfsEnsureDebug(string folderPath, ResultToken ensureResult, string operationLabel)
{
if (ensureResult == null)
return;
LogEntry(
$"Automatic NTFS permission group ensure finished for '{folderPath}'. " +
$"Automatic NTFS {operationLabel} finished for '{folderPath}'. " +
$"CreatedGroups={ensureResult.createdGroups.Count}, " +
$"ReusedGroups={ensureResult.reusedGroups.Count}, " +
$"AddedAcls={ensureResult.addedAclEntries.Count}, " +
@@ -529,42 +690,42 @@ namespace LiamWorkflowActivities
if (ensureResult.createdGroups.Count > 0)
{
LogEntry(
$"Automatic NTFS permission group ensure detected missing AD groups for '{folderPath}' and created them: {string.Join(", ", ensureResult.createdGroups)}",
$"Automatic NTFS {operationLabel} detected missing AD groups for '{folderPath}' and created them: {string.Join(", ", ensureResult.createdGroups)}",
LogLevels.Debug);
}
if (ensureResult.reusedGroups.Count > 0)
{
LogEntry(
$"Automatic NTFS permission group ensure reused existing AD groups for '{folderPath}': {string.Join(", ", ensureResult.reusedGroups)}",
$"Automatic NTFS {operationLabel} reused existing AD groups for '{folderPath}': {string.Join(", ", ensureResult.reusedGroups)}",
LogLevels.Debug);
}
if (ensureResult.addedAclEntries.Count > 0)
{
LogEntry(
$"Automatic NTFS permission group ensure added missing ACL entries for '{folderPath}': {string.Join(", ", ensureResult.addedAclEntries)}",
$"Automatic NTFS {operationLabel} added missing ACL entries for '{folderPath}': {string.Join(", ", ensureResult.addedAclEntries)}",
LogLevels.Debug);
}
if (ensureResult.skippedAclEntries.Count > 0)
{
LogEntry(
$"Automatic NTFS permission group ensure kept existing ACL entries for '{folderPath}': {string.Join(", ", ensureResult.skippedAclEntries)}",
$"Automatic NTFS {operationLabel} kept existing ACL entries for '{folderPath}': {string.Join(", ", ensureResult.skippedAclEntries)}",
LogLevels.Debug);
}
if (ensureResult.ensuredTraverseGroups.Count > 0)
{
LogEntry(
$"Automatic NTFS permission group ensure touched traverse groups for '{folderPath}': {string.Join(", ", ensureResult.ensuredTraverseGroups)}",
$"Automatic NTFS {operationLabel} touched traverse groups for '{folderPath}': {string.Join(", ", ensureResult.ensuredTraverseGroups)}",
LogLevels.Debug);
}
if (ensureResult.warnings.Count > 0)
{
LogEntry(
$"Automatic NTFS permission group ensure produced warnings for '{folderPath}': {string.Join(" | ", ensureResult.warnings)}",
$"Automatic NTFS {operationLabel} produced warnings for '{folderPath}': {string.Join(" | ", ensureResult.warnings)}",
LogLevels.Debug);
}
}
@@ -582,6 +743,33 @@ namespace LiamWorkflowActivities
|| rawValue.Equals("yes", StringComparison.OrdinalIgnoreCase);
}
private static int GetAdditionalConfigurationInt32(cLiamProviderBase provider, string key, int defaultValue, int minValue)
{
if (provider?.AdditionalConfiguration == null || string.IsNullOrWhiteSpace(key))
return defaultValue;
if (!provider.AdditionalConfiguration.TryGetValue(key, out var rawValue) || string.IsNullOrWhiteSpace(rawValue))
return defaultValue;
if (!int.TryParse(rawValue.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedValue))
{
LogEntry(
$"AdditionalConfiguration '{key}' has invalid integer value '{rawValue}'. Defaulting to {defaultValue}.",
LogLevels.Warning);
return defaultValue;
}
if (parsedValue < minValue)
{
LogEntry(
$"AdditionalConfiguration '{key}' value {parsedValue} is below minimum {minValue}. Defaulting to {defaultValue}.",
LogLevels.Warning);
return defaultValue;
}
return parsedValue;
}
private static bool IsWorkflowWhatIfEnabled(cLiamProviderBase provider)
{
return IsAdditionalConfigurationEnabled(provider, "WhatIf");

View File

@@ -283,6 +283,7 @@
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="Folder Path" Grid.Row="0" Grid.Column="0" Margin="0,0,8,8" VerticalAlignment="Center"/>
@@ -299,7 +300,8 @@
<CheckBox x:Name="NtfsEnsureTraverseCheckBox" Grid.Row="4" Grid.Column="1" Margin="0,0,0,8" Content="Ensure traverse groups and ACLs on parent path"/>
<Button x:Name="ExecuteNtfsEnsureButton" Grid.Row="5" Grid.Column="1" Width="220" HorizontalAlignment="Right" Content="Ensure Missing Groups / ACLs" Click="ExecuteNtfsEnsureButton_Click"/>
<Button x:Name="ExecuteNtfsEnsureButton" Grid.Row="5" Grid.Column="1" Width="220" HorizontalAlignment="Right" Margin="0,0,0,8" Content="Ensure Missing Groups / ACLs" Click="ExecuteNtfsEnsureButton_Click"/>
<Button x:Name="ExecuteNtfsTraverseOnlyButton" Grid.Row="6" Grid.Column="1" Width="220" HorizontalAlignment="Right" Content="Ensure Traverse Only" Click="ExecuteNtfsTraverseOnlyButton_Click"/>
</Grid>
</TabItem>
</TabControl>

View File

@@ -584,7 +584,7 @@ namespace LiamWorkflowDiagnostics
if (runWhatIf && result.AutomaticEnsurePreview != null && result.AutomaticEnsurePreview.Count > 0)
{
AppendLog($"EnsureNtfsPermissionGroups wurde nur simuliert fuer {result.AutomaticEnsurePreview.Count} Ordner. Details stehen im Result-JSON.", LogLevels.Warning);
AppendLog($"Automatisches NTFS-Ensure wurde nur simuliert fuer {result.AutomaticEnsurePreview.Count} DataAreas. Details stehen im Result-JSON.", LogLevels.Warning);
}
AppendLog($"DataAreas erhalten: {result.DataAreas.Count}");
@@ -617,35 +617,21 @@ namespace LiamWorkflowDiagnostics
await ExecuteProviderActionAsync("NTFS Folder Create", async () =>
{
var result = await Task.Run(() => LiamWorkflowRuntime.CreateDataAreaAsync(
provider,
folderPath,
parentPath,
null,
ownerSids,
readerSids,
writerSids));
var result = await RunWithWorkflowWhatIfAsync(provider, () => Task.Run(() => LiamWorkflowRuntime.CreateDataAreaAsync(
provider,
folderPath,
parentPath,
null,
ownerSids,
readerSids,
writerSids)));
return new
{
result.Success,
ResultToken = MapResultToken(result.ResultToken)
};
}, () =>
{
return CreateWhatIfResult(
"NTFS Folder Create",
"Wuerde einen Ordner anlegen und fehlende Gruppen sowie ACLs sicherstellen. Es wurden keine Aenderungen ausgefuehrt.",
new
{
ProviderRootPath = provider.RootPath,
NewFolderPath = folderPath,
ParentFolderPath = parentPath,
OwnerSids = ownerSids,
ReaderSids = readerSids,
WriterSids = writerSids
});
});
}, actionHandlesWhatIf: true);
}
catch (Exception ex)
{
@@ -667,35 +653,21 @@ namespace LiamWorkflowDiagnostics
await ExecuteProviderActionAsync("NTFS Ensure Groups / ACLs", async () =>
{
var result = await Task.Run(() => LiamWorkflowRuntime.EnsureNtfsPermissionGroupsAsync(
provider,
folderPath,
null,
ownerSids,
readerSids,
writerSids,
ensureTraverse));
var result = await RunWithWorkflowWhatIfAsync(provider, () => Task.Run(() => LiamWorkflowRuntime.EnsureNtfsPermissionGroupsAsync(
provider,
folderPath,
null,
ownerSids,
readerSids,
writerSids,
ensureTraverse)));
return new
{
result.Success,
ResultToken = MapResultToken(result.ResultToken)
};
}, () =>
{
return CreateWhatIfResult(
"NTFS Ensure Groups / ACLs",
"Wuerde fehlende NTFS-Berechtigungsgruppen und ACLs additiv sicherstellen. Es wurden keine Aenderungen ausgefuehrt.",
new
{
ProviderRootPath = provider.RootPath,
FolderPath = folderPath,
OwnerSids = ownerSids,
ReaderSids = readerSids,
WriterSids = writerSids,
EnsureTraverseGroups = ensureTraverse
});
});
}, actionHandlesWhatIf: true);
}
catch (Exception ex)
{
@@ -704,6 +676,34 @@ namespace LiamWorkflowDiagnostics
}
}
private async void ExecuteNtfsTraverseOnlyButton_Click(object sender, RoutedEventArgs e)
{
try
{
var provider = EnsureInitializedProvider<cLiamProviderNtfs>("NTFS");
var folderPath = GetRequiredText(NtfsEnsureFolderPathTextBox.Text, "Folder Path");
await ExecuteProviderActionAsync("NTFS Ensure Traverse Only", async () =>
{
var token = await Task.Run(() => provider.EnsureTraverseGroupsAsync(
folderPath,
null,
IsWhatIfEnabled));
return new
{
Success = token != null && token.resultErrorId == 0,
ResultToken = MapResultToken(token)
};
}, actionHandlesWhatIf: true);
}
catch (Exception ex)
{
AppendLog($"NTFS Ensure Traverse Only fehlgeschlagen: {ex.Message}", LogLevels.Error);
MessageBox.Show(this, ex.ToString(), "NTFS Ensure Traverse Only", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private async void ExecuteAdCreateButton_Click(object sender, RoutedEventArgs e)
{
try
@@ -950,7 +950,7 @@ namespace LiamWorkflowDiagnostics
}
}
private async Task ExecuteProviderActionAsync(string actionName, Func<Task<object>> action, Func<object> whatIfAction = null)
private async Task ExecuteProviderActionAsync(string actionName, Func<Task<object>> action, Func<object> whatIfAction = null, bool actionHandlesWhatIf = false)
{
if (action == null)
throw new ArgumentNullException(nameof(action));
@@ -959,11 +959,11 @@ namespace LiamWorkflowDiagnostics
try
{
SaveSettings();
var runInWhatIfMode = IsWhatIfEnabled && whatIfAction != null;
var runInWhatIfMode = IsWhatIfEnabled && (whatIfAction != null || actionHandlesWhatIf);
AppendLog(runInWhatIfMode
? $"{actionName} im WhatIf-Modus gestartet. Schreibende Aenderungen werden nur simuliert."
: $"{actionName} gestartet.");
var result = runInWhatIfMode
var result = runInWhatIfMode && whatIfAction != null
? await Task.Run(whatIfAction)
: await action();
ResultTextBox.Text = JsonConvert.SerializeObject(result, Formatting.Indented);
@@ -985,6 +985,39 @@ namespace LiamWorkflowDiagnostics
}
}
private async Task<TResult> RunWithWorkflowWhatIfAsync<TResult>(cLiamProviderBase provider, Func<Task<TResult>> action)
{
if (provider == null)
throw new ArgumentNullException(nameof(provider));
if (action == null)
throw new ArgumentNullException(nameof(action));
if (!IsWhatIfEnabled)
return await action();
var additionalConfiguration = provider.AdditionalConfiguration;
if (additionalConfiguration == null)
{
additionalConfiguration = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
provider.AdditionalConfiguration = additionalConfiguration;
}
var hadWhatIf = additionalConfiguration.TryGetValue("WhatIf", out var previousWhatIf);
additionalConfiguration["WhatIf"] = "1";
try
{
return await action();
}
finally
{
if (hadWhatIf)
additionalConfiguration["WhatIf"] = previousWhatIf;
else
additionalConfiguration.Remove("WhatIf");
}
}
private bool TryGetSuccessFlag(object instance, out bool success)
{
success = false;

257
README.md
View File

@@ -1,5 +1,190 @@
# LIAM
## Naming Conventions fuer NTFS-Gruppen
NTFS-Namenskonventionen bestehen aus `NamingTemplate`, `DescriptionTemplate` und `Wildcard`. Die Templates werden beim Erzeugen oder Sicherstellen von AD-Gruppen materialisiert. Der `Wildcard` ist eine regulaere Expression und wird beim Auslesen der ACLs verwendet, um vorhandene Gruppen wieder einer Rolle zuzuordnen.
Die wichtigsten Eingaben fuer die Ersetzung sind:
| Quelle | Beispiel |
| --- | --- |
| `RootPath` | `\\SRVWSM001.imagoverum.com\file_shares\share2` |
| Aktuelle DataArea | `\\SRVWSM001.imagoverum.com\file_shares\share2\test33` |
| `CustomTags` | `Filesystem_GroupGlobalTag=UG`, `Filesystem_GroupDomainLocalTag=FS`, `Filesystem_GroupOwnerTag=_OW` |
| Rolle / Scope | z. B. `Owner` und `DomainLocal` |
### Standard-Platzhalter
| Platzhalter | Wirkung | Beispielwert bei Root `\\server\file_shares\share2` und DataArea `...\share2\test33` |
| --- | --- | --- |
| `{{NAME}}` | Name des aktuellen Ordners oder Shares. | `test33` |
| `{{RELATIVEPATH}}` | Relativer Pfad der DataArea unterhalb des `RootPath`, mit Sanitizing und konfiguriertem Segmenttrenner. | `test33` |
| `{{RELATIVEPATH(0)}}` | Letztes relatives Pfadsegment. | `test33` |
| `{{RELATIVEPATH(1)}}` | Die letzten zwei relativen Pfadsegmente. Bei weniger Segmenten werden alle vorhandenen verwendet. | z. B. `team1_test33` |
| `{{ROOT_SERVER}}` | Server-/Namespace-Teil des UNC-RootPath. | `server` |
| `{{SERVER_NAME}}` | Server-/Namespace-Teil des aktuell verarbeiteten UNC-Pfads. | `server` |
| `{{DFS_NAMESPACE_NAME}}` | Name des DFS-Namespaces, wenn der aktuelle Pfad in einem DFS-Namespace liegt. | `File_Shares` |
| `{{SHARE_NAME}}` | Klassifizierter Share-Name: klassischer Share oder DFS-Link, in dem die aktuelle DataArea liegt. | `share2` |
| `{{ROOT_NAME}}` | Letztes Segment des RootPath. | `share2` |
| `{{ROOT_PATH}}` | Alle RootPath-Segmente ohne Server, mit Segmenttrenner verbunden. | `file_shares_share2` |
| `{{ROOT_PATH(1)}}` | Letztes RootPath-Segment. | `share2` |
| `{{ROOT_PATH(2)}}` | Letzte zwei RootPath-Segmente. | `file_shares_share2` |
| `{{ROOT_SEGMENT(0)}}` | RootPath-Segment per 0-basierter Position, ohne Server. | `file_shares` |
| `{{ROOT_SEGMENT(1)}}` | Zweites RootPath-Segment. | `share2` |
`{{RELATIVEPATH(n)}}` zaehlt von hinten: `0` ist das letzte Segment, `1` sind die letzten zwei Segmente, `2` die letzten drei Segmente usw. `{{ROOT_PATH(n)}}` verwendet dagegen die letzten `n` Segmente des RootPath.
`{{ROOT_*}}` bezieht sich immer auf den konfigurierten `RootPath`. `{{SERVER_NAME}}`, `{{DFS_NAMESPACE_NAME}}` und `{{SHARE_NAME}}` werden dagegen anhand der NTFS-Pfadklassifizierung des aktuell verarbeiteten Pfads ermittelt. Bei einem klassischen Share ist `{{SHARE_NAME}}` der Share. Bei DFS ist `{{DFS_NAMESPACE_NAME}}` der Namespace und `{{SHARE_NAME}}` der DFS-Link.
Wenn die aktuelle DataArea dem `RootPath` selbst entspricht, ist `{{RELATIVEPATH}}` leer. `{{NAME}}` ist dann der Name des RootPath, also z. B. `share2` oder `LEI.01.test`.
### Rollen- und Scope-Platzhalter
Diese Platzhalter werden aus Rolle, Scope und `CustomTags` aufgebaut:
| Platzhalter | Wirkung | Typische Werte |
| --- | --- | --- |
| `{{PREFIX}}` | Gruppenprefix aus `Filesystem_GroupPrefixTag`, fallback auf `ADGroupPrefix`. | `ACL`, `FS` |
| `{{SCOPETAG}}` | Scope-Tag. Bei `Global` aus `Filesystem_GroupGlobalTag`, bei `DomainLocal` aus `Filesystem_GroupDomainLocalTag`. | `UG`, `FS`, `G`, `DL` |
| `{{GROUPTYPEPOSTFIX}}` | Rollen-Postfix. Owner/Write/Read/Traverse verwenden die jeweiligen `Filesystem_Group*Tag` Werte. | `_OW`, `_RW`, `_R`, `_T` |
| `{{_LOOP}}` | Optionaler Eindeutigkeitszaehler. Beim ersten Versuch leer, danach `_1`, `_2`, usw. | leer, `_1`, `_2` |
| `{{LOOP}}` | Variante ohne Trennzeichen. Beim ersten Versuch leer, danach `1`, `2`, usw. | leer, `1`, `2` |
Der Loop-Platzhalter ist flexibel: Alles innerhalb der geschweiften Klammern, das `LOOP` enthaelt, wird als Loop-Tag behandelt. `{{-LOOP}}` wird beim ersten Versuch leer und danach z. B. `-1`.
### CustomTags
Alle Eintraege aus `CustomTags` koennen ebenfalls direkt verwendet werden. Die Schreibweise muss exakt zum Key passen.
Beispiele:
```text
{{ADGroupPrefix}}_{{RELATIVEPATH}}{{GROUPTYPEPOSTFIX}}
{{Filesystem_GroupGlobalTag}}_{{ROOT_NAME}}_{{NAME}}{{GROUPTYPEPOSTFIX}}
{{Filesystem_GroupDomainLocalTag}}_{{ROOT_PATH(1)}}_{{RELATIVEPATH}}{{GROUPTYPEPOSTFIX}}
```
### Beispiele fuer NamingTemplate
Share-basierte Gruppen nur mit Share-Name:
```text
{{SCOPETAG}}_{{NAME}}{{_LOOP}}{{GROUPTYPEPOSTFIX}}
```
Bei `RootPath=\\intra.brkr.corp\File_Shares\LEI.01.test`, Scope `DomainLocal`, `Filesystem_GroupDomainLocalTag=FS` und Rolle `Owner` entsteht:
```text
FS_LEI.01.TEST_OW
```
Gruppen fuer Unterordner relativ zum Root:
```text
{{SCOPETAG}}_{{RELATIVEPATH}}{{_LOOP}}{{GROUPTYPEPOSTFIX}}
```
Bei `RootPath=\\SRVWSM001.imagoverum.com\file_shares\share2` und DataArea `...\share2\test33` entsteht fuer Owner:
```text
FS_TEST33_OW
```
Root-Share und Unterordner kombinieren:
```text
{{SCOPETAG}}_{{ROOT_NAME}}.{{RELATIVEPATH}}{{_LOOP}}{{GROUPTYPEPOSTFIX}}
```
Bei Root `share2` und Ordner `test33` entsteht:
```text
FS_SHARE2.TEST33_OW
```
Nur die letzten zwei relativen Pfadsegmente verwenden:
```text
{{SCOPETAG}}_{{RELATIVEPATH(1)}}{{_LOOP}}{{GROUPTYPEPOSTFIX}}
```
Bei `...\share2\team1\test33` entsteht:
```text
FS_TEAM1_TEST33_OW
```
Namespace-/Share-Bestandteile aus dem RootPath verwenden:
```text
{{SCOPETAG}}_{{ROOT_SEGMENT(0)}}_{{ROOT_SEGMENT(1)}}_{{NAME}}{{GROUPTYPEPOSTFIX}}
```
Bei Root `\\SRVWSM001.imagoverum.com\file_shares\share2` und Name `test33` entsteht:
```text
FS_FILE_SHARES_SHARE2_TEST33_OW
```
### Beispiele fuer AGP und AGDLP
Bei AGP liegen die Global-Gruppen direkt auf der ACL. Eine typische Konvention ist:
```text
NamingTemplate: {{SCOPETAG}}_{{NAME}}{{_LOOP}}{{GROUPTYPEPOSTFIX}}
Scope: Global
```
Bei AGDLP liegen die DomainLocal-Gruppen auf der ACL, die Global-Gruppen werden darin verschachtelt. Typisch ist:
```text
Global NamingTemplate: {{SCOPETAG}}_{{NAME}}{{_LOOP}}{{GROUPTYPEPOSTFIX}}
DomainLocal NamingTemplate: {{SCOPETAG}}_{{NAME}}{{_LOOP}}{{GROUPTYPEPOSTFIX}}
```
Mit `Filesystem_GroupGlobalTag=UG` und `Filesystem_GroupDomainLocalTag=FS` entstehen z. B.:
```text
UG_LEI.01.TEST_OW
FS_LEI.01.TEST_OW
```
In diesem Fall gehoert bei AGDLP die `FS_...` DomainLocal-Gruppe auf die NTFS-ACL; `UG_...` wird Mitglied der `FS_...` Gruppe.
### Wildcard-Regeln
`Wildcard` ist eine regulaere Expression. Sie muss nach Platzhalterersetzung zum erzeugten oder vorhandenen `sAMAccountName` passen.
Passendes Beispiel:
```text
^{{SCOPETAG}}_[a-zA-ZoeaeueOeAeUess0-9_\-\.]*{{GROUPTYPEPOSTFIX}}$
```
Mit `SCOPETAG=FS` und `GROUPTYPEPOSTFIX=_OW` matcht das:
```text
FS_LEI.01.TEST_OW
```
Nicht passend ist ein fuehrendes Leerzeichen:
```text
^ {{SCOPETAG}}_[a-zA-ZoeaeueOeAeUess0-9_\-\.]*{{GROUPTYPEPOSTFIX}}$
```
Dieser Regex erwartet nach `^` ein Leerzeichen und matcht deshalb nicht auf `FS_LEI.01.TEST_OW`.
Wenn eine Traverse-Naming-Convention keine Gruppe erzeugen soll, kann das `NamingTemplate` leer bleiben. Fuer Owner/Write/Read sollte der `Wildcard` nicht leer sein. Ein leerer Regex ist keine sinnvolle Matching-Regel, weil er technisch sehr breit matchen kann.
### Sanitizing, Laenge und Grossschreibung
Pfadbasierte Platzhalter wie `{{NAME}}`, `{{RELATIVEPATH}}`, `{{ROOT_PATH}}` und `{{ROOT_SEGMENT(n)}}` werden fuer AD-Gruppennamen bereinigt. Standardmaessig werden Steuerzeichen und die fuer AD-Gruppennamen bzw. `sAMAccountName` problematischen Zeichen `/`, `\`, `[`, `]`, `:`, `;`, `|`, `=`, `,`, `+`, `*`, `?`, `<` und `>` mit `_` ersetzt. Das Ersatzzeichen kann ueber `NtfsGroupNameSanitizeReplacement` geaendert oder entfernt werden. Zeichen wie Leerzeichen, Bindestrich, Punkt, Unterstrich, Klammern, `@` und Quotes bleiben erhalten.
Generierte AD-Gruppennamen werden standardmaessig in Grossbuchstaben geschrieben. Mit `PreserveNtfsAdGroupNameCase=1` bleibt die Schreibweise erhalten.
AD-Gruppennamen werden auf die maximale Laenge von 64 Zeichen begrenzt. Wenn ein Template mit `{{RELATIVEPATH}}` oder `{{NAME}}` zu lang wird, werden Pfadbestandteile gekuerzt. Fuer Loop-Platzhalter werden bis zu drei Ziffern reserviert.
## AdditionalParameters / AdditionalConfiguration
Provider-spezifische Zusatzparameter werden in Matrix42 am DataArea-Collector ueber das Fragment `C4IT_GCC_DataArea_Collector_AdditionalAttributes` gepflegt. Pro Parameter wird ein Eintrag mit `Name` und `Value` angelegt.
@@ -25,13 +210,18 @@ Im Diagnose-JSON erscheinen diese Werte unter `AdditionalConfiguration`. Paramet
| --- | --- | --- |
| `EnsureNtfsPermissionGroups` | `true`, `1`, `yes` | Stellt beim Auslesen von NTFS-Ordnern automatisch fehlende AD-Berechtigungsgruppen und NTFS-ACLs sicher. |
| `EnsureNtfsPermissionGroupsForShares` | `true`, `1`, `yes` | Erweitert das automatische Ensure auf Share-DataAreas. Ohne diesen Parameter wird das automatische Ensure nur fuer Ordner ausgefuehrt. |
| `EnsureNtfsTraverseGroups` | `true`, `1`, `yes` | Aktiviert die automatische Traverse-Gruppenverarbeitung als eigene Phase. Traverse-Gruppen und Traverse-ACLs werden sichergestellt; vorhandene LIAM-Owner/Write/Read-Globalgruppen werden in die naechste Traverse-Gruppe aufgenommen. |
| `AllowManualNtfsPermissionEnsureForShares` | `true`, `1`, `yes` | Erlaubt die manuelle Ensure-Aktivitaet auch fuer Share-DataAreas. |
| `NtfsIncludePaths` | Pfadliste, getrennt mit `;`, &#124; oder Zeilenumbruechen | Beschraenkt die NTFS-Verarbeitung auf passende Pfade. Unterstuetzt relative Pfade unterhalb des RootPath, absolute UNC-Pfade und einfache Wildcards mit `*`. Wenn der Parameter leer ist, sind alle Pfade eingeschlossen. |
| `NtfsExcludePaths` | Pfadliste, getrennt mit `;`, &#124; oder Zeilenumbruechen | Schliesst passende Pfade von der NTFS-Verarbeitung aus. Excludes gewinnen gegen Includes. Unterstuetzt relative Pfade, absolute UNC-Pfade und einfache Wildcards mit `*`. |
| `NtfsTraverseBoundaryPath` | Relativer oder absoluter Pfad | Setzt eine Traverse-Grenze fuer die Traverse-Gruppenverarbeitung. Damit koennen Traverse-Gruppen ueber den eigentlichen Einsprung hinaus bis zu einer definierten Ebene sichergestellt werden. |
| `NtfsTraverseBoundaryPath` | Relativer oder absoluter Pfad | Setzt eine Traverse-Grenze fuer die Traverse-Gruppenverarbeitung. Damit koennen Traverse-Gruppen ueber den eigentlichen Einsprung hinaus bis zu einer definierten Ebene sichergestellt werden. Der Boundary-Pfad selbst darf ebenfalls als Traverse-Ziel verarbeitet werden. Wenn der Parameter leer ist, verwendet LIAM den `RootPath` als Traverse-Grenze. |
| `NtfsPermissionGroupsMinLevel` / `NtfsPermissionGroupsMaxLevel` | Ganzzahl, z. B. `0`, `1`, `2` | Optionaler Levelbereich fuer Owner/Write/Read-Ensure. Ebene `0` ist der `RootPath`; Unterordner sind `1`, `2`, `3`, ... Wenn beide Parameter leer sind, bleibt das bisherige Verhalten unveraendert. |
| `NtfsTraverseGroupsMinLevel` / `NtfsTraverseGroupsMaxLevel` | Ganzzahl, z. B. `-1`, `0`, `2` | Optionaler Levelbereich fuer Traverse-Ensure. Ebene `0` ist der `RootPath`; Parent-Pfade oberhalb des `RootPath` sind `-1`, `-2`, ... Wenn `NtfsTraverseGroupsMaxLevel` nicht gesetzt ist, wird fuer Traverse implizit `MaxDepth - 1` verwendet. |
| `NtfsGroupNameSanitizeReplacement` | Zeichenfolge, z. B. `_`, `.`, leer, `none`, `remove`, `<empty>` | Legt fest, womit ungueltige Zeichen in dynamischen gruppennamenrelevanten Pfadbestandteilen ersetzt werden. Standard ist `_`. Mit leerem Wert oder `none`/`remove`/`<empty>` werden ungueltige Zeichen entfernt und Pfadsegmente ohne Trennzeichen verbunden. |
| `PreserveNtfsAdGroupNameCase` | `true`, `1`, `yes` | Unterbindet das automatische Uppercase fuer generierte NTFS-AD-Gruppennamen. Ohne diesen Parameter werden generierte Gruppennamen wie bisher in Grossbuchstaben erzeugt. |
| `ForceStrictAdGroupNames` | `true`, `1`, `yes` | Erzwingt strikte AD-Gruppennamen. Wildcard-/ACL-basierte Wiederverwendung abweichender bestehender Gruppen wird damit eingeschraenkt; es werden nur exakt passende konfigurierte oder generierte Namen verwendet. |
| `NtfsAdGroupReuseMode` | `Safe`, `Name` | Steuert die Wiederverwendung vorhandener AD-Gruppen im automatischen Ensure. Standard ist `Safe`: vorhandene Gruppen werden nur wiederverwendet, wenn sie bereits passend auf der Ordner-ACL liegen oder einen passenden LIAM-Marker im AD-Attribut `info` besitzen. `Name` erlaubt die Legacy-Wiederverwendung nur anhand des Namens und wird als unsicherer Modus geloggt. |
| `NtfsAdGroupMarkerBackfill` | `true`, `1`, `yes` | Ergaenzt bei sicher wiederverwendeten, ACL-verknuepften Gruppen einen LIAM-Marker im AD-Attribut `info`, sofern das Attribut les- und schreibbar ist. |
| `NtfsAdDomainControllers` | Kommagetrennte DC-Liste, z. B. `dc01.contoso.local,dc02.contoso.local` | Pinnt NTFS-AD-Operationen auf einen Domain Controller. Der erste erreichbare DC wird verwendet. Wenn kein Eintrag erreichbar ist oder der Parameter fehlt, wird der PDC Emulator verwendet; danach faellt der Code auf die normale Domain-Locator-Logik zurueck. Der ausgewaehlte DC wird im Debug-Log protokolliert. |
Beispiele:
@@ -39,14 +229,79 @@ Beispiele:
```text
EnsureNtfsPermissionGroups=1
EnsureNtfsPermissionGroupsForShares=1
EnsureNtfsTraverseGroups=1
NtfsIncludePaths=Finance\*;HR\Reports
NtfsExcludePaths=*\_archive\*
NtfsTraverseBoundaryPath=\\fileserver\file_shares
NtfsPermissionGroupsMinLevel=1
NtfsPermissionGroupsMaxLevel=2
NtfsTraverseGroupsMinLevel=-1
NtfsTraverseGroupsMaxLevel=2
NtfsGroupNameSanitizeReplacement=.
PreserveNtfsAdGroupNameCase=1
NtfsAdDomainControllers=dc01.contoso.local,dc02.contoso.local
```
#### Automatisches NTFS-Ensure
Beim Auslesen der NTFS-DataAreas kann LIAM Gruppen und ACLs automatisch sicherstellen. Die automatische Verarbeitung ist bewusst fehlertolerant: Wenn ein einzelner DataArea-Pfad wegen Whitelist, Blacklist, fehlendem Verzeichnis oder Providerfehler nicht verarbeitet werden kann, wird dieser Pfad im Log als Warning uebersprungen und die naechste DataArea weiter verarbeitet.
Die Schalter koennen kombiniert werden:
| Konfiguration | Verhalten |
| --- | --- |
| `EnsureNtfsPermissionGroups=1` | Erstellt oder reused Owner/Write/Read-Gruppen und setzt die passenden NTFS-ACLs fuer Ordner. Das bestehende Verhalten mit eingebetteter Traverse-Verarbeitung bleibt aktiv, solange `EnsureNtfsTraverseGroups` nicht gesetzt ist. |
| `EnsureNtfsPermissionGroups=1` und `EnsureNtfsPermissionGroupsForShares=1` | Wie oben, aber auch fuer Share-DataAreas. |
| `EnsureNtfsTraverseGroups=1` | Fuehrt eine separate Traverse-only-Phase fuer Ordner und Share-DataAreas aus. Es werden Traverse-Gruppen und Traverse-ACLs sichergestellt, aber keine Owner/Write/Read-Gruppen erzeugt. |
| `EnsureNtfsPermissionGroups=1` und `EnsureNtfsTraverseGroups=1` | Fuehrt zuerst Owner/Write/Read-Ensure aus und danach die separate Traverse-only-Phase. Dadurch koennen neu angelegte Owner/Write/Read-Globalgruppen direkt in die Traverse-Gruppen aufgenommen werden. Die eingebettete Traverse-Verarbeitung im Permission-Ensure wird in diesem Modus deaktiviert, damit Traverse nicht doppelt laeuft. |
Traverse-only sucht die erwarteten LIAM-Owner/Write/Read-Gruppen anhand der Naming-Conventions und bestehenden ACLs. Gefunden werden nur LIAM-Gruppen, die zur konfigurierten Wildcard passen; beliebige Legacy-ACL-Gruppen werden nicht automatisch als Traverse-Mitglieder uebernommen. Fehlende Owner/Write/Read-Gruppen werden in Traverse-only nicht angelegt, sondern als Warning protokolliert.
Fuer AGP werden die Global-Gruppen auf der ACL und als Traverse-Mitglieder verwendet. Fuer AGDLP liegen weiterhin die DomainLocal-Gruppen auf der NTFS-ACL; die Traverse-Mitgliedschaft verwendet die zugehoerigen Global-Gruppen.
Traverse-Gruppen werden lazy erzeugt: LIAM legt eine Traverse-Gruppe fuer einen Pfad nur dann an, wenn darunter mindestens eine LIAM-Owner/Write/Read-Gruppe oder eine darunterliegende Traverse-Gruppe aufgenommen werden soll. Bestehende Traverse-Gruppen werden weiterverwendet, leere neue Traverse-Gruppen ohne aktuelle Mitglieder werden aber nicht vorsorglich erzeugt.
#### Traverse-Grenze
`NtfsTraverseBoundaryPath` begrenzt, bis zu welcher Ebene Traverse-Gruppen und Traverse-ACLs sichergestellt werden. Wenn der Parameter nicht gesetzt ist, verwendet LIAM den `RootPath` als Traverse-Grenze. Damit darf die Traverse-Verarbeitung standardmaessig auch Parent-Pfade oberhalb des Include-Filters erreichen, solange der Pfad existiert und nicht durch `NtfsExcludePaths` ausgeschlossen ist.
Beispiel:
```text
RootPath=\\intra.brkr.corp\File_Shares\BRE.01.LEW
NtfsIncludePaths=\\intra.brkr.corp\File_Shares\BRE.01.LEW\Projekte\*
EnsureNtfsTraverseGroups=1
```
In diesem Beispiel ist keine explizite Boundary noetig, weil `RootPath` automatisch als Traverse-Grenze verwendet wird. Dadurch koennen Traverse-Gruppen fuer `...\BRE.01.LEW`, `...\Projekte` und die Ordner darunter sichergestellt werden, sobald darunter Berechtigungsgruppen oder weitere Traverse-Gruppen aufzunehmen sind. Ein abweichender `NtfsTraverseBoundaryPath` ist nur noetig, wenn die Traverse-Verarbeitung bewusst oberhalb oder unterhalb des `RootPath` enden soll.
#### Levelbereiche fuer Ensure und Ordneranlage
Die optionalen Levelbereiche schraenken ein, auf welchen Ebenen Gruppen und ACLs automatisch sichergestellt werden. Wenn keine Permission-Level-Parameter gesetzt sind, bleibt Owner/Write/Read unveraendert. Fuer Traverse wird ohne explizites `NtfsTraverseGroupsMaxLevel` automatisch `MaxDepth - 1` als maximale Verarbeitungsebene verwendet.
Ebene `0` ist immer der konfigurierte `RootPath`. Unterordner unterhalb des `RootPath` haben positive Level (`1`, `2`, `3`, ...). Parent-Pfade oberhalb des `RootPath` haben negative Level (`-1`, `-2`, ...). Negative Level sind vor allem fuer Traverse relevant, wenn `NtfsTraverseBoundaryPath` oberhalb des `RootPath` liegt.
Beispiel:
```text
RootPath=\\server\share\Bereich
NtfsTraverseBoundaryPath=\\server\share
NtfsPermissionGroupsMinLevel=1
NtfsPermissionGroupsMaxLevel=2
NtfsTraverseGroupsMinLevel=-1
NtfsTraverseGroupsMaxLevel=2
```
Damit werden Owner/Write/Read-Gruppen nur fuer `\\server\share\Bereich\...` auf Ebene `1` und `2` erzeugt. Traverse-Gruppen duerfen zusaetzlich bis zum Share auf Ebene `-1` erzeugt werden. `NtfsTraverseBoundaryPath` bleibt die harte Grenze fuer Traverse; die Min-/Max-Level legen nur fest, welche Ebenen innerhalb dieses Korridors verarbeitet werden.
Die Permission-Level gelten auch fuer die explizite NTFS-Ordneranlage ueber `CreateDataArea`. Wenn der Zielordner ausserhalb des Permission-Levelbereichs liegt oder die Permission-Levelkonfiguration ungueltig ist, wird die Ordneranlage vor AD-/ACL-Aenderungen abgebrochen. Traverse-Level blockieren die Ordneranlage nicht; sie schraenken nur die Traverse-Gruppen und Traverse-ACLs ein.
Wenn nur ein Min- oder Max-Wert gesetzt ist, ist die andere Seite offen. Ausnahme: Fuer Traverse ist ein fehlender Max-Wert durch `MaxDepth - 1` begrenzt. Ungueltige Werte oder ein Min-Wert groesser als der Max-Wert fuehren dazu, dass die betroffene Ensure-Phase mit einer Warning uebersprungen wird; bei `CreateDataArea` fuehrt eine ungueltige Permission-Levelkonfiguration zum Abbruch vor der Anlage.
#### WhatIf / Preview
Wenn `WhatIf=1` gesetzt ist, fuehrt LIAM die automatische NTFS-Verarbeitung als Preview aus. Fuer Permission-Ensure und Traverse-Ensure werden geplante Gruppen, wiederverwendete Gruppen, geplante ACLs, bestehende ACLs, Traverse-Gruppen und Warnings im Ergebnis und im Debug-Log ausgegeben. Es werden keine AD-Gruppen, Gruppenmitgliedschaften oder ACLs dauerhaft geaendert.
### Active Directory
| Parameter | Werte | Wirkung |

View File

@@ -7,5 +7,5 @@ using System.Runtime.InteropServices;
[assembly: AssemblyCopyright("Copyright © 2026, Consulting4IT GmbH, Germany")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyInformationalVersion("3.3.1")]
[assembly: AssemblyVersion("3.3.1")]
[assembly: AssemblyInformationalVersion("3.3.5")]
[assembly: AssemblyVersion("3.3.5")]

View File

@@ -0,0 +1,695 @@
<#
.SYNOPSIS
Creates a local LIAM NTFS demo fixture with folders and ACL groups from a production log.
.DESCRIPTION
This script recreates the BRE.01.LEW/Projekte folder and ACL-group fixture from
the diagnostic log in the local imagoverum.com demo environment:
- local folder: C:\file_shares\share2
- optional SMB share: \\<local-server>\file_shares\share2
- group strategy: Ntfs_AGDLP
- group tags: FS for global groups, UG for domain-local groups
- access tags: _O, _W, _R, _T
By default the script creates the exact folders visible in the log and creates
the logged ACL groups in the configured OU, then grants them Read permissions
on the matching local folders. Expected LIAM groups can be created separately
with -CreateExpectedLiamGroups.
Run with -WhatIf first. The ActiveDirectory module is only required when
AD group creation and ACL assignment are enabled.
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[string]$ShareRootPath = 'C:\file_shares',
[string]$RootFolderName = 'share2',
[string]$SmbShareName = 'file_shares',
[string]$GroupOuDN = 'OU=AGP,OU=LIAM,OU=Global,DC=imagoverum,DC=com',
[string[]]$AdditionalFolderRelativePaths = @(),
[string]$GroupNameSanitizeReplacement = '_',
[switch]$PreserveAdGroupNameCase,
[switch]$CreateExpectedLiamGroups,
[switch]$SkipLogAclFixture,
[switch]$SkipAdGroups,
[switch]$SkipNtfsAcl,
[switch]$SkipSmbShare
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
if ([System.Environment]::OSVersion.Platform -ne [System.PlatformID]::Win32NT) {
throw 'Dieses Script kann nur unter Windows ausgefuehrt werden.'
}
$rootPath = Join-Path -Path $ShareRootPath -ChildPath $RootFolderName
$readRights = [System.Security.AccessControl.FileSystemRights]0x200A9
$writeRights = [System.Security.AccessControl.FileSystemRights]0x301BF
$ownerRights = [System.Security.AccessControl.FileSystemRights]0x1F01FF
$logFolderRelativePaths = @(
'Arbeitsgruppen',
'Quartalsplanungen',
'Projekte',
'Projekte\__Templ',
'Projekte\_BAMS_AD Projekte',
'Projekte\__Admin Only',
'Projekte\_LSMS_AD Projekte',
'Projekte\_SW Projekte',
'Projekte\_AD Projekte',
'Projekte\_Templ_BMID',
'Projekte\_HW Projekte',
'Projekte\_IVD_Projekte',
'ProjekteAbgeschlossen',
'RESTORE',
'Design Control Harmonization',
'PMO'
)
$logAclFixtures = @(
@{
RelativePath = ''
Groups = @(
'UG_BRE.BDAL.DE.Users',
'DALDE-gArchimedesLEW_ALL_RO',
'DALDE-gArchimedesLEW_Admins',
'DALDE-gArchimedesLEW_Projekte_Browse',
'UG_BRE.01',
'FS_BRE.01.Archimedes_LEW-RW',
'AG_ETT.01.File Server Administrators_Restricted'
)
},
@{
RelativePath = 'Projekte'
Groups = @(
'UG_BRE.BDAL.DE.Users',
'DALDE-gArchimedesLEW_AccessGroup_PAW',
'DALDE-gArchimedesLEW_AccessGroup_SVC',
'DALDE-gArchimedesLEW_AccessGroup_TER',
'DALDE-gArchimedesLEW_ALL_RO',
'DALDE-gArchimedesLEW_Admins',
'DALDE-gArchimedesLEW_AccessGroup_PRM',
'DALDE-gArchimedesLEW_AccessGroup_QS',
'DALDE-gArchimedesLEW_AccessGroup_WB',
'DALDE-gArchimedesLEW_RO_Leipzig',
'DALDE-gArchimedesLEW_AccessGroup_BOM_RO',
'DALDE-gArchimedesLEW_AccessGroup_EEL',
'DALDE-gArchimedesLEW_AccessGroup_TPS',
'DALDE-gArchimedesLEW_RW',
'DALDE-gArchimedesLEW_AccessGroup_KST',
'DALDE-gArchimedesLEW_AccessGroup_ESW_SW_Only',
'DALDE-_Autocad',
'DALDE-gArchimedesLEW_AccessGroup_APP',
'DALDE-gArchimedesLEW_Projekte_RO',
'DALDE-gArchimedesLEW_AccessGroup_ESW_SW_HW',
'DALDE-gArchimedesLEW_AccessGroup_EPT',
'DALDE-gArchimedesLEW_Projekte_Browse',
'UG_BRE.01',
'AG_ETT.01.File Server Administrators_Restricted'
)
},
@{
RelativePath = 'Projekte\__Templ'
Groups = @(
'UG_BRE.BDAL.DE.Users',
'DALDE-gArchimedesLEW_Projekte_Browse',
'UG_BRE.01',
'FG_BRE.01.ArchimedesLEW_AccessGroup_APP',
'FG_BRE.01.ArchimedesLEW_AccessGroup_GL',
'FG_BRE.01.ArchimedesLEW_AccessGroup_EEl',
'FG_BRE.01.ArchimedesLEW_AccessGroup_EpT',
'FG_BRE.01.ArchimedesLEW_AccessGroup_ESw',
'FG_BRE.01.ArchimedesLEW_AccessGroup_G&L',
'FG_BRE.01.ArchimedesLEW_AccessGroup_KSt',
'FG_BRE.01.ArchimedesLEW_AccessGroup_PRM',
'FG_BRE.01.ArchimedesLEW_AccessGroup_QS',
'FG_BRE.01.ArchimedesLEW_AccessGroup_Svc',
'FG_BRE.01.ArchimedesLEW_AccessGroup_TeR',
'FG_BRE.01.ArchimedesLEW_AccessGroup_TPS',
'FG_BRE.01.ArchimedesLEW_AccessGroup_RA_LSMS',
'FG_BRE.01.ArchimedesLEW_AccessGroup_Proc',
'FG_BRE.01.ArchimedesLEW_AccessGroup_BD',
'FG_BRE.01.ArchimedesLEW_AccessGroup_Mark',
'FG_BRE.01.ArchimedesLEW_AccessGroup_Fin',
'AG_ETT.01.File Server Administrators_Restricted'
)
},
@{
RelativePath = 'Projekte\_BAMS_AD Projekte'
Groups = @(
'AG_ETT.01.File Server Administrators_Restricted'
)
},
@{
RelativePath = 'Projekte\__Admin Only'
Groups = @(
'DALDE-gArchimedesLEW_Admins',
'AG_ETT.01.File Server Administrators_Restricted'
)
},
@{
RelativePath = 'Projekte\_LSMS_AD Projekte'
Groups = @(
'AG_ETT.01.File Server Administrators_Restricted'
)
},
@{
RelativePath = 'Projekte\_SW Projekte'
Groups = @(
'DALDE-gArchimedesLEW_AccessGroup_PAW',
'DALDE-gArchimedesLEW_AccessGroup_SVC',
'DALDE-gArchimedesLEW_SWProjekte_RW',
'DALDE-gArchimedesLEW_AccessGroup_TER',
'DALDE-gArchimedesLEW_ManagerSW_RW',
'DALDE-gArchimedesLEW_Admins',
'DALDE-gArchimedesLEW_AccessGroup_PRM',
'DALDE-gArchimedesLEW_AccessGroup_QS',
'DALDE-gArchimedesLEW_AccessGroup_WB',
'DALDE-gArchimedesLEW_SWProjekte_RO',
'DALDE-gArchimedesLEW_AccessGroup_BOM_RO',
'DALDE-gArchimedesLEW_AccessGroup_EEL',
'DALDE-gArchimedesLEW_AccessGroup_TPS',
'DALDE-gArchimedesLEW_AccessGroup_KST',
'DALDE-gArchimedesLEW_AccessGroup_ESW_SW_Only',
'DALDE-gArchimedesLEW_AccessGroup_APP',
'DALDE-gArchimedesLEW_AccessGroup_ESW_SW_HW',
'DALDE-gArchimedesLEW_AccessGroup_EPT',
'DALDE-gArchimedesLEW_Projekte_Browse',
'AG_ETT.01.File Server Administrators_Restricted'
)
},
@{
RelativePath = 'Projekte\_AD Projekte'
Groups = @(
'UG_BRE.BDAL.DE.Users',
'DALDE-gArchimedesLEW_AccessGroup_PAW',
'DALDE-gArchimedesLEW_AccessGroup_SVC',
'DALDE-gArchimedesLEW_AccessGroup_TER',
'DALDE-gArchimedesLEW_ALL_RO',
'DALDE-gArchimedesLEW_AccessGroup_PRM',
'DALDE-gArchimedesLEW_AccessGroup_QS',
'DALDE-gArchimedesLEW_AccessGroup_WB',
'DALDE-gArchimedesLEW_RO_Leipzig',
'DALDE-gArchimedesLEW_AccessGroup_BOM_RO',
'DALDE-gArchimedesLEW_AccessGroup_EEL',
'DALDE-gArchimedesLEW_AccessGroup_TPS',
'DALDE-gArchimedesLEW_RW',
'DALDE-gArchimedesLEW_AccessGroup_KST',
'DALDE-gArchimedesLEW_RO',
'DALDE-gArchimedesLEW_AccessGroup_ESW_SW_Only',
'DALDE-_Autocad',
'DALDE-gArchimedesLEW_AccessGroup_APP',
'DALDE-gArchimedesLEW_Projekte_RO',
'DALDE-gArchimedesLEW_AccessGroup_ESW_SW_HW',
'DALDE-gArchimedesLEW_AccessGroup_EPT',
'DALDE-gArchimedesLEW_Projekte_Browse',
'UG_BRE.01',
'FS_BRE.01.Archimedes_LEW_Projekte_AD Projekte RW',
'FS_BRE.01.Archimedes_LEW_Projekte_AD Projekte RO',
'AG_ETT.01.File Server Administrators_Restricted'
)
},
@{
RelativePath = 'Projekte\_Templ_BMID'
Groups = @(
'AG_ETT.01.File Server Administrators_Restricted'
)
},
@{
RelativePath = 'Projekte\_HW Projekte'
Groups = @(
'DALDE-gArchimedesLEW_AccessGroup_PAW',
'DALDE-gArchimedesLEW_AccessGroup_SVC',
'DALDE-gArchimedesLEW_AccessGroup_FSt',
'DALDE-gArchimedesLEW_ManagerHW_RW',
'DALDE-gArchimedesLEW_AccessGroup_GL',
'DALDE-gArchimedesLEW_AccessGroup_TER',
'DALDE-gArchimedesLEW_AccessGroup_EBIO',
'DALDE-gArchimedesLEW_AccessGroup_PRM',
'DALDE-gArchimedesLEW_HWProjekt_RO',
'DALDE-gArchimedesLEW_AccessGroup_QS',
'DALDE-gArchimedesLEW_AccessGroup_WB',
'DALDE-gArchimedesLEW_AccessGroup_BOM_RO',
'DALDE-gArchimedesLEW_HWProjekt_RFAP1_RO',
'DALDE-gArchimedesLEW_AccessGroup_EEL',
'DALDE-gArchimedesLEW_AccessGroup_TPS',
'DALDE-gArchimedesLEW_AccessGroup_KST',
'DALDE-gArchimedesLEW_AccessGroup_APP',
'DALDE-gArchimedesLEW_AccessGroup_ESW_SW_HW',
'DALDE-gArchimedesLEW_AccessGroup_EPT',
'DALDE-gArchimedesLEW_Projekte_Browse',
'UG_BRE.01.ArchimedesLEW_Projekte_HW Projekte RW',
'AG_ETT.01.File Server Administrators_Restricted'
)
},
@{
RelativePath = 'Projekte\_IVD_Projekte'
Groups = @(
'DALDE-gArchimedesLEW_IVD_Projekte_RW',
'FS_BRE.01.ArchimedesLEW_Projekte_IVD Projekte RW',
'FS_BRE.01.ArchimedesLEW_Projekte_IVD Projekte RO',
'AG_ETT.01.File Server Administrators_Restricted'
)
}
)
function ConvertTo-LiamSafeNameSegment {
param(
[Parameter(Mandatory = $true)]
[string]$Value,
[Parameter(Mandatory = $true)]
[string]$Replacement,
[Parameter(Mandatory = $true)]
[bool]$PreserveCase
)
$safeReplacement = if ($null -eq $Replacement) { '_' } else { $Replacement.Trim() }
if ($safeReplacement -in @('<empty>', 'empty', 'none', 'remove')) {
$safeReplacement = ''
}
if ([string]::IsNullOrWhiteSpace($safeReplacement) -and $Replacement -notin @('<empty>', 'empty', 'none', 'remove')) {
$safeReplacement = '_'
}
$safeValue = [regex]::Replace($Value, '[\x00-\x1F\x7F/\\\[\]:;\|=,\+\*\?<>]', $safeReplacement)
if ($PreserveCase) {
return $safeValue
}
return $safeValue.ToUpperInvariant()
}
function Get-LiamFolderToken {
param(
[Parameter(Mandatory = $true)]
[string]$FolderPath
)
$itemName = Split-Path -Path $FolderPath -Leaf
if ([string]::IsNullOrWhiteSpace($itemName)) {
$itemName = $RootFolderName
}
ConvertTo-LiamSafeNameSegment `
-Value $itemName `
-Replacement $GroupNameSanitizeReplacement `
-PreserveCase $PreserveAdGroupNameCase.IsPresent
}
function Get-LiamGroupSet {
param(
[Parameter(Mandatory = $true)]
[string]$FolderPath
)
$name = Get-LiamFolderToken -FolderPath $FolderPath
[pscustomobject]@{
FolderPath = $FolderPath
Name = $name
GlobalOwner = "FS_${name}_O"
GlobalWrite = "FS_${name}_W"
GlobalRead = "FS_${name}_R"
GlobalTraverse = "FS_${name}_T"
LocalOwner = "UG_${name}_O"
LocalWrite = "UG_${name}_W"
LocalRead = "UG_${name}_R"
}
}
function Ensure-Folder {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
if (Test-Path -LiteralPath $Path) {
return
}
if ($PSCmdlet.ShouldProcess($Path, 'Create directory')) {
New-Item -ItemType Directory -Path $Path -Force | Out-Null
}
}
function Ensure-SmbShare {
param(
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $true)]
[string]$Path
)
$existingShare = Get-SmbShare -Name $Name -ErrorAction SilentlyContinue
if ($existingShare) {
if ($existingShare.Path -ne $Path) {
Write-Warning "SMB share '$Name' already points to '$($existingShare.Path)', not '$Path'."
}
return
}
if ($PSCmdlet.ShouldProcess($Name, "Create SMB share for '$Path'")) {
New-SmbShare -Name $Name -Path $Path -ChangeAccess 'Authenticated Users' | Out-Null
}
}
function ConvertTo-LdapFilterEscapedValue {
param(
[Parameter(Mandatory = $true)]
[string]$Value
)
$escapedValue = $Value.Replace('\', '\5c')
$escapedValue = $escapedValue.Replace('*', '\2a')
$escapedValue = $escapedValue.Replace('(', '\28')
$escapedValue = $escapedValue.Replace(')', '\29')
$escapedValue = $escapedValue.Replace([string][char]0, '\00')
return $escapedValue
}
function Get-AdGroupBySamAccountName {
param(
[Parameter(Mandatory = $true)]
[string]$SamAccountName,
[string[]]$Properties = @()
)
$escapedSamAccountName = ConvertTo-LdapFilterEscapedValue -Value $SamAccountName
$parameters = @{
LDAPFilter = "(sAMAccountName=$escapedSamAccountName)"
ErrorAction = 'Stop'
}
if ($Properties.Count -gt 0) {
$parameters['Properties'] = $Properties
}
try {
return Get-ADGroup @parameters | Select-Object -First 1
}
catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
return $null
}
}
function Ensure-AdGroup {
param(
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $true)]
[ValidateSet('Global', 'DomainLocal')]
[string]$Scope,
[Parameter(Mandatory = $true)]
[string]$Description
)
$existingGroup = Get-AdGroupBySamAccountName -SamAccountName $Name
if ($existingGroup) {
return $existingGroup
}
if ($PSCmdlet.ShouldProcess($Name, "Create AD group in '$GroupOuDN'")) {
New-ADGroup `
-Name $Name `
-SamAccountName $Name `
-GroupCategory Security `
-GroupScope $Scope `
-Path $GroupOuDN `
-Description $Description | Out-Null
}
return Get-AdGroupBySamAccountName -SamAccountName $Name
}
function Ensure-AdGroupMembership {
param(
[Parameter(Mandatory = $true)]
[string]$ParentGroup,
[Parameter(Mandatory = $true)]
[string]$MemberGroup
)
$parent = Get-AdGroupBySamAccountName -SamAccountName $ParentGroup -Properties @('member')
$member = Get-AdGroupBySamAccountName -SamAccountName $MemberGroup
if (-not $parent -or -not $member) {
if ($WhatIfPreference) {
if ($PSCmdlet.ShouldProcess($ParentGroup, "Add member group '$MemberGroup'")) {
}
return
}
throw "Cannot add '$MemberGroup' to '$ParentGroup' because at least one group does not exist."
}
if ($parent.member -contains $member.DistinguishedName) {
return
}
if ($PSCmdlet.ShouldProcess($ParentGroup, "Add member group '$MemberGroup'")) {
Add-ADGroupMember -Identity $parent -Members $member
}
}
function Ensure-LiamGroups {
param(
[Parameter(Mandatory = $true)]
[pscustomobject]$GroupSet
)
$groups = @(
@{ Name = $GroupSet.GlobalOwner; Scope = 'Global'; Description = "$($GroupSet.Name) - _O" }
@{ Name = $GroupSet.GlobalWrite; Scope = 'Global'; Description = "$($GroupSet.Name) - _W" }
@{ Name = $GroupSet.GlobalRead; Scope = 'Global'; Description = "$($GroupSet.Name) - _R" }
@{ Name = $GroupSet.LocalOwner; Scope = 'DomainLocal'; Description = "$($GroupSet.Name) - _O" }
@{ Name = $GroupSet.LocalWrite; Scope = 'DomainLocal'; Description = "$($GroupSet.Name) - _W" }
@{ Name = $GroupSet.LocalRead; Scope = 'DomainLocal'; Description = "$($GroupSet.Name) - _R" }
@{ Name = $GroupSet.GlobalTraverse; Scope = 'Global'; Description = "$($GroupSet.Name) - _T" }
)
foreach ($group in $groups) {
Ensure-AdGroup -Name $group.Name -Scope $group.Scope -Description $group.Description | Out-Null
}
Ensure-AdGroupMembership -ParentGroup $GroupSet.LocalOwner -MemberGroup $GroupSet.GlobalOwner
Ensure-AdGroupMembership -ParentGroup $GroupSet.LocalWrite -MemberGroup $GroupSet.GlobalWrite
Ensure-AdGroupMembership -ParentGroup $GroupSet.LocalRead -MemberGroup $GroupSet.GlobalRead
}
function Ensure-LogAclFixtureGroups {
param(
[Parameter(Mandatory = $true)]
[object[]]$Fixtures
)
$groupNames = $Fixtures |
ForEach-Object { $_['Groups'] } |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
Sort-Object -Unique
foreach ($groupName in $groupNames) {
$groupScope = if ($groupName -like 'UG_*') { 'DomainLocal' } else { 'Global' }
Ensure-AdGroup `
-Name $groupName `
-Scope $groupScope `
-Description 'Source ACL group from BRE.01.LEW diagnostic log' | Out-Null
}
}
function Add-FolderAccessRule {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[string]$Account,
[Parameter(Mandatory = $true)]
[System.Security.AccessControl.FileSystemRights]$Rights
)
if (-not (Test-Path -LiteralPath $Path)) {
if ($WhatIfPreference) {
if ($PSCmdlet.ShouldProcess($Path, "Add NTFS ACL '$Rights' for '$Account'")) {
}
return
}
throw "Path not found: $Path"
}
$acl = Get-Acl -LiteralPath $Path
$identity = [System.Security.Principal.NTAccount]::new($Account)
try {
$sid = $identity.Translate([System.Security.Principal.SecurityIdentifier])
}
catch [System.Security.Principal.IdentityNotMappedException] {
if ($WhatIfPreference) {
if ($PSCmdlet.ShouldProcess($Path, "Add NTFS ACL '$Rights' for '$Account'")) {
}
return
}
throw
}
foreach ($rule in $acl.Access) {
if ($rule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow) {
continue
}
if ($rule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value -ne $sid.Value) {
continue
}
if (($rule.FileSystemRights -band $Rights) -eq $Rights) {
return
}
}
$accessRule = [System.Security.AccessControl.FileSystemAccessRule]::new(
$sid,
$Rights,
[System.Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit',
[System.Security.AccessControl.PropagationFlags]::None,
[System.Security.AccessControl.AccessControlType]::Allow
)
$acl.AddAccessRule($accessRule)
if ($PSCmdlet.ShouldProcess($Path, "Add NTFS ACL '$Rights' for '$Account'")) {
Set-Acl -LiteralPath $Path -AclObject $acl
}
}
function Ensure-LiamNtfsAcl {
param(
[Parameter(Mandatory = $true)]
[pscustomobject]$GroupSet,
[Parameter(Mandatory = $true)]
[string]$DomainNetBiosName
)
Add-FolderAccessRule -Path $GroupSet.FolderPath -Account "$DomainNetBiosName\$($GroupSet.LocalOwner)" -Rights $ownerRights
Add-FolderAccessRule -Path $GroupSet.FolderPath -Account "$DomainNetBiosName\$($GroupSet.LocalWrite)" -Rights $writeRights
Add-FolderAccessRule -Path $GroupSet.FolderPath -Account "$DomainNetBiosName\$($GroupSet.LocalRead)" -Rights $readRights
}
function Ensure-LogAclFixtureAcls {
param(
[Parameter(Mandatory = $true)]
[object[]]$Fixtures,
[Parameter(Mandatory = $true)]
[string]$DomainNetBiosName
)
foreach ($fixture in $Fixtures) {
$relativePath = $fixture['RelativePath']
$folderPath = if ([string]::IsNullOrWhiteSpace($relativePath)) {
$rootPath
}
else {
Join-Path -Path $rootPath -ChildPath $relativePath
}
foreach ($groupName in ($fixture['Groups'] | Sort-Object -Unique)) {
Add-FolderAccessRule `
-Path $folderPath `
-Account "$DomainNetBiosName\$groupName" `
-Rights $readRights
}
}
}
$relativeFolders = @($logFolderRelativePaths + $AdditionalFolderRelativePaths) |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
ForEach-Object { $_.Trim().TrimStart('\', '/') } |
Sort-Object -Unique
Ensure-Folder -Path $ShareRootPath
Ensure-Folder -Path $rootPath
foreach ($relativeFolder in $relativeFolders) {
Ensure-Folder -Path (Join-Path -Path $rootPath -ChildPath $relativeFolder)
}
if (-not $SkipSmbShare) {
Ensure-SmbShare -Name $SmbShareName -Path $ShareRootPath
}
$foldersToManage = @($rootPath)
foreach ($relativeFolder in $relativeFolders) {
$foldersToManage += (Join-Path -Path $rootPath -ChildPath $relativeFolder)
}
$domainNetBiosName = $null
if (-not $SkipAdGroups -or -not $SkipNtfsAcl) {
Import-Module ActiveDirectory -ErrorAction Stop
$domainNetBiosName = (Get-ADDomain).NetBIOSName
}
if (-not $SkipAdGroups -and -not $SkipLogAclFixture) {
Ensure-LogAclFixtureGroups -Fixtures $logAclFixtures
}
$summary = foreach ($folder in $foldersToManage) {
$groupSet = Get-LiamGroupSet -FolderPath $folder
if (-not $SkipAdGroups -and $CreateExpectedLiamGroups) {
Ensure-LiamGroups -GroupSet $groupSet
}
if (-not $SkipNtfsAcl -and $CreateExpectedLiamGroups) {
Ensure-LiamNtfsAcl -GroupSet $groupSet -DomainNetBiosName $domainNetBiosName
}
$relativePath = if ($folder -eq $rootPath) {
''
}
else {
$folder.Substring($rootPath.Length).TrimStart('\')
}
$fixture = $logAclFixtures | Where-Object { $_['RelativePath'] -eq $relativePath } | Select-Object -First 1
[pscustomobject]@{
Folder = $folder
LogAclGroupCount = if ($fixture) { $fixture['Groups'].Count } else { 0 }
LocalOwnerAcl = $groupSet.LocalOwner
LocalWriteAcl = $groupSet.LocalWrite
LocalReadAcl = $groupSet.LocalRead
GlobalOwner = $groupSet.GlobalOwner
GlobalWrite = $groupSet.GlobalWrite
GlobalRead = $groupSet.GlobalRead
GlobalTraverse = $groupSet.GlobalTraverse
}
}
if (-not $SkipNtfsAcl -and -not $SkipLogAclFixture) {
Ensure-LogAclFixtureAcls -Fixtures $logAclFixtures -DomainNetBiosName $domainNetBiosName
}
$summary
Write-Host ''
Write-Host 'LIAM provider RootPath for this machine:'
Write-Host ("\\{0}\{1}\{2}" -f $env:COMPUTERNAME, $SmbShareName, $RootFolderName)