6 Commits
v3.3.1 ... main

Author SHA1 Message Date
Meik
8394d1e67e Fix README table pipe escaping 2026-05-19 20:39:08 +02:00
Meik
0c16e8a5b1 Document additional provider parameters 2026-05-19 20:35:51 +02:00
Meik
a9b4cfe10b Pin Active Directory provider to domain controller 2026-05-19 20:01:52 +02:00
Meik
723eae1018 Log NTFS ACL naming matches 2026-05-19 19:20:25 +02:00
Meik
0cad46ddef Pin NTFS AD operations to domain controller 2026-05-19 19:12:33 +02:00
Meik
ea0517d1dc Select Consulting4IT signing certificate 2026-05-12 13:03:12 +02:00
13 changed files with 406 additions and 44 deletions

View File

@@ -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<List<cLiamDataAreaBase>> getDataAreasAsync(int Depth = -1)
{
var CM = MethodBase.GetCurrentMethod();

View File

@@ -52,6 +52,7 @@ namespace LiamAD
public class cADLogonInfo
{
public string Domain;
public string DomainControllers;
public string User;
public string UserSecret;
public string TargetGroupPath;

View File

@@ -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()}";
}
/// <summary>
/// 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 + "))";

View File

@@ -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<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, 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<bool> 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)
{

View File

@@ -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<string, HashSet<string>> publishedShareCache = new Dictionary<string, HashSet<string>>(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,
@@ -1347,72 +1350,110 @@ namespace C4IT.LIAM
var traverseNamingConvention = Provider.NamingConventions.First(i => i.AccessRole == eLiamAccessRoles.Traverse);
foreach (FileSystemAccessRule rule in ACLs)
{
if (rule.IdentityReference.Value == "S-1-1-0")
var aclSid = rule.IdentityReference.Value;
if (aclSid == "S-1-1-0")
continue;
GroupPrincipal grp = GroupPrincipal.FindByIdentity(Provider.activeDirectoryBase.adContext, IdentityType.Sid, rule.IdentityReference.Value);
GroupPrincipal grp = GroupPrincipal.FindByIdentity(Provider.activeDirectoryBase.adContext, IdentityType.Sid, aclSid);
if (grp == null)
continue;
DefaultLogger.LogEntry(LogLevels.Debug, $"Try matching: {grp.Name}");
if (Regex.IsMatch(grp.SamAccountName, ownerNamingConvention.Wildcard, RegexOptions.IgnoreCase))
{
this.OwnerGroupIdentifier = rule.IdentityReference.Value;
DefaultLogger.LogEntry(LogLevels.Debug, $"ACL SID '{aclSid}' on '{path}' could not be resolved to an AD group. Naming convention matching skipped.");
continue;
}
var samAccountName = grp.SamAccountName ?? string.Empty;
if (string.IsNullOrWhiteSpace(samAccountName))
{
DefaultLogger.LogEntry(LogLevels.Debug, $"ACL SID '{aclSid}' on '{path}' resolved to '{grp.Name}', but no sAMAccountName is available. Naming convention matching skipped.");
continue;
}
if (Regex.IsMatch(samAccountName, ownerNamingConvention.Wildcard, RegexOptions.IgnoreCase))
{
this.OwnerGroupIdentifier = aclSid;
DefaultLogger.LogEntry(LogLevels.Debug, $"ACL SID '{aclSid}' on '{path}' resolved to '{samAccountName}' and matched Owner naming convention '{ownerNamingConvention.Wildcard}'.");
if (Provider.GroupStrategy == eLiamGroupStrategies.Ntfs_AGDLP)
{
var ldapFilter = String.Format("memberOf={0}", grp.DistinguishedName);
var res = await Provider.activeDirectoryBase.RequestSecurityGroupsListAsync(ldapFilter);
var ownerNamingConventionGlobal = Provider.NamingConventions.First(i => i.AccessRole == eLiamAccessRoles.Owner && i.Scope == eLiamAccessRoleScopes.Global);
var matchedGlobalGroup = false;
foreach (var memberItem in res)
foreach (var memberItem in res ?? new cADCollectionBase())
{
var SecurityGroup = new cLiamAdGroup(this.Provider, (cSecurityGroupResult)memberItem.Value);
if (Regex.IsMatch(SecurityGroup.TechnicalName, ownerNamingConventionGlobal.Wildcard, RegexOptions.IgnoreCase))
{
this.OwnerGroupIdentifier = SecurityGroup.UID;
matchedGlobalGroup = true;
DefaultLogger.LogEntry(LogLevels.Debug, $"AGDLP Owner ACL group '{samAccountName}' resolved to global group '{SecurityGroup.TechnicalName}' with SID '{SecurityGroup.UID}'.");
}
}
if (!matchedGlobalGroup)
DefaultLogger.LogEntry(LogLevels.Debug, $"AGDLP Owner ACL group '{samAccountName}' matched, but no nested global group matched naming convention '{ownerNamingConventionGlobal.Wildcard}'. Keeping ACL SID '{aclSid}'.");
}
}
else if (Regex.IsMatch(grp.SamAccountName, writeNamingConvention.Wildcard, RegexOptions.IgnoreCase))
else if (Regex.IsMatch(samAccountName, writeNamingConvention.Wildcard, RegexOptions.IgnoreCase))
{
this.WriteGroupIdentifier = rule.IdentityReference.Value;
this.WriteGroupIdentifier = aclSid;
DefaultLogger.LogEntry(LogLevels.Debug, $"ACL SID '{aclSid}' on '{path}' resolved to '{samAccountName}' and matched Write naming convention '{writeNamingConvention.Wildcard}'.");
if (Provider.GroupStrategy == eLiamGroupStrategies.Ntfs_AGDLP)
{
var ldapFilter = String.Format("memberOf={0}", grp.DistinguishedName);
var res = await Provider.activeDirectoryBase.RequestSecurityGroupsListAsync(ldapFilter);
var writeNamingConventionGlobal = Provider.NamingConventions.First(i => i.AccessRole == eLiamAccessRoles.Write && i.Scope == eLiamAccessRoleScopes.Global);
var matchedGlobalGroup = false;
foreach (var memberItem in res)
foreach (var memberItem in res ?? new cADCollectionBase())
{
var SecurityGroup = new cLiamAdGroup(this.Provider, (cSecurityGroupResult)memberItem.Value);
if (Regex.IsMatch(SecurityGroup.TechnicalName, writeNamingConventionGlobal.Wildcard, RegexOptions.IgnoreCase))
{
this.WriteGroupIdentifier = SecurityGroup.UID;
matchedGlobalGroup = true;
DefaultLogger.LogEntry(LogLevels.Debug, $"AGDLP Write ACL group '{samAccountName}' resolved to global group '{SecurityGroup.TechnicalName}' with SID '{SecurityGroup.UID}'.");
}
}
if (!matchedGlobalGroup)
DefaultLogger.LogEntry(LogLevels.Debug, $"AGDLP Write ACL group '{samAccountName}' matched, but no nested global group matched naming convention '{writeNamingConventionGlobal.Wildcard}'. Keeping ACL SID '{aclSid}'.");
}
}
else if (Regex.IsMatch(grp.SamAccountName, readNamingConvention.Wildcard, RegexOptions.IgnoreCase))
else if (Regex.IsMatch(samAccountName, readNamingConvention.Wildcard, RegexOptions.IgnoreCase))
{
this.ReadGroupIdentifier = rule.IdentityReference.Value;
this.ReadGroupIdentifier = aclSid;
DefaultLogger.LogEntry(LogLevels.Debug, $"ACL SID '{aclSid}' on '{path}' resolved to '{samAccountName}' and matched Read naming convention '{readNamingConvention.Wildcard}'.");
if (Provider.GroupStrategy == eLiamGroupStrategies.Ntfs_AGDLP)
{
var ldapFilter = String.Format("memberOf={0}", grp.DistinguishedName);
var res = await Provider.activeDirectoryBase.RequestSecurityGroupsListAsync(ldapFilter);
var readNamingConventionGlobal = Provider.NamingConventions.First(i => i.AccessRole == eLiamAccessRoles.Read && i.Scope == eLiamAccessRoleScopes.Global);
var matchedGlobalGroup = false;
foreach (var memberItem in res)
foreach (var memberItem in res ?? new cADCollectionBase())
{
var SecurityGroup = new cLiamAdGroup(this.Provider, (cSecurityGroupResult)memberItem.Value);
if (Regex.IsMatch(SecurityGroup.TechnicalName, readNamingConventionGlobal.Wildcard, RegexOptions.IgnoreCase))
{
this.ReadGroupIdentifier = SecurityGroup.UID;
matchedGlobalGroup = true;
DefaultLogger.LogEntry(LogLevels.Debug, $"AGDLP Read ACL group '{samAccountName}' resolved to global group '{SecurityGroup.TechnicalName}' with SID '{SecurityGroup.UID}'.");
}
}
if (!matchedGlobalGroup)
DefaultLogger.LogEntry(LogLevels.Debug, $"AGDLP Read ACL group '{samAccountName}' matched, but no nested global group matched naming convention '{readNamingConventionGlobal.Wildcard}'. Keeping ACL SID '{aclSid}'.");
}
}
else if (Regex.IsMatch(grp.SamAccountName, traverseNamingConvention.Wildcard, RegexOptions.IgnoreCase))
else if (Regex.IsMatch(samAccountName, traverseNamingConvention.Wildcard, RegexOptions.IgnoreCase))
{
this.TraverseGroupIdentifier = rule.IdentityReference.Value;
this.TraverseGroupIdentifier = aclSid;
DefaultLogger.LogEntry(LogLevels.Debug, $"ACL SID '{aclSid}' on '{path}' resolved to '{samAccountName}' and matched Traverse naming convention '{traverseNamingConvention.Wildcard}'.");
}
else
{
DefaultLogger.LogEntry(LogLevels.Debug, $"No match for: {grp.Name}");
DefaultLogger.LogEntry(LogLevels.Debug, $"ACL SID '{aclSid}' on '{path}' resolved to '{samAccountName}', but did not match Owner/Write/Read/Traverse naming conventions.");
}
}
}

View File

@@ -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<IAM_SecurityGroupTemplate>();
}
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;

View File

@@ -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<IAM_SecurityGroup>();
}
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<FileSystemAccessRule>();
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)
{
@@ -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}");

View File

@@ -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<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()
{
if (privLogonInfo == null)

View File

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

View File

@@ -4,6 +4,12 @@ setlocal EnableDelayedExpansion
set "ProductName=C4IT Light Identity Access Management"
set "SignTool=..\..\Common Code\Tools\signtool.exe"
set "TimeStamp=http://rfc3161timestamp.globalsign.com/advanced"
set "SignCertificateSubject=Consulting4IT GmbH"
set "SignCertificateEmail=info@consulting4it.de"
if defined LIAM_SIGN_CERT_SUBJECT (
set "SignCertificateSubject=%LIAM_SIGN_CERT_SUBJECT%"
)
REM Alle passenden Dateien in einer Variablen sammeln
set "FileList="
@@ -13,6 +19,11 @@ for %%F in (".\bin\Release\Liam*.dll") do (
REM SignTool mit allen gesammelten Dateien aufrufen
echo Signing all matching files at once...
call "%SignTool%" sign /a /tr %TimeStamp% /td SHA256 /fd SHA256 /d "%ProductName%" !FileList!
echo Expected signer: %SignCertificateSubject% ^<%SignCertificateEmail%^>
if defined LIAM_SIGN_CERT_THUMBPRINT (
call "%SignTool%" sign /sha1 "%LIAM_SIGN_CERT_THUMBPRINT%" /tr %TimeStamp% /td SHA256 /fd SHA256 /d "%ProductName%" !FileList!
) else (
call "%SignTool%" sign /n "%SignCertificateSubject%" /tr %TimeStamp% /td SHA256 /fd SHA256 /d "%ProductName%" !FileList!
)
pause

View File

@@ -4,6 +4,12 @@ setlocal EnableDelayedExpansion
set "ProductName=C4IT Light Identity Access Management"
set "SignTool=..\..\Common Code\Tools\signtool.exe"
set "TimeStamp=http://rfc3161timestamp.globalsign.com/advanced"
set "SignCertificateSubject=Consulting4IT GmbH"
set "SignCertificateEmail=info@consulting4it.de"
if defined LIAM_SIGN_CERT_SUBJECT (
set "SignCertificateSubject=%LIAM_SIGN_CERT_SUBJECT%"
)
set "FileList="
@@ -24,6 +30,11 @@ if not defined FileList (
)
echo Signing all matching files at once...
call "%SignTool%" sign /a /tr %TimeStamp% /td SHA256 /fd SHA256 /d "%ProductName%" !FileList!
echo Expected signer: %SignCertificateSubject% ^<%SignCertificateEmail%^>
if defined LIAM_SIGN_CERT_THUMBPRINT (
call "%SignTool%" sign /sha1 "%LIAM_SIGN_CERT_THUMBPRINT%" /tr %TimeStamp% /td SHA256 /fd SHA256 /d "%ProductName%" !FileList!
) else (
call "%SignTool%" sign /n "%SignCertificateSubject%" /tr %TimeStamp% /td SHA256 /fd SHA256 /d "%ProductName%" !FileList!
)
pause

73
README.md Normal file
View File

@@ -0,0 +1,73 @@
# LIAM
## AdditionalParameters / AdditionalConfiguration
Provider-spezifische Zusatzparameter werden in Matrix42 am DataArea-Collector ueber das Fragment `C4IT_GCC_DataArea_Collector_AdditionalAttributes` gepflegt. Pro Parameter wird ein Eintrag mit `Name` und `Value` angelegt.
Beispiel:
| Name | Value |
| --- | --- |
| `EnsureNtfsPermissionGroups` | `1` |
| `NtfsAdDomainControllers` | `dc01.contoso.local,dc02.contoso.local` |
Im Diagnose-JSON erscheinen diese Werte unter `AdditionalConfiguration`. Parameternamen werden ohne Beachtung der Gross-/Kleinschreibung gelesen. Boolean-Werte sind in der Regel aktiv, wenn der Wert `true`, `1` oder `yes` ist. Leere oder nicht vorhandene Werte deaktivieren den jeweiligen Schalter, sofern unten nichts anderes beschrieben ist.
### Allgemein
| Parameter | Provider | Werte | Wirkung |
| --- | --- | --- | --- |
| `WhatIf` | Workflow / alle Provider, soweit unterstuetzt | `true`, `1`, `yes` | Aktiviert den Simulationsmodus fuer Workflow-Pfade, die WhatIf unterstuetzen. Aktionen werden dann vorbereitet und protokolliert, aber nicht dauerhaft ausgefuehrt. |
### NTFS
| Parameter | Werte | Wirkung |
| --- | --- | --- |
| `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. |
| `AllowManualNtfsPermissionEnsureForShares` | `true`, `1`, `yes` | Erlaubt die manuelle Ensure-Aktivitaet auch fuer Share-DataAreas. |
| `NtfsIncludePaths` | Pfadliste, getrennt mit `;`, &#124; 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 `;`, &#124; 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. |
| `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. |
| `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:
```text
EnsureNtfsPermissionGroups=1
EnsureNtfsPermissionGroupsForShares=1
NtfsIncludePaths=Finance\*;HR\Reports
NtfsExcludePaths=*\_archive\*
NtfsTraverseBoundaryPath=\\fileserver\file_shares
NtfsGroupNameSanitizeReplacement=.
PreserveNtfsAdGroupNameCase=1
NtfsAdDomainControllers=dc01.contoso.local,dc02.contoso.local
```
### Active Directory
| Parameter | Werte | Wirkung |
| --- | --- | --- |
| `AdDomainControllers` | Kommagetrennte DC-Liste, z. B. `dc01.contoso.local,dc02.contoso.local` | Pinnt Active-Directory-Provider-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. |
| `ActiveDirectoryDomainControllers` | Kommagetrennte DC-Liste | Alias/Fallback fuer `AdDomainControllers`, wenn `AdDomainControllers` nicht gesetzt ist. |
Beispiel:
```text
AdDomainControllers=dc01.contoso.local,dc02.contoso.local
```
### Microsoft Teams
| Parameter | Werte | Wirkung |
| --- | --- | --- |
| `WithoutPrivateChannels` | `true`, `1` | Private Channels werden beim Teams-Provider nicht beruecksichtigt. |
### Hinweise zur Pfad- und Listen-Syntax
`NtfsIncludePaths` und `NtfsExcludePaths` verwenden `;`, &#124; oder Zeilenumbrueche als Trenner. Kommas sind dort Teil des Werts. Die Domain-Controller-Parameter verwenden dagegen eine kommagetrennte Prioritaetsliste.
Pfadfilter koennen relativ zum konfigurierten NTFS-RootPath oder als absolute UNC-Pfade angegeben werden. Einfache Wildcards mit `*` sind moeglich.

View File

@@ -269,6 +269,32 @@ 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.
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: