using System; using System.DirectoryServices; using System.Linq; using System.Threading; using C4IT.Logging; using C4IT.LIAM; using LiamAD; using System.Collections.Generic; using System.Security.Principal; using System.Text; namespace LiamAD { /// /// Helfer für cLiamProviderAD: Erstellt AD Member- und Owner-Gruppen für Services /// nach konfigurierter Namenskonvention und setzt ManagedBy. /// public class ADServiceGroupCreator { private readonly cLiamProviderAD _provider; private readonly cActiveDirectoryBase _adBase; private readonly string _ldapRoot; private readonly string _user; private readonly string _password; public enum ADGroupType { Security, // Sicherheit Distribution // Verteiler } public ADServiceGroupCreator(cLiamProviderAD provider) { _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; } /// /// Erstellt oder findet beide AD-Gruppen (Member & Owner) für einen Service. /// Neu mit: gruppenbereich (Scope) und gruppentyp (für Member-Gruppe). /// Owner-Gruppe ist immer Security. /// public List> EnsureServiceGroups( string serviceName, string description = null, eLiamAccessRoleScopes gruppenbereich = eLiamAccessRoleScopes.Universal, ADGroupType gruppentyp = ADGroupType.Distribution, IEnumerable ownerSidList = null, IEnumerable memberSidList = null) { const int MaxLoop = 50; var result = new List>(); // Konventionen für Member und Owner var ownerConv = _provider.NamingConventions .FirstOrDefault(nc => nc.AccessRole == eLiamAccessRoles.ADOwner); var memberConv = _provider.NamingConventions .FirstOrDefault(nc => nc.AccessRole == eLiamAccessRoles.ADMember); if (ownerConv == null || memberConv == null) throw new InvalidOperationException("Namenskonvention für ADMember oder ADOwner fehlt."); // Tags _provider.CustomTags.TryGetValue("ADGroupPrefix", out var prefix); _provider.CustomTags.TryGetValue("ADOwner", out var ownerPostfix); _provider.CustomTags.TryGetValue("ADMember", out var memberPostfix); // 1) Owner-Gruppe (immer Security) string ownerName = null; for (int loop = 0; loop <= MaxLoop; loop++) { string loopPart = loop > 0 ? "_" + loop : string.Empty; ownerName = ownerConv.NamingTemplate .Replace("{{ADGroupPrefix}}", prefix ?? string.Empty) .Replace("{{NAME}}", serviceName) .Replace("{{_LOOP}}", loopPart) .Replace("{{GROUPTYPEPOSTFIX}}", ownerPostfix); if (!GroupExists(ownerName)) break; if (loop == MaxLoop) throw new InvalidOperationException($"Kein eindeutiger Owner-Name für '{serviceName}' nach {MaxLoop} Versuchen."); } EnsureGroup(ownerName, ownerConv, description, managedByDn: null, gruppenbereich, ADGroupType.Security); AddMembersBySid(ownerName, ownerSidList); // NEU: SIDs als Owner hinzufügen var ownerDn = GetDistinguishedName(ownerName); var ownerSid = GetSid(ownerName); result.Add(Tuple.Create(eLiamAccessRoles.ADOwner.ToString(), ownerSid, ownerName, ownerDn)); // 2) Member-Gruppe (Gruppentyp nach Parameter) string memberName = null; for (int loop = 0; loop <= MaxLoop; loop++) { string loopPart = loop > 0 ? "_" + loop : string.Empty; memberName = memberConv.NamingTemplate .Replace("{{ADGroupPrefix}}", prefix ?? string.Empty) .Replace("{{NAME}}", serviceName) .Replace("{{_LOOP}}", loopPart) .Replace("{{GROUPTYPEPOSTFIX}}", memberPostfix); if (!GroupExists(memberName)) break; if (loop == MaxLoop) throw new InvalidOperationException($"Kein eindeutiger Member-Name für '{serviceName}' nach {MaxLoop} Versuchen."); } EnsureGroup(memberName, memberConv, description, managedByDn: ownerDn, gruppenbereich, gruppentyp); AddMembersBySid(memberName, memberSidList); // NEU: SIDs als Member hinzufügen var memberDn = GetDistinguishedName(memberName); var memberSid = GetSid(memberName); result.Add(Tuple.Create(eLiamAccessRoles.ADMember.ToString(), memberSid, memberName, memberDn)); return result; } /// /// Fügt einer bestehenden Gruppe per SID die entsprechenden AD-Objekte hinzu. /// private void AddMembersBySid(string groupName, IEnumerable sidList) { if (sidList == null) return; // Basis für die Suche: komplette Domäne, nicht nur der OU-Pfad string domainRoot = $"LDAP://{_provider.Domain}"; using (var root = new DirectoryEntry(domainRoot, _user, _password, AuthenticationTypes.Secure)) using (var grpSearch = new DirectorySearcher(root)) { grpSearch.Filter = $"(&(objectCategory=group)(sAMAccountName={groupName}))"; var grpRes = grpSearch.FindOne(); if (grpRes == null) return; var grpEntry = grpRes.GetDirectoryEntry(); foreach (var sidStr in sidList) { // Leere oder null überspringen if (string.IsNullOrWhiteSpace(sidStr)) continue; SecurityIdentifier sid; try { sid = new SecurityIdentifier(sidStr); } catch (Exception) { // Ungültige SID-String-Darstellung überspringen continue; } // In LDAP-Filter-Notation umwandeln var bytes = new byte[sid.BinaryLength]; sid.GetBinaryForm(bytes, 0); var sb = new StringBuilder(); foreach (var b in bytes) sb.AppendFormat("\\{0:X2}", b); string octetSid = sb.ToString(); // Suche nach dem Objekt in der Domäne using (var usrSearch = new DirectorySearcher(root)) { usrSearch.Filter = $"(objectSid={octetSid})"; var usrRes = usrSearch.FindOne(); if (usrRes == null) continue; var userDn = usrRes.Properties["distinguishedName"][0].ToString(); // Doppelteinträge vermeiden if (!grpEntry.Properties["member"].Contains(userDn)) grpEntry.Properties["member"].Add(userDn); } } grpEntry.CommitChanges(); } } /// /// Wandelt eine SID (String-Form) in das für LDAP nötige Oktet-String-Format um. /// private string SidStringToLdapFilter(string sidString) { var sid = new SecurityIdentifier(sidString); var bytes = new byte[sid.BinaryLength]; sid.GetBinaryForm(bytes, 0); var sb = new StringBuilder(); foreach (var b in bytes) sb.AppendFormat("\\{0:X2}", b); return sb.ToString(); } private string GetSid(string name) { using (var root = new DirectoryEntry(_ldapRoot, _user, _password, AuthenticationTypes.Secure)) using (var ds = new DirectorySearcher(root)) { ds.Filter = $"(&(objectCategory=group)(sAMAccountName={name}))"; var r = ds.FindOne(); if (r == null) return null; var de = r.GetDirectoryEntry(); var sidBytes = (byte[])de.Properties["objectSid"][0]; return new SecurityIdentifier(sidBytes, 0).Value; } } private string FormatName(cLiamNamingConvention conv, string serviceName, System.Collections.Generic.IDictionary tags) { string tmpl = conv.NamingTemplate.Replace("{{NAME}}", serviceName); foreach (var kv in tags) tmpl = tmpl.Replace("{{" + kv.Key + "}}", kv.Value); return tmpl; } /// /// Stellt sicher, dass die Gruppe existiert – neu mit Scope & Type. /// private void EnsureGroup( string groupName, cLiamNamingConvention conv, string description, string managedByDn, eLiamAccessRoleScopes groupScope, ADGroupType groupType) { if (!GroupExists(groupName)) { using (var root = new DirectoryEntry(_ldapRoot, _user, _password, AuthenticationTypes.Secure)) { var grp = root.Children.Add("CN=" + groupName, "group"); grp.Properties["sAMAccountName"].Value = groupName; grp.Properties["displayName"].Value = groupName; // Hier: Security-Bit (0x80000000) nur, wenn Security, sonst 0 int typeBit = (groupType == ADGroupType.Security) ? unchecked((int)0x80000000) : 0; // Scope-Bit aus Param grp.Properties["groupType"].Value = unchecked(typeBit | GetScopeBit(groupScope)); if (!string.IsNullOrEmpty(description)) grp.Properties["description"].Value = description; if (managedByDn != null) grp.Properties["managedBy"].Value = managedByDn; grp.CommitChanges(); } WaitReplication(groupName, TimeSpan.FromMinutes(2)); } } private bool GroupExists(string name) { return _adBase.directoryEntry.Children.Cast() .Any(c => string.Equals( c.Properties["sAMAccountName"]?.Value?.ToString(), name, StringComparison.OrdinalIgnoreCase)); } private void WaitReplication(string groupName, TimeSpan timeout) { var sw = System.Diagnostics.Stopwatch.StartNew(); while (sw.Elapsed < timeout) { if (GroupExists(groupName)) return; Thread.Sleep(2000); } } private string GetDistinguishedName(string name) { using (var root = new DirectoryEntry(_ldapRoot, _user, _password, AuthenticationTypes.Secure)) using (var ds = new DirectorySearcher(root)) { ds.Filter = "(&(objectClass=group)(sAMAccountName=" + name + "))"; var res = ds.FindOne(); return res?.Properties["distinguishedName"]?[0]?.ToString(); } } private int GetScopeBit(eLiamAccessRoleScopes scope) { switch (scope) { case eLiamAccessRoleScopes.Universal: return 0x8; case eLiamAccessRoleScopes.Global: return 0x2; case eLiamAccessRoleScopes.DomainLocal: return 0x4; default: return 0x8; } } } }