Compare commits
29 Commits
44954bdd39
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18a96d3e23 | ||
|
|
c1e26ba615 | ||
|
|
c4efc41c4b | ||
|
|
daec5adfb5 | ||
|
|
dd0ebd3d6d | ||
|
|
0b1dbd7925 | ||
|
|
e7fb041ff5 | ||
|
|
5dfccca237 | ||
|
|
f9133b3549 | ||
|
|
10c487572d | ||
|
|
e9bed32334 | ||
|
|
af5f0784b8 | ||
|
|
077bac99ae | ||
|
|
2f1ab27ce9 | ||
|
|
b0fd1d0c75 | ||
|
|
b9cd74c5a9 | ||
|
|
e8b138f9c3 | ||
|
|
3ca1a41816 | ||
|
|
e23397c2ac | ||
|
|
fb9494063b | ||
|
|
fd818f9eb2 | ||
|
|
2217a393be | ||
|
|
6abe0a690b | ||
|
|
fe059de50d | ||
|
|
60518a801f | ||
|
|
b0cfed750c | ||
|
|
275abe8ebd | ||
|
|
f1fb5dbc1b | ||
|
|
15533ef005 |
@@ -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
@@ -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>[^{]*)}})")))
|
||||
{
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 =>
|
||||
{
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 || ntfsArea is cLiamNtfsShare,
|
||||
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");
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
79
README.md
79
README.md
@@ -22,6 +22,9 @@ Die wichtigsten Eingaben fuer die Ersetzung sind:
|
||||
| `{{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` |
|
||||
@@ -31,6 +34,8 @@ Die wichtigsten Eingaben fuer die Ersetzung sind:
|
||||
|
||||
`{{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
|
||||
@@ -174,7 +179,7 @@ Wenn eine Traverse-Naming-Convention keine Gruppe erzeugen soll, kann das `Namin
|
||||
|
||||
### Sanitizing, Laenge und Grossschreibung
|
||||
|
||||
Pfadbasierte Platzhalter wie `{{NAME}}`, `{{RELATIVEPATH}}`, `{{ROOT_PATH}}` und `{{ROOT_SEGMENT(n)}}` werden fuer AD-Gruppennamen bereinigt. Standardmaessig werden ungueltige Zeichen mit `_` ersetzt. Das Ersatzzeichen kann ueber `NtfsGroupNameSanitizeReplacement` geaendert oder entfernt werden.
|
||||
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.
|
||||
|
||||
@@ -205,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 `;`, | 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 `;`, | 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:
|
||||
@@ -219,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 |
|
||||
|
||||
@@ -7,5 +7,5 @@ using System.Runtime.InteropServices;
|
||||
[assembly: AssemblyCopyright("Copyright © 2026, Consulting4IT GmbH, Germany")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
|
||||
[assembly: AssemblyInformationalVersion("3.3.2")]
|
||||
[assembly: AssemblyVersion("3.3.2")]
|
||||
[assembly: AssemblyInformationalVersion("3.3.5")]
|
||||
[assembly: AssemblyVersion("3.3.5")]
|
||||
|
||||
695
Sonstiges/New-LiamNtfsDemoShareStructure.ps1
Normal file
695
Sonstiges/New-LiamNtfsDemoShareStructure.ps1
Normal 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)
|
||||
Reference in New Issue
Block a user