using System; using System.Linq; using System.Management.Automation; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Security; using System.DirectoryServices; using System.Diagnostics; using System.Threading; using System.Management.Automation.Runspaces; using System.Security.Principal; namespace C4IT.LIAM { public partial class ExchangeManager { /// /// Stellt sicher, dass eine AD-Sicherheitsgruppe für den angegebenen AccessRole existiert (erstellt sie falls nicht) /// und wartet optional, bis die Replikation abgeschlossen ist. /// Liefert den tatsächlichen Gruppennamen zurück. /// private string EnsureSecurityGroup(eLiamAccessRoles accessRole, string baseName) { const int MaxLoop = 50; // Abbruchbedingung: nach 50 Versuchen abbrechen // 1. Namenskonvention für diese Rolle finden var namingConvention = _provider.NamingConventions .FirstOrDefault(nc => nc.AccessRole == accessRole); if (namingConvention == null) throw new InvalidOperationException($"Keine Namenskonvention für Rolle '{accessRole}' gefunden."); // 2. Benötigte CustomTags aus dem Provider ziehen // - Prefix (z.B. "ACL") // - GROUPTYPEPOSTFIX (z.B. "ExchangeMLMember") _provider.CustomTags.TryGetValue("ADGroupPrefix", out var prefix); _provider.CustomTags.TryGetValue(accessRole.ToString(), out var typePostfix); // 3. Schleife für _LOOP hochzählen, bis ein einzigartiger Name gefunden ist string groupName = null; string description = null; for (int loop = 0; loop <= MaxLoop; loop++) { // nur einfügen, wenn loop > 0 var loopPart = loop > 0 ? $"_{loop}" : string.Empty; // Platzhalter im Template ersetzen groupName = namingConvention.NamingTemplate .Replace("{{ADGroupPrefix}}", prefix ?? string.Empty) .Replace("{{NAME}}", baseName) .Replace("{{_LOOP}}", loopPart) .Replace("{{GROUPTYPEPOSTFIX}}", typePostfix ?? string.Empty); description = namingConvention.DescriptionTemplate .Replace("{{ADGroupPrefix}}", prefix ?? string.Empty) .Replace("{{NAME}}", baseName) .Replace("{{_LOOP}}", loopPart) .Replace("{{GROUPTYPEPOSTFIX}}", typePostfix ?? string.Empty); // Existenz prüfen bool exists = GetSecurityGroups(groupName) .Any(g => string.Equals( g.Properties["sAMAccountName"]?.Value?.ToString(), groupName, StringComparison.OrdinalIgnoreCase)); if (!exists) break; // Name ist frei – raus aus der Schleife if (loop == MaxLoop) throw new InvalidOperationException( $"Konnte nach {MaxLoop} Versuchen keinen eindeutigen Gruppennamen für '{baseName}' erzeugen."); } // 4. Gruppen-Scope-Bit setzen int scopeBit; switch (namingConvention.Scope) { case eLiamAccessRoleScopes.Global: scopeBit = 0x2; break; case eLiamAccessRoleScopes.DomainLocal: scopeBit = 0x4; break; case eLiamAccessRoleScopes.Universal: scopeBit = 0x8; break; default: scopeBit = 0x8; break; } int groupType = unchecked((int)(0x80000000 | scopeBit)); // 5. Gruppe im AD anlegen string ldapPath = $"LDAP://{_organizationalUnit}"; string password = new System.Net.NetworkCredential(string.Empty, _credential.Password).Password; using (var root = new DirectoryEntry( ldapPath, _credential.UserName, password, AuthenticationTypes.Secure)) { var newGroup = root.Children.Add($"CN={groupName}", "group"); newGroup.Properties["sAMAccountName"].Value = groupName; newGroup.Properties["displayName"].Value = groupName; newGroup.Properties["groupType"].Value = groupType; if(!string.IsNullOrEmpty(description)) { newGroup.Properties["description"].Value = description; } newGroup.CommitChanges(); } // 6. Auf Replikation warten (optional) const int replicationTimeoutMinutes = 2; if (!WaitForGroupReplication(groupName, TimeSpan.FromMinutes(replicationTimeoutMinutes))) { throw new TimeoutException( $"Die AD-Gruppe '{groupName}' konnte innerhalb von {replicationTimeoutMinutes} Minuten nicht repliziert werden."); } return groupName; } /// /// Wartet darauf, dass die Gruppe nach der Erstellung im AD repliziert ist. /// private bool WaitForGroupReplication(string groupName, TimeSpan timeout) { var sw = Stopwatch.StartNew(); var pollInterval = TimeSpan.FromSeconds(5); while (sw.Elapsed < timeout) { var found = GetSecurityGroups(groupName) .Any(g => string.Equals( g.Properties["sAMAccountName"]?.Value?.ToString(), groupName, StringComparison.OrdinalIgnoreCase)); if (found) return true; Thread.Sleep(pollInterval); } return false; } /// /// Setzt das ManagedBy-Attribut einer AD-Gruppe auf eine andere Gruppe /// – mit den im Konstruktor übergebenen Credentials und Domain. /// private void SetManagedBy(string groupName, string managerGroup) { string ldapPath = $"LDAP://{_organizationalUnit}"; // SecureString -> Klartext string password = SecureStringToString(_credential.Password); using (var root = new DirectoryEntry(ldapPath, _credential.UserName, password, AuthenticationTypes.Secure)) using (var ds = new DirectorySearcher(root)) { // Gruppe holen ds.Filter = $"(&(objectClass=group)(sAMAccountName={groupName}))"; var result = ds.FindOne(); if (result == null) throw new InvalidOperationException($"Gruppe '{groupName}' nicht gefunden in {ldapPath}"); var groupEntry = result.GetDirectoryEntry(); // DistinguishedName der Manager-Gruppe ermitteln using (var mgrSearch = new DirectorySearcher(root)) { mgrSearch.Filter = $"(&(objectClass=group)(sAMAccountName={managerGroup}))"; var mgrResult = mgrSearch.FindOne(); if (mgrResult == null) throw new InvalidOperationException($"Manager-Gruppe '{managerGroup}' nicht gefunden in {ldapPath}"); string managerDn = mgrResult.GetDirectoryEntry() .Properties["distinguishedName"] .Value .ToString(); // Attribut setzen und speichern groupEntry.Properties["managedBy"].Value = managerDn; groupEntry.CommitChanges(); } } } /// /// Erstellt eine Shared Mailbox samt zugehöriger AD-Gruppen (FullAccess, SendAs, Owner) und setzt die nötigen Berechtigungen. /// public Tuple>> CreateSharedMailboxWithOwnershipGroups( string name, string alias, string displayName = null, string primarySmtpAddress = null) { CreationResult result = new CreationResult(); // Ensure AD groups string fullAccessGroup = EnsureSecurityGroup(eLiamAccessRoles.ExchangeSMBFullAccess, name); string sendAsGroup = EnsureSecurityGroup(eLiamAccessRoles.ExchangeSMBSendAs, name); string ownerGroup = EnsureSecurityGroup(eLiamAccessRoles.ExchangeSMBOwner, name); SetManagedBy(fullAccessGroup, ownerGroup); SetManagedBy(sendAsGroup, ownerGroup); // Create mailbox using (Runspace rs = CreateRunspace()) { using (PowerShell ps = PowerShell.Create()) { ps.Runspace = rs; ps.AddCommand("New-Mailbox"); ps.AddParameter("Name", name); ps.AddParameter("Alias", alias); ps.AddParameter("Shared", true); ps.AddParameter("OrganizationalUnit", _organizationalUnit); if (!string.IsNullOrEmpty(displayName)) ps.AddParameter("DisplayName", displayName); if (!string.IsNullOrEmpty(primarySmtpAddress)) ps.AddParameter("PrimarySmtpAddress", primarySmtpAddress); ps.Invoke(); AddMailboxPermission(name, fullAccessGroup, "FullAccess"); AddSendAsPermission(name, sendAsGroup).GetAwaiter().GetResult(); } } // Retrieve mailbox GUID DirectoryEntry mbEntry = FindAdObject("(&(objectClass=user)(mailNickname=" + alias + "))"); if (mbEntry != null && mbEntry.Properties.Contains("objectGUID") && mbEntry.Properties["objectGUID"].Count > 0) { byte[] bytes = (byte[])mbEntry.Properties["objectGUID"][0]; result.ObjectGuid = new Guid(bytes); } // Collect group details string[] roles = new string[] { eLiamAccessRoles.ExchangeSMBFullAccess.ToString(), eLiamAccessRoles.ExchangeSMBSendAs.ToString(), eLiamAccessRoles.ExchangeSMBOwner.ToString() }; string[] names = new string[] { fullAccessGroup, sendAsGroup, ownerGroup }; for (int i = 0; i < roles.Length; i++) { DirectoryEntry grpEntry = FindAdObject("(&(objectCategory=group)(sAMAccountName=" + names[i] + "))"); if (grpEntry != null && grpEntry.Properties.Contains("objectSid") && grpEntry.Properties["objectSid"].Count > 0) { byte[] sidBytes = (byte[])grpEntry.Properties["objectSid"][0]; string sid = new SecurityIdentifier(sidBytes, 0).Value; string distinguishedName = grpEntry.Properties["distinguishedName"][0].ToString(); result.Groups.Add(Tuple.Create(roles[i], sid, names[i], distinguishedName)); } } return Tuple.Create(result.ObjectGuid, result.Groups); } /// /// Erstellt eine Distribution Group samt zugehöriger AD-Gruppen (Member, Owner) und setzt die nötigen Berechtigungen. /// public Tuple>> CreateDistributionGroupWithOwnershipGroups( string name, string alias, string displayName = null, string primarySmtpAddress = null) { CreationResult result = new CreationResult(); // Ensure AD groups string memberGroup = EnsureSecurityGroup(eLiamAccessRoles.ExchangeMLMember, name); string ownerGroup = EnsureSecurityGroup(eLiamAccessRoles.ExchangeMLOwner, name); SetManagedBy(memberGroup, ownerGroup); // Create distribution group using (Runspace rs = CreateRunspace()) { using (PowerShell ps = PowerShell.Create()) { ps.Runspace = rs; ps.AddCommand("New-DistributionGroup"); ps.AddParameter("Name", name); ps.AddParameter("Alias", alias); ps.AddParameter("OrganizationalUnit", _organizationalUnit); if (!string.IsNullOrEmpty(displayName)) ps.AddParameter("DisplayName", displayName); if (!string.IsNullOrEmpty(primarySmtpAddress)) ps.AddParameter("PrimarySmtpAddress", primarySmtpAddress); ps.Invoke(); // b) GUID holen ps.Commands.Clear(); ps.AddCommand("Get-DistributionGroup") .AddParameter("Identity", name); var dg = ps.Invoke().FirstOrDefault(); if (dg != null && dg.Properties["Guid"] != null) { var guidVal = dg.Properties["Guid"].Value; if (guidVal is Guid g) result.ObjectGuid = g; else if (guidVal is string s && Guid.TryParse(s, out Guid parsed)) result.ObjectGuid = parsed; } AddMemberToDistributionGroup(name, memberGroup); SetDistributionGroupManagedBy(name, ownerGroup); } } // Collect group details string[] dRoles = new string[] { eLiamAccessRoles.ExchangeMLMember.ToString(), eLiamAccessRoles.ExchangeMLOwner.ToString() }; string[] dNames = new string[] { memberGroup, ownerGroup }; for (int i = 0; i < dRoles.Length; i++) { DirectoryEntry grpEntry = FindAdObject("(&(objectCategory=group)(sAMAccountName=" + dNames[i] + "))"); if (grpEntry != null && grpEntry.Properties.Contains("objectSid") && grpEntry.Properties["objectSid"].Count > 0) { byte[] sidBytes = (byte[])grpEntry.Properties["objectSid"][0]; string sid = new SecurityIdentifier(sidBytes, 0).Value; string distinguishedName = grpEntry.Properties["distinguishedName"][0].ToString(); result.Groups.Add(Tuple.Create(dRoles[i], sid, dNames[i], distinguishedName)); } } return Tuple.Create(result.ObjectGuid, result.Groups); } /// /// Setzt das ManagedBy-Attribut einer Distribution Group. /// private void SetDistributionGroupManagedBy(string groupName, string managerGroup) { using (var runspace = CreateRunspace()) using (var ps = PowerShell.Create()) { ps.Runspace = runspace; ps.AddCommand("Set-DistributionGroup") .AddParameter("Identity", groupName) .AddParameter("ManagedBy", managerGroup) .AddParameter("ErrorAction", "SilentlyContinue"); ps.Invoke(); } } /// /// Hilfsmethode: SecureString in Klartext wandeln. /// private static string SecureStringToString(SecureString ss) { if (ss == null) return string.Empty; IntPtr ptr = Marshal.SecureStringToBSTR(ss); try { return Marshal.PtrToStringBSTR(ptr) ?? string.Empty; } finally { Marshal.ZeroFreeBSTR(ptr); } } } }