From 0cad46ddef61f1c5fa7617c9b6beea0b588eb424 Mon Sep 17 00:00:00 2001 From: Meik Date: Tue, 19 May 2026 19:12:33 +0200 Subject: [PATCH] Pin NTFS AD operations to domain controller --- LiamNtfs/C4IT.LIAM.Ntfs.cs | 3 + LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs | 14 +++- LiamNtfs/C4IT_IAM_SET/SecurityGroup.cs | 22 ++++-- LiamNtfs/cActiveDirectoryBase.cs | 74 ++++++++++++++++++- LiamNtfs/cNtfsBase.cs | 1 + ...nalConfiguration_BlackWhitelist_Konzept.md | 18 +++++ 6 files changed, 119 insertions(+), 13 deletions(-) diff --git a/LiamNtfs/C4IT.LIAM.Ntfs.cs b/LiamNtfs/C4IT.LIAM.Ntfs.cs index a925db9..b8f6aa1 100644 --- a/LiamNtfs/C4IT.LIAM.Ntfs.cs +++ b/LiamNtfs/C4IT.LIAM.Ntfs.cs @@ -58,6 +58,7 @@ namespace C4IT.LIAM private const string AdditionalConfigurationTraverseBoundaryPathKey = "NtfsTraverseBoundaryPath"; private const string AdditionalConfigurationGroupNameSanitizeReplacementKey = "NtfsGroupNameSanitizeReplacement"; private const string AdditionalConfigurationPreserveAdGroupNameCaseKey = "PreserveNtfsAdGroupNameCase"; + private const string AdditionalConfigurationAdDomainControllersKey = "NtfsAdDomainControllers"; public readonly cNtfsBase ntfsBase = new cNtfsBase(); public readonly cActiveDirectoryBase activeDirectoryBase = new cActiveDirectoryBase(); private readonly Dictionary> publishedShareCache = new Dictionary>(StringComparer.OrdinalIgnoreCase); @@ -137,6 +138,7 @@ namespace C4IT.LIAM var LI = new cNtfsLogonInfo() { Domain = Domain, + DomainControllers = GetAdditionalConfigurationValue(AdditionalConfigurationAdDomainControllersKey), User = Credential?.Identification, UserSecret = Credential?.Secret, TargetNetworkName = RootPath, @@ -980,6 +982,7 @@ namespace C4IT.LIAM { ConfigID = "manual", domainName = this.Domain, + effectiveDomainController = activeDirectoryBase.EffectiveDomainController, username = this.Credential.Identification, password = new NetworkCredential("", this.Credential.Secret).SecurePassword, baseFolder = this.RootPath, diff --git a/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs b/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs index 0920f17..6152f54 100644 --- a/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs +++ b/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs @@ -27,6 +27,7 @@ namespace C4IT_IAM_SET public const string constApplicationDataPath = "%ProgramData%\\Consulting4IT GmbH\\LIAM"; public string domainName; + public string effectiveDomainController; public string username; public SecureString password; private cNetworkConnection Connection; @@ -87,6 +88,11 @@ namespace C4IT_IAM_SET templates = new List(); } + private string GetAdServer() + { + return string.IsNullOrWhiteSpace(effectiveDomainController) ? domainName : effectiveDomainController; + } + private ResultToken checkRequiredVariables() { ResultToken resultToken = new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString()); @@ -302,6 +308,7 @@ namespace C4IT_IAM_SET { username = username, domainName = domainName, + effectiveDomainController = effectiveDomainController, password = password, ForceStrictAdGroupNames = forceStrictAdGroupNames, PreserveAdGroupNameCase = preserveAdGroupNameCase @@ -441,7 +448,7 @@ namespace C4IT_IAM_SET return resultToken; } - var domainContext = new PrincipalContext(ContextType.Domain, domainName, username, new NetworkCredential("", password).Password); + var domainContext = new PrincipalContext(ContextType.Domain, GetAdServer(), username, new NetworkCredential("", password).Password); DefaultLogger.LogEntry(LogLevels.Debug, "PrincipalContext erfolgreich erstellt."); // Überprüfen von newDataArea und IAM_Folders @@ -1005,7 +1012,7 @@ namespace C4IT_IAM_SET return null; } - var basePath = "LDAP://" + domainName; + var basePath = "LDAP://" + GetAdServer(); if (!string.IsNullOrWhiteSpace(groupOUPath)) basePath += "/" + groupOUPath; @@ -1213,6 +1220,7 @@ namespace C4IT_IAM_SET { ResultToken resultToken = new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString()); resultToken.resultErrorId = 0; + newSecurityGroups.effectiveDomainController = effectiveDomainController; if (Directory.Exists(newDataArea.IAM_Folders[0].technicalName)) { resultToken.resultMessage = "New folder " + newDataArea.IAM_Folders[0].technicalName + " already exists"; @@ -1602,7 +1610,7 @@ namespace C4IT_IAM_SET try { - PrincipalContext ctx = new PrincipalContext(ContextType.Domain, domainName, username, new NetworkCredential("", password).Password); + PrincipalContext ctx = new PrincipalContext(ContextType.Domain, GetAdServer(), username, new NetworkCredential("", password).Password); UserPrincipal user; user = UserPrincipal.FindByIdentity(ctx, IdentityType.Sid, (sid)); return user; diff --git a/LiamNtfs/C4IT_IAM_SET/SecurityGroup.cs b/LiamNtfs/C4IT_IAM_SET/SecurityGroup.cs index 3e9bb92..9c97dac 100644 --- a/LiamNtfs/C4IT_IAM_SET/SecurityGroup.cs +++ b/LiamNtfs/C4IT_IAM_SET/SecurityGroup.cs @@ -21,6 +21,7 @@ namespace C4IT_IAM_Engine public class SecurityGroups { public string domainName; + public string effectiveDomainController; public string username; public SecureString password; public bool ForceStrictAdGroupNames; @@ -32,6 +33,11 @@ namespace C4IT_IAM_Engine { IAM_SecurityGroups = new List(); } + + private string GetLdapServer() + { + return string.IsNullOrWhiteSpace(effectiveDomainController) ? domainName : effectiveDomainController; + } public bool GroupsAllreadyExisting(string ouPath) { LogMethodBegin(MethodBase.GetCurrentMethod()); @@ -47,7 +53,7 @@ namespace C4IT_IAM_Engine { DirectoryEntry entry = new DirectoryEntry { - Path = "LDAP://" + domainName, + Path = "LDAP://" + GetLdapServer(), Username = username, Password = new NetworkCredential("", password).Password, AuthenticationType = AuthenticationTypes.Secure | AuthenticationTypes.Sealing @@ -86,7 +92,7 @@ namespace C4IT_IAM_Engine { DirectoryEntry entry = new DirectoryEntry { - Path = "LDAP://" + domainName, + Path = "LDAP://" + GetLdapServer(), Username = username, Password = new NetworkCredential("", password).Password, AuthenticationType = AuthenticationTypes.Secure | AuthenticationTypes.Sealing @@ -431,7 +437,7 @@ namespace C4IT_IAM_Engine { DirectoryEntry entry = new DirectoryEntry { - Path = "LDAP://" + domainName, + Path = "LDAP://" + GetLdapServer(), Username = username, Password = new NetworkCredential("", password).Password, AuthenticationType = AuthenticationTypes.Secure | AuthenticationTypes.Sealing @@ -473,7 +479,7 @@ namespace C4IT_IAM_Engine return null; } - var basePath = "LDAP://" + domainName; + var basePath = "LDAP://" + GetLdapServer(); if (!string.IsNullOrWhiteSpace(ouPath)) basePath += "/" + ouPath; @@ -528,7 +534,7 @@ namespace C4IT_IAM_Engine return null; DefaultLogger.LogEntry(LogLevels.Debug, $"Reusing existing AD group '{matchedName}' via wildcard '{wildcardPattern}'."); - return new DirectoryEntry("LDAP://" + domainName + "/" + matchedDistinguishedName, username, new NetworkCredential("", password).Password, AuthenticationTypes.Secure | AuthenticationTypes.Sealing); + return new DirectoryEntry("LDAP://" + GetLdapServer() + "/" + matchedDistinguishedName, username, new NetworkCredential("", password).Password, AuthenticationTypes.Secure | AuthenticationTypes.Sealing); } private DirectoryEntry FindGroupEntryFromFolderAcl(string folderPath, string wildcardPattern) @@ -555,7 +561,7 @@ namespace C4IT_IAM_Engine .Cast(); var matchedNames = new HashSet(StringComparer.OrdinalIgnoreCase); - using (var domainContext = new PrincipalContext(ContextType.Domain, domainName, username, new NetworkCredential("", password).Password)) + using (var domainContext = new PrincipalContext(ContextType.Domain, GetLdapServer(), username, new NetworkCredential("", password).Password)) { foreach (var rule in rules) { @@ -735,7 +741,7 @@ namespace C4IT_IAM_Engine if (!GroupAllreadyExisting(groupName)) { - DirectoryEntry entry = new DirectoryEntry("LDAP://" + domainName + "/" + ouPath, username, new NetworkCredential("", password).Password, AuthenticationTypes.Secure | AuthenticationTypes.Sealing); + 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; @@ -763,7 +769,7 @@ namespace C4IT_IAM_Engine } group.CommitChanges(); - DirectoryEntry ent = new DirectoryEntry("LDAP://" + domainName + "/" + "CN=" + groupName + "," + ouPath, username, new NetworkCredential("", password).Password, AuthenticationTypes.Secure | AuthenticationTypes.Sealing); + 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}"); diff --git a/LiamNtfs/cActiveDirectoryBase.cs b/LiamNtfs/cActiveDirectoryBase.cs index 87a886b..33f02ad 100644 --- a/LiamNtfs/cActiveDirectoryBase.cs +++ b/LiamNtfs/cActiveDirectoryBase.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.DirectoryServices; +using System.DirectoryServices.ActiveDirectory; using System.DirectoryServices.AccountManagement; using System.IO; using System.Linq; @@ -24,6 +25,7 @@ namespace LiamNtfs private cNtfsLogonInfo privLogonInfo = null; public PrincipalContext adContext = null; public DirectoryEntry directoryEntry = null; + public string EffectiveDomainController { get; private set; } = null; public Exception LastException { get; private set; } = null; public string LastErrorMessage { get; private set; } = null; @@ -49,8 +51,12 @@ namespace LiamNtfs //TODO: remove dummy delay? await Task.Delay(0); ResetError(); - adContext = new PrincipalContext(ContextType.Domain, LogonInfo.Domain, LogonInfo.User, new NetworkCredential("", LogonInfo.UserSecret).Password); - var ldapPath = $"LDAP://{LogonInfo.Domain}/{LogonInfo.TargetGroupPath}"; + var adServer = ResolveEffectiveDomainController(LogonInfo); + adContext = new PrincipalContext(ContextType.Domain, adServer, LogonInfo.User, new NetworkCredential("", LogonInfo.UserSecret).Password); + EffectiveDomainController = adContext.ConnectedServer ?? adServer; + LogEntry($"NTFS AD domain controller pinned to '{EffectiveDomainController}' for domain '{LogonInfo.Domain}'.", LogLevels.Debug); + + var ldapPath = $"LDAP://{EffectiveDomainController}/{LogonInfo.TargetGroupPath}"; directoryEntry = new DirectoryEntry { Path = ldapPath, @@ -69,6 +75,70 @@ namespace LiamNtfs return false; } + private string ResolveEffectiveDomainController(cNtfsLogonInfo logonInfo) + { + var configuredDomainControllers = ParseDomainControllers(logonInfo?.DomainControllers); + foreach (var domainController in configuredDomainControllers) + { + if (CanConnectToDomainController(domainController, logonInfo)) + return domainController; + + LogEntry($"Configured NTFS AD domain controller '{domainController}' is not reachable. Trying next candidate.", LogLevels.Warning); + } + + var pdc = TryGetPdcRoleOwner(logonInfo); + if (!string.IsNullOrWhiteSpace(pdc)) + return pdc; + + LogEntry($"Could not determine PDC emulator for domain '{logonInfo?.Domain}'. Falling back to domain locator.", LogLevels.Warning); + return logonInfo?.Domain; + } + + private static IEnumerable ParseDomainControllers(string domainControllers) + { + if (string.IsNullOrWhiteSpace(domainControllers)) + return Enumerable.Empty(); + + return domainControllers + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(i => i.Trim()) + .Where(i => !string.IsNullOrWhiteSpace(i)); + } + + private bool CanConnectToDomainController(string domainController, cNtfsLogonInfo logonInfo) + { + try + { + using (var context = new PrincipalContext(ContextType.Domain, domainController, logonInfo.User, new NetworkCredential("", logonInfo.UserSecret).Password)) + return !string.IsNullOrWhiteSpace(context.ConnectedServer); + } + catch (Exception E) + { + LogException(E, LogLevels.Debug); + return false; + } + } + + private string TryGetPdcRoleOwner(cNtfsLogonInfo logonInfo) + { + try + { + var credentials = new DirectoryContext( + DirectoryContextType.Domain, + logonInfo.Domain, + logonInfo.User, + new NetworkCredential("", logonInfo.UserSecret).Password); + + using (var domain = Domain.GetDomain(credentials)) + return domain?.PdcRoleOwner?.Name; + } + catch (Exception E) + { + LogException(E, LogLevels.Debug); + return null; + } + } + private async Task privRelogon() { if (privLogonInfo == null) diff --git a/LiamNtfs/cNtfsBase.cs b/LiamNtfs/cNtfsBase.cs index f19b13a..7124cbb 100644 --- a/LiamNtfs/cNtfsBase.cs +++ b/LiamNtfs/cNtfsBase.cs @@ -334,6 +334,7 @@ namespace LiamNtfs public class cNtfsLogonInfo { public string Domain; + public string DomainControllers; public string User; public string UserSecret; public string TargetNetworkName; diff --git a/Sonstiges/LIAM_NTFS_AdditionalConfiguration_BlackWhitelist_Konzept.md b/Sonstiges/LIAM_NTFS_AdditionalConfiguration_BlackWhitelist_Konzept.md index a2b9c7b..495ff0c 100644 --- a/Sonstiges/LIAM_NTFS_AdditionalConfiguration_BlackWhitelist_Konzept.md +++ b/Sonstiges/LIAM_NTFS_AdditionalConfiguration_BlackWhitelist_Konzept.md @@ -269,6 +269,24 @@ ACL_share2.test33_O `PreserveNtfsAdGroupNameCase=1` unterbindet die bisher automatische Grossschreibung der erzeugten AD-Gruppennamen. Ohne diesen Schalter bleibt das bisherige Verhalten erhalten und die generierten CN-/sAMAccountName-Werte werden in Grossbuchstaben erzeugt. +### 12. Domain-Controller-Pinning fuer direkte AD-Readbacks + +Der NTFS-Provider pinnt seine AD-Zugriffe auf einen konkreten Domain Controller, damit neu angelegte Gruppen unmittelbar im gleichen oder in einem direkt folgenden Workflow-Lauf wieder gefunden werden koennen. Ohne Pinning kann Windows bei getrennten WF-Instanzen unterschiedliche DCs auswaehlen; bei langen AD-Replikationsintervallen waeren frisch angelegte Gruppen dann auf dem zweiten DC noch nicht sichtbar. + +Standardverhalten ohne Konfiguration: + +- der Provider ermittelt automatisch den PDC Emulator der Domain +- alle NTFS-AD-Zugriffe innerhalb des Provider-Laufs verwenden diesen DC +- der verwendete DC wird einmal beim AD-Logon im Debug-Log ausgegeben + +Optional kann ueber `AdditionalConfiguration` eine priorisierte, kommagetrennte DC-Liste gesetzt werden: + +```text +NtfsAdDomainControllers=dc01.imagoverum.com,dc02.imagoverum.com +``` + +Der Provider testet die Eintraege in Reihenfolge. Der erste erreichbare DC wird verwendet. Wenn kein konfigurierter DC erreichbar ist, faellt der Provider auf den automatisch ermittelten PDC Emulator zurueck. Wenn auch dieser nicht ermittelt werden kann, wird wie bisher die Domain selbst verwendet und damit wieder der Windows-DC-Locator genutzt. + ## Matching-Regeln Empfohlene Semantik: