288 lines
12 KiB
C#
288 lines
12 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|
||
} |