From a9b4cfe10bedfb8b44550fbdf1fcdc44e8a8f1c7 Mon Sep 17 00:00:00 2001 From: Meik Date: Tue, 19 May 2026 20:01:52 +0200 Subject: [PATCH] Pin Active Directory provider to domain controller --- LIAMActiveDirectory/C4IT.LIAM.AD.cs | 25 +++++- LIAMActiveDirectory/cADBase.cs | 1 + LIAMActiveDirectory/cADServiceGroupCreator.cs | 27 ++++-- LIAMActiveDirectory/cActiveDirectoryBase.cs | 86 ++++++++++++++++++- ...nalConfiguration_BlackWhitelist_Konzept.md | 8 ++ 5 files changed, 135 insertions(+), 12 deletions(-) diff --git a/LIAMActiveDirectory/C4IT.LIAM.AD.cs b/LIAMActiveDirectory/C4IT.LIAM.AD.cs index cef84f5..f4c90ab 100644 --- a/LIAMActiveDirectory/C4IT.LIAM.AD.cs +++ b/LIAMActiveDirectory/C4IT.LIAM.AD.cs @@ -30,6 +30,8 @@ namespace C4IT.LIAM public class cLiamProviderAD : cLiamProviderBase { public static Guid adModuleId = new Guid("e820a625-0653-ee11-b886-00155d300101"); + private const string AdditionalConfigurationAdDomainControllersKey = "AdDomainControllers"; + private const string AdditionalConfigurationActiveDirectoryDomainControllersKey = "ActiveDirectoryDomainControllers"; public readonly cActiveDirectoryBase activeDirectoryBase = new cActiveDirectoryBase(); private readonly ADServiceGroupCreator _serviceGroupCreator; @@ -76,6 +78,7 @@ namespace C4IT.LIAM var LI = new cADLogonInfo() { Domain = Domain, + DomainControllers = GetConfiguredDomainControllers(), User = Credential?.Identification, UserSecret = Credential?.Secret, TargetGroupPath = this.GroupPath @@ -95,6 +98,26 @@ namespace C4IT.LIAM return false; } + private string GetConfiguredDomainControllers() + { + var value = GetAdditionalConfigurationValue(AdditionalConfigurationAdDomainControllersKey); + if (!string.IsNullOrWhiteSpace(value)) + return value; + + return GetAdditionalConfigurationValue(AdditionalConfigurationActiveDirectoryDomainControllersKey); + } + + private string GetAdditionalConfigurationValue(string key) + { + if (AdditionalConfiguration == null || string.IsNullOrWhiteSpace(key)) + return string.Empty; + + if (!AdditionalConfiguration.TryGetValue(key, out var rawValue) || string.IsNullOrWhiteSpace(rawValue)) + return string.Empty; + + return rawValue.Trim(); + } + public override async Task> getDataAreasAsync(int Depth = -1) { var CM = MethodBase.GetCurrentMethod(); @@ -368,4 +391,4 @@ namespace C4IT.LIAM this.scope = secGroup.Scope.ToString(); } } -} \ No newline at end of file +} diff --git a/LIAMActiveDirectory/cADBase.cs b/LIAMActiveDirectory/cADBase.cs index 3d730fc..b30e4d2 100644 --- a/LIAMActiveDirectory/cADBase.cs +++ b/LIAMActiveDirectory/cADBase.cs @@ -52,6 +52,7 @@ namespace LiamAD public class cADLogonInfo { public string Domain; + public string DomainControllers; public string User; public string UserSecret; public string TargetGroupPath; diff --git a/LIAMActiveDirectory/cADServiceGroupCreator.cs b/LIAMActiveDirectory/cADServiceGroupCreator.cs index e3f52d0..83fcd93 100644 --- a/LIAMActiveDirectory/cADServiceGroupCreator.cs +++ b/LIAMActiveDirectory/cADServiceGroupCreator.cs @@ -19,7 +19,6 @@ namespace LiamAD { private readonly cLiamProviderAD _provider; private readonly cActiveDirectoryBase _adBase; - private readonly string _ldapRoot; private readonly string _user; private readonly string _password; public enum ADGroupType @@ -31,11 +30,25 @@ namespace LiamAD { _provider = provider ?? throw new ArgumentNullException(nameof(provider)); _adBase = provider.activeDirectoryBase; - _ldapRoot = $"LDAP://{provider.Domain}/{provider.GroupPath}"; _user = provider.Credential.Identification; _password = new System.Net.NetworkCredential(_user, provider.Credential.Secret).Password; } + private string GetLdapServer() + { + return string.IsNullOrWhiteSpace(_adBase.EffectiveDomainController) ? _provider.Domain : _adBase.EffectiveDomainController; + } + + private string GetLdapRoot() + { + return $"LDAP://{GetLdapServer()}/{_provider.GroupPath}"; + } + + private string GetLdapDomainRoot() + { + return $"LDAP://{GetLdapServer()}"; + } + /// /// Erstellt oder findet beide AD-Gruppen (Member & Owner) für einen Service. /// Neu mit: gruppenbereich (Scope) und gruppentyp (für Member-Gruppe). @@ -115,7 +128,7 @@ namespace LiamAD if (sidList == null) return; // Basis für die Suche: komplette Domäne, nicht nur der OU-Pfad - string domainRoot = $"LDAP://{_provider.Domain}"; + string domainRoot = GetLdapDomainRoot(); using (var root = new DirectoryEntry(domainRoot, _user, _password, AuthenticationTypes.Secure)) using (var grpSearch = new DirectorySearcher(root)) { @@ -185,7 +198,7 @@ namespace LiamAD private string GetSid(string name) { - using (var root = new DirectoryEntry(_ldapRoot, _user, _password, AuthenticationTypes.Secure)) + using (var root = new DirectoryEntry(GetLdapRoot(), _user, _password, AuthenticationTypes.Secure)) using (var ds = new DirectorySearcher(root)) { ds.Filter = $"(&(objectCategory=group)(sAMAccountName={name}))"; @@ -219,7 +232,7 @@ namespace LiamAD { if (!GroupExists(groupName)) { - using (var root = new DirectoryEntry(_ldapRoot, _user, _password, AuthenticationTypes.Secure)) + using (var root = new DirectoryEntry(GetLdapRoot(), _user, _password, AuthenticationTypes.Secure)) { var grp = root.Children.Add("CN=" + groupName, "group"); grp.Properties["sAMAccountName"].Value = groupName; @@ -261,7 +274,7 @@ namespace LiamAD private string GetDistinguishedName(string name) { - using (var root = new DirectoryEntry(_ldapRoot, _user, _password, AuthenticationTypes.Secure)) + using (var root = new DirectoryEntry(GetLdapRoot(), _user, _password, AuthenticationTypes.Secure)) using (var ds = new DirectorySearcher(root)) { ds.Filter = "(&(objectClass=group)(sAMAccountName=" + name + "))"; @@ -285,4 +298,4 @@ namespace LiamAD } } } -} \ No newline at end of file +} diff --git a/LIAMActiveDirectory/cActiveDirectoryBase.cs b/LIAMActiveDirectory/cActiveDirectoryBase.cs index 6bdd65c..39d7d88 100644 --- a/LIAMActiveDirectory/cActiveDirectoryBase.cs +++ b/LIAMActiveDirectory/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; @@ -25,6 +26,7 @@ namespace LiamAD private cADLogonInfo 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; @@ -50,8 +52,12 @@ namespace LiamAD //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($"AD provider domain controller pinned to '{EffectiveDomainController}' for domain '{LogonInfo.Domain}'.", LogLevels.Debug); + + var ldapPath = $"LDAP://{EffectiveDomainController}/{LogonInfo.TargetGroupPath}"; directoryEntry = new DirectoryEntry { Path = ldapPath, @@ -70,6 +76,78 @@ namespace LiamAD return false; } + private string ResolveEffectiveDomainController(cADLogonInfo logonInfo) + { + var configuredDomainControllers = ParseDomainControllers(logonInfo?.DomainControllers); + foreach (var domainController in configuredDomainControllers) + { + if (CanConnectToDomainController(domainController, logonInfo)) + return domainController; + + LogEntry($"Configured AD provider 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, cADLogonInfo 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(cADLogonInfo 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 string GetAdServer() + { + if (!string.IsNullOrWhiteSpace(EffectiveDomainController)) + return EffectiveDomainController; + + return privLogonInfo?.Domain; + } + private async Task privRelogon() { if (privLogonInfo == null) @@ -193,7 +271,7 @@ namespace LiamAD string managedByDn = k.Properties["managedBy"][0].ToString(); // Erstellen eines DirectoryEntry-Objekts für den managedBy-DN - using (DirectoryEntry managedByEntry = new DirectoryEntry($"LDAP://{managedByDn}")) + using (DirectoryEntry managedByEntry = new DirectoryEntry($"LDAP://{GetAdServer()}/{managedByDn}", privLogonInfo.User, new NetworkCredential("", privLogonInfo.UserSecret).Password, AuthenticationTypes.Secure | AuthenticationTypes.Sealing)) { if (managedByEntry.Properties.Contains("objectSid") && managedByEntry.Properties["objectSid"].Count > 0) { @@ -513,4 +591,4 @@ namespace LiamAD return null; } } -} \ No newline at end of file +} diff --git a/Sonstiges/LIAM_NTFS_AdditionalConfiguration_BlackWhitelist_Konzept.md b/Sonstiges/LIAM_NTFS_AdditionalConfiguration_BlackWhitelist_Konzept.md index 495ff0c..13aa969 100644 --- a/Sonstiges/LIAM_NTFS_AdditionalConfiguration_BlackWhitelist_Konzept.md +++ b/Sonstiges/LIAM_NTFS_AdditionalConfiguration_BlackWhitelist_Konzept.md @@ -287,6 +287,14 @@ 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. +Die gleiche Logik gilt fuer den ActiveDirectory-Provider. Dort koennen die DCs ueber `AdDomainControllers` oder alternativ `ActiveDirectoryDomainControllers` gesetzt werden: + +```text +AdDomainControllers=dc01.imagoverum.com,dc02.imagoverum.com +``` + +Auch hier wird ohne Konfiguration automatisch der PDC Emulator verwendet. Der tatsaechlich verwendete DC wird beim AD-Logon im Debug-Log ausgegeben. + ## Matching-Regeln Empfohlene Semantik: