Pin NTFS AD operations to domain controller

This commit is contained in:
Meik
2026-05-19 19:12:33 +02:00
parent ea0517d1dc
commit 0cad46ddef
6 changed files with 119 additions and 13 deletions

View File

@@ -58,6 +58,7 @@ namespace C4IT.LIAM
private const string AdditionalConfigurationTraverseBoundaryPathKey = "NtfsTraverseBoundaryPath"; private const string AdditionalConfigurationTraverseBoundaryPathKey = "NtfsTraverseBoundaryPath";
private const string AdditionalConfigurationGroupNameSanitizeReplacementKey = "NtfsGroupNameSanitizeReplacement"; private const string AdditionalConfigurationGroupNameSanitizeReplacementKey = "NtfsGroupNameSanitizeReplacement";
private const string AdditionalConfigurationPreserveAdGroupNameCaseKey = "PreserveNtfsAdGroupNameCase"; private const string AdditionalConfigurationPreserveAdGroupNameCaseKey = "PreserveNtfsAdGroupNameCase";
private const string AdditionalConfigurationAdDomainControllersKey = "NtfsAdDomainControllers";
public readonly cNtfsBase ntfsBase = new cNtfsBase(); public readonly cNtfsBase ntfsBase = new cNtfsBase();
public readonly cActiveDirectoryBase activeDirectoryBase = new cActiveDirectoryBase(); public readonly cActiveDirectoryBase activeDirectoryBase = new cActiveDirectoryBase();
private readonly Dictionary<string, HashSet<string>> publishedShareCache = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, HashSet<string>> publishedShareCache = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
@@ -137,6 +138,7 @@ namespace C4IT.LIAM
var LI = new cNtfsLogonInfo() var LI = new cNtfsLogonInfo()
{ {
Domain = Domain, Domain = Domain,
DomainControllers = GetAdditionalConfigurationValue(AdditionalConfigurationAdDomainControllersKey),
User = Credential?.Identification, User = Credential?.Identification,
UserSecret = Credential?.Secret, UserSecret = Credential?.Secret,
TargetNetworkName = RootPath, TargetNetworkName = RootPath,
@@ -980,6 +982,7 @@ namespace C4IT.LIAM
{ {
ConfigID = "manual", ConfigID = "manual",
domainName = this.Domain, domainName = this.Domain,
effectiveDomainController = activeDirectoryBase.EffectiveDomainController,
username = this.Credential.Identification, username = this.Credential.Identification,
password = new NetworkCredential("", this.Credential.Secret).SecurePassword, password = new NetworkCredential("", this.Credential.Secret).SecurePassword,
baseFolder = this.RootPath, baseFolder = this.RootPath,

View File

@@ -27,6 +27,7 @@ namespace C4IT_IAM_SET
public const string constApplicationDataPath = "%ProgramData%\\Consulting4IT GmbH\\LIAM"; public const string constApplicationDataPath = "%ProgramData%\\Consulting4IT GmbH\\LIAM";
public string domainName; public string domainName;
public string effectiveDomainController;
public string username; public string username;
public SecureString password; public SecureString password;
private cNetworkConnection Connection; private cNetworkConnection Connection;
@@ -87,6 +88,11 @@ namespace C4IT_IAM_SET
templates = new List<IAM_SecurityGroupTemplate>(); templates = new List<IAM_SecurityGroupTemplate>();
} }
private string GetAdServer()
{
return string.IsNullOrWhiteSpace(effectiveDomainController) ? domainName : effectiveDomainController;
}
private ResultToken checkRequiredVariables() private ResultToken checkRequiredVariables()
{ {
ResultToken resultToken = new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString()); ResultToken resultToken = new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString());
@@ -302,6 +308,7 @@ namespace C4IT_IAM_SET
{ {
username = username, username = username,
domainName = domainName, domainName = domainName,
effectiveDomainController = effectiveDomainController,
password = password, password = password,
ForceStrictAdGroupNames = forceStrictAdGroupNames, ForceStrictAdGroupNames = forceStrictAdGroupNames,
PreserveAdGroupNameCase = preserveAdGroupNameCase PreserveAdGroupNameCase = preserveAdGroupNameCase
@@ -441,7 +448,7 @@ namespace C4IT_IAM_SET
return resultToken; 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."); DefaultLogger.LogEntry(LogLevels.Debug, "PrincipalContext erfolgreich erstellt.");
// Überprüfen von newDataArea und IAM_Folders // Überprüfen von newDataArea und IAM_Folders
@@ -1005,7 +1012,7 @@ namespace C4IT_IAM_SET
return null; return null;
} }
var basePath = "LDAP://" + domainName; var basePath = "LDAP://" + GetAdServer();
if (!string.IsNullOrWhiteSpace(groupOUPath)) if (!string.IsNullOrWhiteSpace(groupOUPath))
basePath += "/" + groupOUPath; basePath += "/" + groupOUPath;
@@ -1213,6 +1220,7 @@ namespace C4IT_IAM_SET
{ {
ResultToken resultToken = new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString()); ResultToken resultToken = new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString());
resultToken.resultErrorId = 0; resultToken.resultErrorId = 0;
newSecurityGroups.effectiveDomainController = effectiveDomainController;
if (Directory.Exists(newDataArea.IAM_Folders[0].technicalName)) if (Directory.Exists(newDataArea.IAM_Folders[0].technicalName))
{ {
resultToken.resultMessage = "New folder " + newDataArea.IAM_Folders[0].technicalName + " already exists"; resultToken.resultMessage = "New folder " + newDataArea.IAM_Folders[0].technicalName + " already exists";
@@ -1602,7 +1610,7 @@ namespace C4IT_IAM_SET
try 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; UserPrincipal user;
user = UserPrincipal.FindByIdentity(ctx, IdentityType.Sid, (sid)); user = UserPrincipal.FindByIdentity(ctx, IdentityType.Sid, (sid));
return user; return user;

View File

@@ -21,6 +21,7 @@ namespace C4IT_IAM_Engine
public class SecurityGroups public class SecurityGroups
{ {
public string domainName; public string domainName;
public string effectiveDomainController;
public string username; public string username;
public SecureString password; public SecureString password;
public bool ForceStrictAdGroupNames; public bool ForceStrictAdGroupNames;
@@ -32,6 +33,11 @@ namespace C4IT_IAM_Engine
{ {
IAM_SecurityGroups = new List<IAM_SecurityGroup>(); IAM_SecurityGroups = new List<IAM_SecurityGroup>();
} }
private string GetLdapServer()
{
return string.IsNullOrWhiteSpace(effectiveDomainController) ? domainName : effectiveDomainController;
}
public bool GroupsAllreadyExisting(string ouPath) public bool GroupsAllreadyExisting(string ouPath)
{ {
LogMethodBegin(MethodBase.GetCurrentMethod()); LogMethodBegin(MethodBase.GetCurrentMethod());
@@ -47,7 +53,7 @@ namespace C4IT_IAM_Engine
{ {
DirectoryEntry entry = new DirectoryEntry DirectoryEntry entry = new DirectoryEntry
{ {
Path = "LDAP://" + domainName, Path = "LDAP://" + GetLdapServer(),
Username = username, Username = username,
Password = new NetworkCredential("", password).Password, Password = new NetworkCredential("", password).Password,
AuthenticationType = AuthenticationTypes.Secure | AuthenticationTypes.Sealing AuthenticationType = AuthenticationTypes.Secure | AuthenticationTypes.Sealing
@@ -86,7 +92,7 @@ namespace C4IT_IAM_Engine
{ {
DirectoryEntry entry = new DirectoryEntry DirectoryEntry entry = new DirectoryEntry
{ {
Path = "LDAP://" + domainName, Path = "LDAP://" + GetLdapServer(),
Username = username, Username = username,
Password = new NetworkCredential("", password).Password, Password = new NetworkCredential("", password).Password,
AuthenticationType = AuthenticationTypes.Secure | AuthenticationTypes.Sealing AuthenticationType = AuthenticationTypes.Secure | AuthenticationTypes.Sealing
@@ -431,7 +437,7 @@ namespace C4IT_IAM_Engine
{ {
DirectoryEntry entry = new DirectoryEntry DirectoryEntry entry = new DirectoryEntry
{ {
Path = "LDAP://" + domainName, Path = "LDAP://" + GetLdapServer(),
Username = username, Username = username,
Password = new NetworkCredential("", password).Password, Password = new NetworkCredential("", password).Password,
AuthenticationType = AuthenticationTypes.Secure | AuthenticationTypes.Sealing AuthenticationType = AuthenticationTypes.Secure | AuthenticationTypes.Sealing
@@ -473,7 +479,7 @@ namespace C4IT_IAM_Engine
return null; return null;
} }
var basePath = "LDAP://" + domainName; var basePath = "LDAP://" + GetLdapServer();
if (!string.IsNullOrWhiteSpace(ouPath)) if (!string.IsNullOrWhiteSpace(ouPath))
basePath += "/" + ouPath; basePath += "/" + ouPath;
@@ -528,7 +534,7 @@ namespace C4IT_IAM_Engine
return null; return null;
DefaultLogger.LogEntry(LogLevels.Debug, $"Reusing existing AD group '{matchedName}' via wildcard '{wildcardPattern}'."); 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) private DirectoryEntry FindGroupEntryFromFolderAcl(string folderPath, string wildcardPattern)
@@ -555,7 +561,7 @@ namespace C4IT_IAM_Engine
.Cast<FileSystemAccessRule>(); .Cast<FileSystemAccessRule>();
var matchedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var matchedNames = new HashSet<string>(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) foreach (var rule in rules)
{ {
@@ -735,7 +741,7 @@ namespace C4IT_IAM_Engine
if (!GroupAllreadyExisting(groupName)) 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}"); DefaultLogger.LogEntry(LogLevels.Debug, $"Creating ad entry with CN / sAmAccountName: {groupName}");
DirectoryEntry group = entry.Children.Add("CN=" + groupName, "group"); DirectoryEntry group = entry.Children.Add("CN=" + groupName, "group");
group.Properties["sAmAccountName"].Value = groupName; group.Properties["sAmAccountName"].Value = groupName;
@@ -763,7 +769,7 @@ namespace C4IT_IAM_Engine
} }
group.CommitChanges(); 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); var objectid = SecurityGroups.getSID(ent);
DefaultLogger.LogEntry(LogLevels.Debug, $"Security group created in ad: {secGroup.technicalName}"); DefaultLogger.LogEntry(LogLevels.Debug, $"Security group created in ad: {secGroup.technicalName}");

View File

@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.DirectoryServices; using System.DirectoryServices;
using System.DirectoryServices.ActiveDirectory;
using System.DirectoryServices.AccountManagement; using System.DirectoryServices.AccountManagement;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@@ -24,6 +25,7 @@ namespace LiamNtfs
private cNtfsLogonInfo privLogonInfo = null; private cNtfsLogonInfo privLogonInfo = null;
public PrincipalContext adContext = null; public PrincipalContext adContext = null;
public DirectoryEntry directoryEntry = null; public DirectoryEntry directoryEntry = null;
public string EffectiveDomainController { get; private set; } = null;
public Exception LastException { get; private set; } = null; public Exception LastException { get; private set; } = null;
public string LastErrorMessage { get; private set; } = null; public string LastErrorMessage { get; private set; } = null;
@@ -49,8 +51,12 @@ namespace LiamNtfs
//TODO: remove dummy delay? //TODO: remove dummy delay?
await Task.Delay(0); await Task.Delay(0);
ResetError(); ResetError();
adContext = new PrincipalContext(ContextType.Domain, LogonInfo.Domain, LogonInfo.User, new NetworkCredential("", LogonInfo.UserSecret).Password); var adServer = ResolveEffectiveDomainController(LogonInfo);
var ldapPath = $"LDAP://{LogonInfo.Domain}/{LogonInfo.TargetGroupPath}"; 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 directoryEntry = new DirectoryEntry
{ {
Path = ldapPath, Path = ldapPath,
@@ -69,6 +75,70 @@ namespace LiamNtfs
return false; 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<string> ParseDomainControllers(string domainControllers)
{
if (string.IsNullOrWhiteSpace(domainControllers))
return Enumerable.Empty<string>();
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<bool> privRelogon() private async Task<bool> privRelogon()
{ {
if (privLogonInfo == null) if (privLogonInfo == null)

View File

@@ -334,6 +334,7 @@ namespace LiamNtfs
public class cNtfsLogonInfo public class cNtfsLogonInfo
{ {
public string Domain; public string Domain;
public string DomainControllers;
public string User; public string User;
public string UserSecret; public string UserSecret;
public string TargetNetworkName; public string TargetNetworkName;

View File

@@ -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. `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 ## Matching-Regeln
Empfohlene Semantik: Empfohlene Semantik: