This commit is contained in:
Drechsler, Meik
2025-10-15 14:56:07 +02:00
commit f563d78417
896 changed files with 654481 additions and 0 deletions

View File

@@ -0,0 +1,380 @@
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
{
/// <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>
public Tuple<Guid, List<Tuple<string, string, string, string>>> 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);
}
/// <summary>
/// Erstellt eine Distribution Group samt zugehöriger AD-Gruppen (Member, Owner) und setzt die nötigen Berechtigungen.
/// </summary>
public Tuple<Guid, List<Tuple<string, string, string, string>>> 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);
}
/// <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);
}
}
}
}