using System; using System.Linq; using System.Management.Automation; using System.Collections.Generic; using System.Collections.ObjectModel; 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; using C4IT.Logging; using static C4IT.Logging.cLogManager; namespace C4IT.LIAM { public partial class ExchangeManager { private static readonly TimeSpan PowerShellInvokeTimeout = TimeSpan.FromSeconds(120); private static PSDataCollection InvokePowerShellWithTimeout(PowerShell ps, TimeSpan timeout, string operationName) { IAsyncResult asyncResult = null; try { asyncResult = ps.BeginInvoke(); if (!asyncResult.AsyncWaitHandle.WaitOne(timeout)) { try { ps.Stop(); } catch (Exception stopEx) { LogException(stopEx); } throw new TimeoutException( $"PowerShell operation '{operationName}' timed out after {timeout.TotalSeconds:0} seconds."); } var results = ps.EndInvoke(asyncResult); if (ps.HadErrors) { var errorMessage = CollectPowerShellErrors(ps); throw new InvalidOperationException( string.IsNullOrWhiteSpace(errorMessage) ? $"PowerShell operation '{operationName}' failed without detailed error output." : $"PowerShell operation '{operationName}' failed: {errorMessage}"); } return results; } finally { if (asyncResult != null) asyncResult.AsyncWaitHandle.Close(); } } private static string CollectPowerShellErrors(PowerShell ps) { if (ps?.Streams?.Error == null || ps.Streams.Error.Count <= 0) return string.Empty; var errors = ps.Streams.Error .Select(e => e.Exception?.Message ?? e.ToString()) .Where(m => !string.IsNullOrWhiteSpace(m)) .Take(3); return string.Join(" | ", errors); } private static string GetSharedMailboxCreateErrorCode(Exception ex) { return ex is TimeoutException ? "EXCH_SHAREDMAILBOX_TIMEOUT" : "EXCH_SHAREDMAILBOX_CREATE_FAILED"; } private static string GetDistributionGroupCreateErrorCode(Exception ex) { return ex is TimeoutException ? "EXCH_DISTRIBUTIONGROUP_TIMEOUT" : "EXCH_DISTRIBUTIONGROUP_CREATE_FAILED"; } /// /// 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) { string errorCode; string errorMessage; var result = CreateSharedMailboxWithOwnershipGroups( name, alias, displayName, primarySmtpAddress, out errorCode, out errorMessage); if (result == null) throw new InvalidOperationException($"[{errorCode}] {errorMessage}"); return result; } /// /// Erstellt eine Shared Mailbox samt zugehöriger AD-Gruppen (FullAccess, SendAs, Owner) und setzt die nötigen Berechtigungen. /// Liefert bei Fehlern einen Error-Code und eine Message zurück. /// public Tuple>> CreateSharedMailboxWithOwnershipGroups( string name, string alias, string displayName, string primarySmtpAddress, out string errorCode, out string errorMessage) { errorCode = string.Empty; errorMessage = string.Empty; CreationResult result = new CreationResult(); try { LogEntry( $"Start shared mailbox creation: Name='{name}', Alias='{alias}', DisplayName='{displayName}', PrimarySmtpAddress='{primarySmtpAddress}'", LogLevels.Info); // 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); LogEntry( $"Shared mailbox groups prepared: FullAccess='{fullAccessGroup}', SendAs='{sendAsGroup}', Owner='{ownerGroup}'", LogLevels.Debug); // Create mailbox and permissions 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); InvokePowerShellWithTimeout(ps, PowerShellInvokeTimeout, $"New-Mailbox '{alias}'"); ps.Commands.Clear(); ps.AddCommand("Add-MailboxPermission"); ps.AddParameter("Identity", name); ps.AddParameter("User", fullAccessGroup); ps.AddParameter("AccessRights", "FullAccess"); ps.AddParameter("InheritanceType", "All"); ps.AddParameter("ErrorAction", "Stop"); InvokePowerShellWithTimeout(ps, PowerShellInvokeTimeout, $"Add-MailboxPermission '{name}' -> '{fullAccessGroup}'"); ps.Commands.Clear(); ps.AddCommand("Add-ADPermission"); ps.AddParameter("Identity", name); ps.AddParameter("User", sendAsGroup); ps.AddParameter("ExtendedRights", "Send-As"); ps.AddParameter("AccessRights", "ExtendedRight"); ps.AddParameter("ErrorAction", "Stop"); InvokePowerShellWithTimeout(ps, PowerShellInvokeTimeout, $"Add-ADPermission Send-As '{name}' -> '{sendAsGroup}'"); } // 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)); } } errorCode = "OK"; LogEntry( $"Shared mailbox created successfully: Name='{name}', Alias='{alias}', ObjectGuid='{result.ObjectGuid}', GroupCount='{result.Groups.Count}'", LogLevels.Info); return Tuple.Create(result.ObjectGuid, result.Groups); } catch (Exception ex) { errorCode = GetSharedMailboxCreateErrorCode(ex); errorMessage = ex.Message; LogEntry($"Shared mailbox creation failed [{errorCode}] {errorMessage}", LogLevels.Error); LogException(ex); return null; } } /// /// 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) { string errorCode; string errorMessage; var result = CreateDistributionGroupWithOwnershipGroups( name, alias, displayName, primarySmtpAddress, out errorCode, out errorMessage); if (result == null) throw new InvalidOperationException($"[{errorCode}] {errorMessage}"); return result; } /// /// Erstellt eine Distribution Group samt zugehöriger AD-Gruppen (Member, Owner) und setzt die nötigen Berechtigungen. /// Liefert bei Fehlern einen Error-Code und eine Message zurück. /// public Tuple>> CreateDistributionGroupWithOwnershipGroups( string name, string alias, string displayName, string primarySmtpAddress, out string errorCode, out string errorMessage) { errorCode = string.Empty; errorMessage = string.Empty; CreationResult result = new CreationResult(); try { LogEntry( $"Start distribution group creation: Name='{name}', Alias='{alias}', DisplayName='{displayName}', PrimarySmtpAddress='{primarySmtpAddress}'", LogLevels.Info); // Ensure AD groups string memberGroup = EnsureSecurityGroup(eLiamAccessRoles.ExchangeMLMember, name); string ownerGroup = EnsureSecurityGroup(eLiamAccessRoles.ExchangeMLOwner, name); SetManagedBy(memberGroup, ownerGroup); LogEntry( $"Distribution group permission groups prepared: Member='{memberGroup}', Owner='{ownerGroup}'", LogLevels.Debug); // Create distribution group and permissions 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); InvokePowerShellWithTimeout(ps, PowerShellInvokeTimeout, $"New-DistributionGroup '{alias}'"); // GUID holen ps.Commands.Clear(); ps.AddCommand("Get-DistributionGroup") .AddParameter("Identity", name); var dg = InvokePowerShellWithTimeout(ps, PowerShellInvokeTimeout, $"Get-DistributionGroup '{name}'") .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; } ps.Commands.Clear(); ps.AddCommand("Add-DistributionGroupMember") .AddParameter("Identity", name) .AddParameter("Member", memberGroup) .AddParameter("ErrorAction", "Stop"); InvokePowerShellWithTimeout(ps, PowerShellInvokeTimeout, $"Add-DistributionGroupMember '{name}' -> '{memberGroup}'"); ps.Commands.Clear(); ps.AddCommand("Set-DistributionGroup") .AddParameter("Identity", name) .AddParameter("ManagedBy", ownerGroup) .AddParameter("ErrorAction", "Stop"); InvokePowerShellWithTimeout(ps, PowerShellInvokeTimeout, $"Set-DistributionGroup ManagedBy '{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)); } } errorCode = "OK"; LogEntry( $"Distribution group created successfully: Name='{name}', Alias='{alias}', ObjectGuid='{result.ObjectGuid}', GroupCount='{result.Groups.Count}'", LogLevels.Info); return Tuple.Create(result.ObjectGuid, result.Groups); } catch (Exception ex) { errorCode = GetDistributionGroupCreateErrorCode(ex); errorMessage = ex.Message; LogEntry($"Distribution group creation failed [{errorCode}] {errorMessage}", LogLevels.Error); LogException(ex); return null; } } /// /// 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); } } } }