chore: sync LIAM solution snapshot incl. diagnostics tooling
- update multiple LIAM projects and solution/config files - add LiamWorkflowDiagnostics app sources and generated outputs - include current workspace state (dependencies and build outputs)
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
using System;
|
||||
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;
|
||||
@@ -9,187 +10,254 @@ using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Management.Automation.Runspaces;
|
||||
using System.Security.Principal;
|
||||
|
||||
namespace C4IT.LIAM
|
||||
{
|
||||
using C4IT.Logging;
|
||||
using static C4IT.Logging.cLogManager;
|
||||
|
||||
namespace C4IT.LIAM
|
||||
{
|
||||
public partial class ExchangeManager
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private string EnsureSecurityGroup(eLiamAccessRoles accessRole, string baseName)
|
||||
private static readonly TimeSpan PowerShellInvokeTimeout = TimeSpan.FromSeconds(120);
|
||||
|
||||
private static Collection<PSObject> InvokePowerShellWithTimeout(PowerShell ps, TimeSpan timeout, string operationName)
|
||||
{
|
||||
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++)
|
||||
IAsyncResult asyncResult = null;
|
||||
try
|
||||
{
|
||||
// nur einfügen, wenn loop > 0
|
||||
var loopPart = loop > 0 ? $"_{loop}" : string.Empty;
|
||||
asyncResult = ps.BeginInvoke();
|
||||
if (!asyncResult.AsyncWaitHandle.WaitOne(timeout))
|
||||
{
|
||||
try
|
||||
{
|
||||
ps.Stop();
|
||||
}
|
||||
catch (Exception stopEx)
|
||||
{
|
||||
LogException(stopEx);
|
||||
}
|
||||
|
||||
// Platzhalter im Template ersetzen
|
||||
groupName = namingConvention.NamingTemplate
|
||||
.Replace("{{ADGroupPrefix}}", prefix ?? string.Empty)
|
||||
.Replace("{{NAME}}", baseName)
|
||||
.Replace("{{_LOOP}}", loopPart)
|
||||
.Replace("{{GROUPTYPEPOSTFIX}}", typePostfix ?? string.Empty);
|
||||
throw new TimeoutException(
|
||||
$"PowerShell operation '{operationName}' timed out after {timeout.TotalSeconds:0} seconds.");
|
||||
}
|
||||
|
||||
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)
|
||||
var results = ps.EndInvoke(asyncResult);
|
||||
if (ps.HadErrors)
|
||||
{
|
||||
var errorMessage = CollectPowerShellErrors(ps);
|
||||
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;
|
||||
string.IsNullOrWhiteSpace(errorMessage)
|
||||
? $"PowerShell operation '{operationName}' failed without detailed error output."
|
||||
: $"PowerShell operation '{operationName}' failed: {errorMessage}");
|
||||
}
|
||||
|
||||
newGroup.CommitChanges();
|
||||
}
|
||||
|
||||
// 6. Auf Replikation warten (optional)
|
||||
const int replicationTimeoutMinutes = 2;
|
||||
if (!WaitForGroupReplication(groupName, TimeSpan.FromMinutes(replicationTimeoutMinutes)))
|
||||
return results;
|
||||
}
|
||||
finally
|
||||
{
|
||||
throw new TimeoutException(
|
||||
$"Die AD-Gruppe '{groupName}' konnte innerhalb von {replicationTimeoutMinutes} Minuten nicht repliziert werden.");
|
||||
if (asyncResult != null)
|
||||
asyncResult.AsyncWaitHandle.Close();
|
||||
}
|
||||
|
||||
return groupName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wartet darauf, dass die Gruppe nach der Erstellung im AD repliziert ist.
|
||||
/// </summary>
|
||||
private bool WaitForGroupReplication(string groupName, TimeSpan timeout)
|
||||
private static string CollectPowerShellErrors(PowerShell ps)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var pollInterval = TimeSpan.FromSeconds(5);
|
||||
if (ps?.Streams?.Error == null || ps.Streams.Error.Count <= 0)
|
||||
return string.Empty;
|
||||
|
||||
while (sw.Elapsed < timeout)
|
||||
{
|
||||
var found = GetSecurityGroups(groupName)
|
||||
.Any(g => string.Equals(
|
||||
g.Properties["sAMAccountName"]?.Value?.ToString(),
|
||||
groupName,
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
var errors = ps.Streams.Error
|
||||
.Select(e => e.Exception?.Message ?? e.ToString())
|
||||
.Where(m => !string.IsNullOrWhiteSpace(m))
|
||||
.Take(3);
|
||||
|
||||
if (found) return true;
|
||||
Thread.Sleep(pollInterval);
|
||||
}
|
||||
|
||||
return false;
|
||||
return string.Join(" | ", errors);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setzt das ManagedBy-Attribut einer AD-Gruppe auf eine andere Gruppe
|
||||
/// – mit den im Konstruktor übergebenen Credentials und Domain.
|
||||
/// </summary>
|
||||
private void SetManagedBy(string groupName, string managerGroup)
|
||||
private static string GetSharedMailboxCreateErrorCode(Exception ex)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
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";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wartet darauf, dass die Gruppe nach der Erstellung im AD repliziert ist.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setzt das ManagedBy-Attribut einer AD-Gruppe auf eine andere Gruppe
|
||||
/// – mit den im Konstruktor übergebenen Credentials und Domain.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Erstellt eine Shared Mailbox samt zugehöriger AD-Gruppen (FullAccess, SendAs, Owner) und setzt die nötigen Berechtigungen.
|
||||
/// </summary>
|
||||
@@ -199,22 +267,63 @@ namespace C4IT.LIAM
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public Tuple<Guid, List<Tuple<string, string, string, string>>> 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();
|
||||
|
||||
// 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())
|
||||
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);
|
||||
@@ -224,47 +333,77 @@ namespace C4IT.LIAM
|
||||
ps.AddParameter("DisplayName", displayName);
|
||||
if (!string.IsNullOrEmpty(primarySmtpAddress))
|
||||
ps.AddParameter("PrimarySmtpAddress", primarySmtpAddress);
|
||||
ps.Invoke();
|
||||
AddMailboxPermission(name, fullAccessGroup, "FullAccess");
|
||||
AddSendAsPermission(name, sendAsGroup).GetAwaiter().GetResult();
|
||||
|
||||
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)
|
||||
// Retrieve mailbox GUID
|
||||
DirectoryEntry mbEntry = FindAdObject("(&(objectClass=user)(mailNickname=" + alias + "))");
|
||||
if (mbEntry != null && mbEntry.Properties.Contains("objectGUID") && mbEntry.Properties["objectGUID"].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));
|
||||
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;
|
||||
}
|
||||
|
||||
return Tuple.Create(result.ObjectGuid, result.Groups);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Erstellt eine Distribution Group samt zugehöriger AD-Gruppen (Member, Owner) und setzt die nötigen Berechtigungen.
|
||||
/// </summary>
|
||||
@@ -274,17 +413,56 @@ namespace C4IT.LIAM
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public Tuple<Guid, List<Tuple<string, string, string, string>>> 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();
|
||||
|
||||
// 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())
|
||||
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;
|
||||
@@ -296,13 +474,14 @@ namespace C4IT.LIAM
|
||||
ps.AddParameter("DisplayName", displayName);
|
||||
if (!string.IsNullOrEmpty(primarySmtpAddress))
|
||||
ps.AddParameter("PrimarySmtpAddress", primarySmtpAddress);
|
||||
ps.Invoke();
|
||||
InvokePowerShellWithTimeout(ps, PowerShellInvokeTimeout, $"New-DistributionGroup '{alias}'");
|
||||
|
||||
// b) GUID holen
|
||||
// GUID holen
|
||||
ps.Commands.Clear();
|
||||
ps.AddCommand("Get-DistributionGroup")
|
||||
.AddParameter("Identity", name);
|
||||
var dg = ps.Invoke().FirstOrDefault();
|
||||
var dg = InvokePowerShellWithTimeout(ps, PowerShellInvokeTimeout, $"Get-DistributionGroup '{name}'")
|
||||
.FirstOrDefault();
|
||||
if (dg != null && dg.Properties["Guid"] != null)
|
||||
{
|
||||
var guidVal = dg.Properties["Guid"].Value;
|
||||
@@ -310,71 +489,93 @@ namespace C4IT.LIAM
|
||||
else if (guidVal is string s && Guid.TryParse(s, out Guid parsed)) result.ObjectGuid = parsed;
|
||||
}
|
||||
|
||||
AddMemberToDistributionGroup(name, memberGroup);
|
||||
SetDistributionGroupManagedBy(name, ownerGroup);
|
||||
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
|
||||
};
|
||||
|
||||
|
||||
// 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)
|
||||
for (int i = 0; i < dRoles.Length; i++)
|
||||
{
|
||||
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));
|
||||
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);
|
||||
}
|
||||
|
||||
return Tuple.Create(result.ObjectGuid, result.Groups);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Setzt das ManagedBy-Attribut einer Distribution Group.
|
||||
/// </summary>
|
||||
private void SetDistributionGroupManagedBy(string groupName, string managerGroup)
|
||||
{
|
||||
using (var runspace = CreateRunspace())
|
||||
using (var ps = PowerShell.Create())
|
||||
catch (Exception ex)
|
||||
{
|
||||
ps.Runspace = runspace;
|
||||
ps.AddCommand("Set-DistributionGroup")
|
||||
.AddParameter("Identity", groupName)
|
||||
.AddParameter("ManagedBy", managerGroup)
|
||||
.AddParameter("ErrorAction", "SilentlyContinue");
|
||||
ps.Invoke();
|
||||
errorCode = GetDistributionGroupCreateErrorCode(ex);
|
||||
errorMessage = ex.Message;
|
||||
LogEntry($"Distribution group creation failed [{errorCode}] {errorMessage}", LogLevels.Error);
|
||||
LogException(ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hilfsmethode: SecureString in Klartext wandeln.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Setzt das ManagedBy-Attribut einer Distribution Group.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hilfsmethode: SecureString in Klartext wandeln.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user