using System; using System.Collections.Generic; using System.DirectoryServices; using System.DirectoryServices.ActiveDirectory; using System.Linq; using System.Net.NetworkInformation; using System.Security.Principal; namespace Disco.Services.Interop.ActiveDirectory { public class ADDomainController { private const string LdapPathTemplate = @"LDAP://{0}/{1}"; private readonly ActiveDirectoryContext context; public DomainController DomainController { get; private set; } public ADDomain Domain { get; private set; } public string Name { get; private set; } public string SiteName { get; private set; } public bool IsSiteServer { get; private set; } public bool IsWritable { get; internal set; } public DateTime? AvailableWhen { get; private set; } public bool IsAvailable { get { var aw = AvailableWhen; if (aw.HasValue && aw.Value < DateTime.Now) AvailableWhen = null; return !aw.HasValue; } internal set { if (value) AvailableWhen = null; else AvailableWhen = DateTime.Now.AddMinutes(ActiveDirectory.DomainControllerUnavailableMinutes); } } public ADDomainController(ActiveDirectoryContext Context, DomainController DomainController, ADDomain Domain, bool IsSiteServer, bool IsWritable) { context = Context; this.Domain = Domain; this.DomainController = DomainController; Name = DomainController.Name; SiteName = DomainController.SiteName; this.IsSiteServer = IsSiteServer; this.IsWritable = IsWritable; AvailableWhen = null; } public ADDirectoryEntry RetrieveDirectoryEntry(string DistinguishedName, string[] LoadProperties = null) { if (string.IsNullOrWhiteSpace(DistinguishedName)) throw new ArgumentNullException("DistinguishedName"); if (!DistinguishedName.EndsWith(Domain.DistinguishedName, StringComparison.OrdinalIgnoreCase)) throw new ArgumentException($"The Distinguished Name ({DistinguishedName}) isn't a member of this domain [{Domain.Name}]", "DistinguishedName"); var entry = new DirectoryEntry(string.Format(LdapPathTemplate, Name, ADHelpers.EscapeDistinguishedName(DistinguishedName))); if (LoadProperties != null) entry.RefreshCache(LoadProperties); return new ADDirectoryEntry(Domain, this, entry); } #region Searching public IEnumerable SearchEntireDomain(string LdapFilter, string[] LoadProperties, int? ResultLimit = null) { return SearchInternal(Domain.DistinguishedName, LdapFilter, LoadProperties, ResultLimit); } public IEnumerable SearchScope(string LdapFilter, string[] LoadProperties, int? ResultLimit = null) { var searchScope = Domain.SearchContainers; // No scope set, search entire domain if (searchScope == null) return SearchEntireDomain(LdapFilter, LoadProperties, ResultLimit); // Ignore domain if (searchScope.Count == 0) return Enumerable.Empty(); // Multi-search var results = searchScope.SelectMany(scope => SearchInternal(scope, LdapFilter, LoadProperties, ResultLimit)); if (ResultLimit.HasValue) results = results.Take(ResultLimit.Value); return results; } internal IEnumerable SearchInternal(string searchRoot, string ldapFilter, string[] loadProperties, int? resultLimit, bool searchSubtree = true) { if (string.IsNullOrEmpty(searchRoot)) throw new ArgumentNullException("SearchRoot"); if (string.IsNullOrEmpty(ldapFilter)) throw new ArgumentNullException("LdapFilter"); if (resultLimit.HasValue && resultLimit.Value < 1) throw new ArgumentOutOfRangeException("ResultLimit", "The ResultLimit must be 1 or greater"); using (ADDirectoryEntry rootEntry = RetrieveDirectoryEntry(searchRoot)) { using (DirectorySearcher searcher = new DirectorySearcher(rootEntry.Entry, ldapFilter, loadProperties, searchSubtree ? System.DirectoryServices.SearchScope.Subtree : System.DirectoryServices.SearchScope.OneLevel)) { searcher.PageSize = 500; if (resultLimit.HasValue) searcher.SizeLimit = resultLimit.Value; return searcher.FindAll().Cast().Select(result => new ADSearchResult(Domain, this, searchRoot, ldapFilter, result)); } } } #endregion #region AD Objects #region User Accounts public ADUserAccount RetrieveADUserAccount(string Id, string[] AdditionalProperties = null) { string[] loadProperites = (AdditionalProperties != null && AdditionalProperties.Length > 0) ? ADUserAccount.LoadProperties.Concat(AdditionalProperties).ToArray() : ADUserAccount.LoadProperties; var result = RetrieveBySamAccountName(Id, ADUserAccount.LdapSamAccountNameFilterTemplate, loadProperites); if (result == null) return null; else return result.AsADUserAccount(false, AdditionalProperties); } #endregion #region Machine Accounts public ADMachineAccount RetrieveADMachineAccount(string Id, Guid? UUIDNetbootGUID, Guid? MacAddressNetbootGUID, string[] AdditionalProperties = null) { if (string.IsNullOrWhiteSpace(Id)) throw new ArgumentNullException("Id"); // Add $ identifier for machine accounts if (!Id.EndsWith("$")) Id += "$"; string[] loadProperites = (AdditionalProperties != null && AdditionalProperties.Length > 0) ? ADMachineAccount.LoadProperties.Concat(AdditionalProperties).ToArray() : ADMachineAccount.LoadProperties; ADSearchResult adResult; adResult = RetrieveBySamAccountName(Id, ADMachineAccount.LdapSamAccountNameFilterTemplate, loadProperites); if (adResult == null && (UUIDNetbootGUID.HasValue || MacAddressNetbootGUID.HasValue)) { string ldapFilter; if (UUIDNetbootGUID.HasValue && MacAddressNetbootGUID.HasValue) { ldapFilter = string.Format(ADMachineAccount.LdapNetbootGuidDoubleFilterTemplate, UUIDNetbootGUID.Value.ToLdapQueryFormat(), MacAddressNetbootGUID.Value.ToLdapQueryFormat()); } else if (UUIDNetbootGUID.HasValue) { ldapFilter = string.Format(ADMachineAccount.LdapNetbootGuidSingleFilterTemplate, UUIDNetbootGUID.Value.ToLdapQueryFormat()); } else // MacAddressNetbootGUID.HasValue { ldapFilter = string.Format(ADMachineAccount.LdapNetbootGuidSingleFilterTemplate, MacAddressNetbootGUID.Value.ToLdapQueryFormat()); } adResult = SearchEntireDomain(ldapFilter, loadProperites, ActiveDirectory.SingleSearchResult).FirstOrDefault(); } if (adResult != null) return adResult.AsADMachineAccount(AdditionalProperties); else return null; // Not Found } public ADMachineAccount RetrieveADMachineAccount(string Id, string[] AdditionalProperties = null) { return RetrieveADMachineAccount(Id, null, null, AdditionalProperties); } public ADMachineAccount RetrieveADMachineAccount(string Id, Guid? NetbootGUID, string[] AdditionalProperties = null) { return RetrieveADMachineAccount(Id, NetbootGUID, null, AdditionalProperties); } #endregion #region Groups public ADGroup RetrieveADGroup(string Id, string[] AdditionalProperties = null) { string[] loadProperites = (AdditionalProperties != null && AdditionalProperties.Length > 0) ? ADGroup.LoadProperties.Concat(AdditionalProperties).ToArray() : ADGroup.LoadProperties; var result = RetrieveBySamAccountName(Id, ADGroup.LdapSamAccountNameFilterTemplate, loadProperites); if (result == null) return null; else return result.AsADGroup(AdditionalProperties); } public ADGroup RetrieveADGroupByDistinguishedName(string DistinguishedName, string[] AdditionalProperties = null) { string[] loadProperites = (AdditionalProperties != null && AdditionalProperties.Length > 0) ? ADGroup.LoadProperties.Concat(AdditionalProperties).ToArray() : ADGroup.LoadProperties; using (var groupEntry = RetrieveDirectoryEntry(DistinguishedName, loadProperites)) { if (groupEntry == null) return null; return groupEntry.AsADGroup(AdditionalProperties); } } public ADGroup RetrieveADGroupWithSecurityIdentifier(SecurityIdentifier SecurityIdentifier, string[] AdditionalProperties = null) { if (SecurityIdentifier == null) throw new ArgumentNullException("SecurityIdentifier"); if (!SecurityIdentifier.IsEqualDomainSid(Domain.SecurityIdentifier)) throw new ArgumentException($"The specified Security Identifier [{SecurityIdentifier}] does not belong to this domain [{Domain.Name}]", "SecurityIdentifier"); var sidBinaryString = SecurityIdentifier.ToBinaryString(); string ldapFilter = string.Format(ADGroup.LdapSecurityIdentifierFilterTemplate, sidBinaryString); string[] loadProperites = (AdditionalProperties != null && AdditionalProperties.Length > 0) ? ADGroup.LoadProperties.Concat(AdditionalProperties).ToArray() : ADGroup.LoadProperties; var result = SearchEntireDomain(ldapFilter, loadProperites, ActiveDirectory.SingleSearchResult).FirstOrDefault(); if (result == null) return null; else return result.AsADGroup(AdditionalProperties); } #endregion #region Object private const string ObjectLdapSamAccountNameFilter = "(&(|(objectCategory=Person)(objectCategory=Computer)(objectCategory=Group))(sAMAccountName={0}))"; private static readonly string[] ObjectLoadProperties = { "objectCategory" }; private static readonly string[] ObjectLoadPropertiesAll = ObjectLoadProperties.Concat(ADUserAccount.LoadProperties).Concat(ADMachineAccount.LoadProperties).Concat(ADGroup.LoadProperties).Distinct().ToArray(); public IADObject RetrieveADObject(string Id, bool Quick, string[] AdditionalProperties = null) { var result = RetrieveBySamAccountName(Id, ObjectLdapSamAccountNameFilter, ObjectLoadPropertiesAll); if (result == null) return null; else { var objectCategory = result.Value("objectCategory"); if (objectCategory == null || objectCategory.Length == 0) throw new InvalidOperationException("objectCategory is null or empty"); if (objectCategory.StartsWith("CN=Person,", StringComparison.OrdinalIgnoreCase)) return result.AsADUserAccount(Quick, AdditionalProperties); else if (objectCategory.StartsWith("CN=Computer,", StringComparison.OrdinalIgnoreCase)) return result.AsADMachineAccount(AdditionalProperties); else if (objectCategory.StartsWith("CN=Group,", StringComparison.OrdinalIgnoreCase)) return result.AsADGroup(AdditionalProperties); else if (objectCategory.StartsWith("CN=Foreign-Security-Principal,", StringComparison.OrdinalIgnoreCase)) return null; else throw new InvalidOperationException("Unexpected objectCategory"); } } public IADObject RetrieveADObjectByDistinguishedName(string distinguishedName, bool quick, string[] additionalProperties = null) { // ignore foreign security principals var containerIndex = distinguishedName.IndexOf(',') + 1; var container = distinguishedName.Substring(containerIndex, distinguishedName.IndexOf(',', containerIndex) - containerIndex); if (string.Equals("CN=ForeignSecurityPrincipals", container, StringComparison.OrdinalIgnoreCase)) return null; using (var entry = RetrieveDirectoryEntry(distinguishedName, additionalProperties)) { if (entry == null) return null; else return entry.AsADObject(quick, additionalProperties); } } #endregion #region Organisational Units private const string OrganisationalUnitsLdapFilter = "(objectCategory=organizationalUnit)"; private static readonly string[] OrganisationalUnitsLoadProperties = { "name", "distinguishedName" }; [Obsolete("Retrieve as needed using RetrieveADOrganisationUnits(parentDistinguishedName)")] public List RetrieveADOrganisationalUnitStructure() { Dictionary> resultTree = new Dictionary>(); var unsortedOrganisationalUnits = SearchEntireDomain(OrganisationalUnitsLdapFilter, OrganisationalUnitsLoadProperties) .Select(r => r.AsADOrganisationalUnit()).ToList(); var indexedOrganisationalUnits = unsortedOrganisationalUnits.ToDictionary(k => k.DistinguishedName); var indexedChildren = unsortedOrganisationalUnits .GroupBy(ou => ou.DistinguishedName.Substring(ou.DistinguishedName.IndexOf(',') + 1)) .ToDictionary(g => g.Key, g => g.ToList()); // Link Children foreach (var ouChildren in indexedChildren) { if (indexedOrganisationalUnits.TryGetValue(ouChildren.Key, out var ouParent)) { ouParent.Children = ouChildren.Value.OrderBy(o => o.Name).ToList(); } } return indexedChildren[Domain.DistinguishedName]; } public List RetrieveADOrganisationUnits(string parentDistinguishedName = null) { if (parentDistinguishedName is null) parentDistinguishedName = Domain.DistinguishedName; return SearchInternal(parentDistinguishedName, OrganisationalUnitsLdapFilter, OrganisationalUnitsLoadProperties, null, false) .Select(r => r.AsADOrganisationalUnit()).OrderBy(ou => ou.Name).ToList(); } #endregion private ADSearchResult RetrieveBySamAccountName(string Id, string LdapFilterTemplate, string[] LoadProperties) { var slashIndex = Id.IndexOf('\\'); if (!Domain.NetBiosName.Equals(Id.Substring(0, slashIndex), StringComparison.OrdinalIgnoreCase)) throw new ArgumentException($"The Id [{Id}] is invalid for this domain [{Domain.Name}]", "Id"); var ldapFilter = string.Format(LdapFilterTemplate, Id.Substring(slashIndex + 1)); return SearchEntireDomain(ldapFilter, LoadProperties, ActiveDirectory.SingleSearchResult).FirstOrDefault(); } #endregion #region Actions public bool IsReachable() { using (Ping p = new Ping()) { var pr = p.Send(Name, 1000); return (pr.Status == IPStatus.Success); } } #endregion public override string ToString() { return Name; } public override bool Equals(object obj) { if (obj == null || !(obj is ADDomainController)) return false; else return Name == ((ADDomainController)obj).Name; } public override int GetHashCode() { return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(Name); } } }