maintenance: AD integration refactoring

This commit is contained in:
Gary Sharp
2022-12-04 13:26:58 +11:00
parent 261baf669e
commit 99be87ed9c
8 changed files with 272 additions and 21 deletions
@@ -270,6 +270,17 @@ namespace Disco.Services.Interop.ActiveDirectory
}
}
}
public IADObject RetrieveADObjectByDistinguishedName(string distinguishedName, bool quick, string[] additionalProperties = null)
{
using (var entry = RetrieveDirectoryEntry(distinguishedName, additionalProperties))
{
if (entry == null)
return null;
else
return entry.AsADObject(quick, additionalProperties);
}
}
#endregion
#region Organisational Units
@@ -7,7 +7,7 @@ namespace Disco.Services.Interop.ActiveDirectory
{
public class ADGroup : IADObject
{
internal static readonly string[] LoadProperties = { "name", "distinguishedName", "sAMAccountName", "objectSid", "memberOf" };
internal static readonly string[] LoadProperties = { "name", "distinguishedName", "sAMAccountName", "objectSid", "memberOf", "member" };
internal static string LdapSearchFilterTemplate = "(&(objectCategory=Group)(|(sAMAccountName={0}*)(name={0}*)(cn={0}*)))";
internal const string LdapSamAccountNameFilterTemplate = "(&(objectCategory=Group)(sAMAccountName={0}))";
internal const string LdapSecurityIdentifierFilterTemplate = "(&(objectCategory=Group)(objectSid={0}))";
@@ -23,11 +23,13 @@ namespace Disco.Services.Interop.ActiveDirectory
public string Name { get; private set; }
public string DisplayName { get { return Name; } }
public List<string> MemberOf { get; private set; }
public List<string> MemberOf { get; }
public List<string> Members { get; }
public Dictionary<string, object[]> LoadedProperties { get; private set; }
private ADGroup(ADDomain Domain, string DistinguishedName, SecurityIdentifier SecurityIdentifier, string SamAccountName, string Name, List<string> MemberOf, Dictionary<string, object[]> LoadedProperties)
private ADGroup(ADDomain Domain, string DistinguishedName, SecurityIdentifier SecurityIdentifier, string SamAccountName, string Name, List<string> MemberOf, List<string> Members, Dictionary<string, object[]> LoadedProperties)
{
this.Domain = Domain;
this.DistinguishedName = DistinguishedName;
@@ -35,6 +37,7 @@ namespace Disco.Services.Interop.ActiveDirectory
this.SamAccountName = SamAccountName;
this.Name = Name;
this.MemberOf = MemberOf;
this.Members = Members;
this.LoadedProperties = LoadedProperties;
}
@@ -48,6 +51,7 @@ namespace Disco.Services.Interop.ActiveDirectory
var sAMAccountName = SearchResult.Value<string>("sAMAccountName");
var objectSid = new SecurityIdentifier(SearchResult.Value<byte[]>("objectSid"), 0);
var memberOf = SearchResult.Values<string>("memberOf").ToList();
var members = SearchResult.Values<string>("member").ToList();
// Additional Properties
Dictionary<string, object[]> additionalProperties;
@@ -60,7 +64,7 @@ namespace Disco.Services.Interop.ActiveDirectory
additionalProperties = new Dictionary<string, object[]>();
}
return new ADGroup(SearchResult.Domain, distinguishedName, objectSid, sAMAccountName, name, memberOf, additionalProperties);
return new ADGroup(SearchResult.Domain, distinguishedName, objectSid, sAMAccountName, name, memberOf, members, additionalProperties);
}
public static ADGroup FromDirectoryEntry(ADDirectoryEntry DirectoryEntry, string[] AdditionalProperties)
@@ -75,6 +79,7 @@ namespace Disco.Services.Interop.ActiveDirectory
var sAMAccountName = properties.Value<string>("sAMAccountName");
var objectSid = new SecurityIdentifier(properties.Value<byte[]>("objectSid"), 0);
var memberOf = properties.Values<string>("memberOf").ToList();
var members = properties.Values<string>("member").ToList();
Dictionary<string, object[]> additionalProperties;
if (AdditionalProperties != null)
@@ -86,7 +91,7 @@ namespace Disco.Services.Interop.ActiveDirectory
additionalProperties = new Dictionary<string, object[]>();
}
return new ADGroup(DirectoryEntry.Domain, distinguishedName, objectSid, sAMAccountName, name, memberOf, additionalProperties);
return new ADGroup(DirectoryEntry.Domain, distinguishedName, objectSid, sAMAccountName, name, memberOf, members, additionalProperties);
}
[Obsolete("Use generic equivalents: GetPropertyValue<T>(string PropertyName)")]
@@ -113,6 +118,8 @@ namespace Disco.Services.Interop.ActiveDirectory
return new SecurityIdentifier[] { SecurityIdentifier }.OfType<T>();
case "memberof":
return MemberOf.OfType<T>();
case "member":
return Members.OfType<T>();
default:
object[] adProperty;
if (LoadedProperties.TryGetValue(PropertyName, out adProperty))
@@ -122,6 +129,42 @@ namespace Disco.Services.Interop.ActiveDirectory
}
}
public IEnumerable<ADUserAccount> GetUserMembersRecursive()
{
var foundGroups = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
return GetUserMembersRecursive(foundGroups);
}
private IEnumerable<ADUserAccount> GetUserMembersRecursive(HashSet<string> foundGroups)
{
if (!foundGroups.Add(DistinguishedName))
yield break;
var memberGroups = new List<ADGroup>();
foreach (var memberDn in Members)
{
if (foundGroups.Contains(memberDn))
continue;
var adObject = ActiveDirectory.RetrieveADObjectByDistinguishedName(memberDn, true);
if (adObject == null)
continue;
else if (adObject is ADGroup group)
memberGroups.Add(group);
else if (adObject is ADUserAccount adUser)
yield return adUser;
}
foreach (var group in memberGroups)
{
if (foundGroups.Contains(group.DistinguishedName))
continue;
foreach (var adUser in group.GetUserMembersRecursive(foundGroups))
yield return adUser;
}
}
public override string ToString()
{
return Id;
@@ -1,6 +1,7 @@
using Disco.Models.Repository;
using System;
using System.Collections.Generic;
using System.DirectoryServices;
using System.Linq;
using System.Security.Principal;
using System.Text;
@@ -108,6 +109,56 @@ namespace Disco.Services.Interop.ActiveDirectory
additionalProperties);
}
public static ADMachineAccount FromDirectoryEntry(ADDirectoryEntry directoryEntry, string[] additionalProperties)
{
if (directoryEntry == null)
throw new ArgumentNullException(nameof(directoryEntry));
var properties = directoryEntry.Entry.Properties;
var name = properties.Value<string>("name");
var description = properties.Value<string>("description");
var sAMAccountName = properties.Value<string>("sAMAccountName");
var distinguishedName = properties.Value<string>("distinguishedName");
var objectSid = new SecurityIdentifier(properties.Value<byte[]>("objectSid"), 0);
var dNSName = properties.Value<string>("dNSHostName");
if (dNSName == null)
dNSName = string.Format("{0}.{1}", sAMAccountName.TrimEnd('$'), directoryEntry.Domain.Name);
var userAccountControl = (ADUserAccountControlFlags)properties.Value<int>("userAccountControl");
var isCriticalSystemObject = properties.Value<bool>("isCriticalSystemObject");
var netbootGUID = default(Guid);
var netbootGuidBytes = properties.Value<byte[]>("netbootGUID");
if (netbootGuidBytes != null)
netbootGUID = new Guid(netbootGuidBytes);
// Additional Properties
Dictionary<string, object[]> additionalProps;
if (additionalProperties != null)
additionalProps = additionalProperties
.Select(p => Tuple.Create(p, properties.Values<object>(p).ToArray()))
.ToDictionary(t => t.Item1, t => t.Item2);
else
{
additionalProps = new Dictionary<string, object[]>();
}
return new ADMachineAccount(
directoryEntry.Domain,
distinguishedName,
objectSid,
sAMAccountName,
name,
description,
dNSName,
netbootGUID,
userAccountControl,
isCriticalSystemObject,
additionalProps);
}
public User ToRepositoryUser()
{
return new User
@@ -126,6 +126,72 @@ namespace Disco.Services.Interop.ActiveDirectory
additionalProperties);
}
public static ADUserAccount FromDirectoryEntry(ADDirectoryEntry directoryEntry, bool quick, string[] additionalProperties)
{
if (directoryEntry == null)
throw new ArgumentNullException(nameof(directoryEntry));
var properties = directoryEntry.Entry.Properties;
var name = properties.Value<string>("name");
var sAMAccountName = properties.Value<string>("sAMAccountName");
var distinguishedName = properties.Value<string>("distinguishedName");
var objectSid = new SecurityIdentifier(properties.Value<byte[]>("objectSid"), 0);
var displayName = properties.Value<string>("displayName") ?? sAMAccountName;
var surname = properties.Value<string>("sn");
var givenName = properties.Value<string>("givenName");
var email = properties.Value<string>("mail");
var phone = properties.Value<string>("telephoneNumber");
var userAccountControl = (ADUserAccountControlFlags)properties.Value<int>("userAccountControl");
var isCriticalSystemObject = properties.Value<bool>("isCriticalSystemObject");
List<ADGroup> groups = null;
// Don't load Groups when doing a quick search
if (!quick)
{
var primaryGroupID = properties.Value<int>("primaryGroupID");
var primaryGroupSid = ADHelpers.BuildPrimaryGroupSid(objectSid, primaryGroupID);
var memberGroups = properties.Values<string>("memberOf");
var primaryGroup = ActiveDirectory.GroupCache.GetGroup(primaryGroupSid);
var groupDistinguishedNames =
new string[] { primaryGroup.DistinguishedName }
.Concat(memberGroups);
groups = ActiveDirectory.GroupCache.GetRecursiveGroups(groupDistinguishedNames).ToList();
}
// Additional Properties
Dictionary<string, object[]> additionalProps;
if (additionalProperties != null)
additionalProps = additionalProperties
.Select(p => Tuple.Create(p, properties.Values<object>(p).ToArray()))
.ToDictionary(t => t.Item1, t => t.Item2);
else
{
additionalProps = new Dictionary<string, object[]>();
}
return new ADUserAccount(
directoryEntry.Domain,
distinguishedName,
objectSid,
sAMAccountName,
name,
displayName,
surname,
givenName,
email,
phone,
userAccountControl,
isCriticalSystemObject,
groups,
additionalProps);
}
[Obsolete("Use generic equivalents: GetPropertyValue<T>(string PropertyName)")]
public object GetPropertyValue(string PropertyName, int Index = 0)
{
@@ -173,6 +173,11 @@ namespace Disco.Services.Interop.ActiveDirectory
var domain = Context.GetDomainFromId(Id);
return domain.GetAvailableDomainController().RetrieveADObject(Id, Quick);
}
public static IADObject RetrieveADObjectByDistinguishedName(string distinguishedName, bool quick)
{
var domain = Context.GetDomainFromDistinguishedName(distinguishedName);
return domain.GetAvailableDomainController().RetrieveADObjectByDistinguishedName(distinguishedName, quick);
}
#endregion
#region Actions
@@ -1,10 +1,14 @@
using Disco.Models.Repository;
using Disco.Services.Interop.ActiveDirectory;
using DocumentFormat.OpenXml.Vml.Office;
using System;
using System.Collections.Generic;
using System.DirectoryServices;
using System.DirectoryServices.ActiveDirectory;
using System.IO;
using System.Linq;
using System.Net.NetworkInformation;
using ZXing;
namespace Disco.Services
{
@@ -95,6 +99,24 @@ namespace Disco.Services
return SearchResults.Select(sr => ADOrganisationalUnit.FromSearchResult(sr));
}
public static IADObject AsADObject(this ADDirectoryEntry directoryEntry, bool quick, string[] additionalProperties)
{
var properties = directoryEntry.Entry.Properties;
var objectCategory = properties.Value<string>("objectCategory");
objectCategory = objectCategory.Substring(0, objectCategory.IndexOf(',')).ToLower();
switch (objectCategory)
{
case "cn=person":
return ADUserAccount.FromDirectoryEntry(directoryEntry, quick, additionalProperties);
case "cn=computer":
return ADMachineAccount.FromDirectoryEntry(directoryEntry, additionalProperties);
case "cn=group":
return ADGroup.FromDirectoryEntry(directoryEntry, additionalProperties);
default:
throw new InvalidOperationException("Unexpected objectCategory");
}
}
#endregion
public static ADUserAccount ActiveDirectoryAccount(this User User, params string[] AdditionalProperties)