Files
LIAM/LIAMActiveDirectory/cADServiceGroupCreator.cs
Meik 3d4f60d83e 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)
2026-02-27 09:12:34 +01:00

288 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// Helfer für cLiamProviderAD: Erstellt AD Member- und Owner-Gruppen für Services
/// nach konfigurierter Namenskonvention und setzt ManagedBy.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
public List<Tuple<string, string, string, string>> EnsureServiceGroups(
string serviceName,
string description = null,
eLiamAccessRoleScopes gruppenbereich = eLiamAccessRoleScopes.Universal,
ADGroupType gruppentyp = ADGroupType.Distribution,
IEnumerable<string> ownerSidList = null,
IEnumerable<string> memberSidList = null)
{
const int MaxLoop = 50;
var result = new List<Tuple<string, string, string, string>>();
// 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;
}
/// <summary>
/// Fügt einer bestehenden Gruppe per SID die entsprechenden AD-Objekte hinzu.
/// </summary>
private void AddMembersBySid(string groupName, IEnumerable<string> 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();
}
}
/// <summary>
/// Wandelt eine SID (String-Form) in das für LDAP nötige Oktet-String-Format um.
/// </summary>
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<string, string> tags)
{
string tmpl = conv.NamingTemplate.Replace("{{NAME}}", serviceName);
foreach (var kv in tags)
tmpl = tmpl.Replace("{{" + kv.Key + "}}", kv.Value);
return tmpl;
}
/// <summary>
/// Stellt sicher, dass die Gruppe existiert neu mit Scope & Type.
/// </summary>
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<DirectoryEntry>()
.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;
}
}
}
}