diff --git a/LiamNtfs/C4IT.LIAM.Ntfs.cs b/LiamNtfs/C4IT.LIAM.Ntfs.cs index dc7888d..cd21a4b 100644 --- a/LiamNtfs/C4IT.LIAM.Ntfs.cs +++ b/LiamNtfs/C4IT.LIAM.Ntfs.cs @@ -144,6 +144,8 @@ namespace C4IT.LIAM private const string AdditionalConfigurationGroupNameSanitizeReplacementKey = "NtfsGroupNameSanitizeReplacement"; private const string AdditionalConfigurationPreserveAdGroupNameCaseKey = "PreserveNtfsAdGroupNameCase"; private const string AdditionalConfigurationAdDomainControllersKey = "NtfsAdDomainControllers"; + private const string AdditionalConfigurationAdGroupReuseModeKey = "NtfsAdGroupReuseMode"; + private const string AdditionalConfigurationAdGroupMarkerBackfillKey = "NtfsAdGroupMarkerBackfill"; public readonly cNtfsBase ntfsBase = new cNtfsBase(); public readonly cActiveDirectoryBase activeDirectoryBase = new cActiveDirectoryBase(); private readonly Dictionary> publishedShareCache = new Dictionary>(StringComparer.OrdinalIgnoreCase); @@ -1394,6 +1396,8 @@ namespace C4IT.LIAM engine.CanManagePermissionsForPath = IsPermissionManagedFolderPath; engine.CanManageTraversePermissionsForPath = IsTraversePermissionManagedPath; engine.forceStrictAdGroupNames = IsAdditionalConfigurationEnabled("ForceStrictAdGroupNames"); + engine.adGroupReuseMode = GetAdGroupReuseMode(); + engine.adGroupMarkerBackfill = IsAdditionalConfigurationEnabled(AdditionalConfigurationAdGroupMarkerBackfillKey); engine.groupNameSanitizeReplacement = GetAdditionalConfigurationValueOrDefault( AdditionalConfigurationGroupNameSanitizeReplacementKey, Helper.DefaultGroupNameSanitizeReplacement); @@ -1458,6 +1462,25 @@ namespace C4IT.LIAM return rawValue == null ? string.Empty : rawValue.Trim(); } + private SecurityGroupReuseMode GetAdGroupReuseMode() + { + var value = GetAdditionalConfigurationValue(AdditionalConfigurationAdGroupReuseModeKey); + if (string.IsNullOrWhiteSpace(value)) + return SecurityGroupReuseMode.Safe; + + if (value.Equals("Safe", StringComparison.OrdinalIgnoreCase)) + return SecurityGroupReuseMode.Safe; + + if (value.Equals("Name", StringComparison.OrdinalIgnoreCase)) + { + LogEntry("NtfsAdGroupReuseMode=Name is active. Existing AD groups may be reused by name without ACL/marker validation.", LogLevels.Warning); + return SecurityGroupReuseMode.Name; + } + + LogEntry($"AdditionalConfiguration '{AdditionalConfigurationAdGroupReuseModeKey}' has invalid value '{value}'. Defaulting to Safe.", LogLevels.Warning); + return SecurityGroupReuseMode.Safe; + } + private string GetEffectiveTraverseBoundaryPath() { var configuredBoundaryPath = GetAdditionalConfigurationValue(AdditionalConfigurationTraverseBoundaryPathKey); diff --git a/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs b/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs index f56ec5a..4cd0d6b 100644 --- a/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs +++ b/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs @@ -57,6 +57,8 @@ namespace C4IT_IAM_SET public Func 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; @@ -323,7 +325,9 @@ namespace C4IT_IAM_SET effectiveDomainController = effectiveDomainController, password = password, ForceStrictAdGroupNames = forceStrictAdGroupNames, - PreserveAdGroupNameCase = preserveAdGroupNameCase + PreserveAdGroupNameCase = preserveAdGroupNameCase, + ReuseMode = adGroupReuseMode, + MarkerBackfill = adGroupMarkerBackfill }; } @@ -934,7 +938,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++; diff --git a/LiamNtfs/C4IT_IAM_SET/SecurityGroup.cs b/LiamNtfs/C4IT_IAM_SET/SecurityGroup.cs index 9c97dac..ea9d224 100644 --- a/LiamNtfs/C4IT_IAM_SET/SecurityGroup.cs +++ b/LiamNtfs/C4IT_IAM_SET/SecurityGroup.cs @@ -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_SecurityGroups; public string rootUID; + private const string MarkerAttributeName = "info"; + private const string MarkerPrefix = "LIAM;Provider=Ntfs;"; public SecurityGroups() { IAM_SecurityGroups = new List(); @@ -269,9 +273,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 +289,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 +305,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 +325,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 +342,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 +359,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 +677,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 ParseMarker(string marker) + { + var result = new Dictionary(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 GetLiamMarkers(string info) + { + if (string.IsNullOrWhiteSpace(info)) + return Enumerable.Empty(); + + 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 users, string folderPath = null) { LogMethodBegin(MethodBase.GetCurrentMethod()); @@ -668,8 +846,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 +862,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 +884,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 +900,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 +923,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 +1040,17 @@ namespace C4IT_IAM_Engine public int targetTyp; public GroupScope Scope; public FileSystemRights rights; + public string MarkerPath; public IAM_SecurityGroup() { memberGroups = new List(); } } + public enum SecurityGroupReuseMode + { + Safe, + Name + } public enum SecurityGroupType { [XmlEnum(Name = "0")] diff --git a/README.md b/README.md index e51cbd2..2c3ff13 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,8 @@ Im Diagnose-JSON erscheinen diese Werte unter `AdditionalConfiguration`. Paramet | `NtfsGroupNameSanitizeReplacement` | Zeichenfolge, z. B. `_`, `.`, leer, `none`, `remove`, `` | Legt fest, womit ungueltige Zeichen in dynamischen gruppennamenrelevanten Pfadbestandteilen ersetzt werden. Standard ist `_`. Mit leerem Wert oder `none`/`remove`/`` 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: