initial
This commit is contained in:
288
LIAMActiveDirectory/cADServiceGroupCreator.cs
Normal file
288
LIAMActiveDirectory/cADServiceGroupCreator.cs
Normal file
@@ -0,0 +1,288 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user