Files
Disco/Disco.Services/Interop/ActiveDirectory/ADDomainController.cs
T
Gary Sharp a819d2722a Feature #49: Active Directory Managed Groups
Document Template Attachments, Device Batches, Device Profiles and User
Flags can be associated with an Active Directory group. This AD group is
then automatically synchronized with relevant User/Machine accounts.
Contains various other UI tweaks and configuration enhancements.
2014-06-16 22:21:31 +10:00

436 lines
20 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.DirectoryServices;
using System.DirectoryServices.ActiveDirectory;
using System.Linq;
using System.Net.NetworkInformation;
using System.Security.Principal;
using System.Text;
namespace Disco.Services.Interop.ActiveDirectory
{
public class ADDomainController
{
private const string LdapPathTemplate = @"LDAP://{0}/{1}";
private 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)
{
this.context = Context;
this.Domain = Domain;
this.DomainController = DomainController;
this.Name = DomainController.Name;
this.SiteName = DomainController.SiteName;
this.IsSiteServer = IsSiteServer;
this.IsWritable = IsWritable;
this.AvailableWhen = null;
}
public ADDirectoryEntry RetrieveDirectoryEntry(string DistinguishedName, string[] LoadProperties = null)
{
if (string.IsNullOrWhiteSpace(DistinguishedName))
throw new ArgumentNullException("DistinguishedName");
if (!DistinguishedName.EndsWith(this.Domain.DistinguishedName, StringComparison.OrdinalIgnoreCase))
throw new ArgumentException(string.Format("The Distinguished Name ({0}) isn't a member of this domain [{1}]", DistinguishedName, this.Domain.Name), "DistinguishedName");
var entry = new DirectoryEntry(string.Format(LdapPathTemplate, this.Name, DistinguishedName));
if (LoadProperties != null)
entry.RefreshCache(LoadProperties);
return new ADDirectoryEntry(this.Domain, this, entry);
}
#region Searching
public IEnumerable<ADSearchResult> SearchEntireDomain(string LdapFilter, string[] LoadProperties, int? ResultLimit = null)
{
return SearchInternal(Domain.DistinguishedName, LdapFilter, LoadProperties, ResultLimit);
}
public IEnumerable<ADSearchResult> SearchScope(string LdapFilter, string[] LoadProperties, int? ResultLimit = null)
{
var searchScope = this.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<ADSearchResult>();
// Multi-search
var results = searchScope.SelectMany(scope => SearchInternal(scope, LdapFilter, LoadProperties, ResultLimit));
if (ResultLimit.HasValue)
results = results.Take(ResultLimit.Value);
return results;
}
internal IEnumerable<ADSearchResult> SearchInternal(string SearchRoot, string LdapFilter, string[] LoadProperties, int? ResultLimit)
{
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 = this.RetrieveDirectoryEntry(SearchRoot))
{
using (DirectorySearcher searcher = new DirectorySearcher(rootEntry.Entry, LdapFilter, LoadProperties, System.DirectoryServices.SearchScope.Subtree))
{
searcher.PageSize = 500;
if (ResultLimit.HasValue)
searcher.SizeLimit = ResultLimit.Value;
return searcher.FindAll().Cast<SearchResult>().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, System.Guid? UUIDNetbootGUID, System.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 = this.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, System.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 = this.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(this.Domain.SecurityIdentifier))
throw new ArgumentException(string.Format("The specified Security Identifier [{0}] does not belong to this domain [{1}]", SecurityIdentifier.ToString(), this.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 = this.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<string>("objectCategory");
objectCategory = objectCategory.Substring(0, objectCategory.IndexOf(',')).ToLower();
switch (objectCategory)
{
case "cn=person":
return result.AsADUserAccount(Quick, AdditionalProperties);
case "cn=computer":
return result.AsADMachineAccount(AdditionalProperties);
case "cn=group":
return result.AsADGroup(AdditionalProperties);
default:
throw new InvalidOperationException("Unexpected objectCategory");
}
}
}
#endregion
#region Organisational Units
private const string OrganisationalUnitsLdapFilter = "(objectCategory=organizationalUnit)";
private static readonly string[] OrganisationalUnitsLoadProperties = { "name", "distinguishedName" };
public List<ADOrganisationalUnit> RetrieveADOrganisationalUnitStructure()
{
Dictionary<string, List<ADOrganisationalUnit>> resultTree = new Dictionary<string, List<ADOrganisationalUnit>>();
var unsortedOrganisationalUnits = this.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)
{
ADOrganisationalUnit ouParent;
if (indexedOrganisationalUnits.TryGetValue(ouChildren.Key, out ouParent))
{
ouParent.Children = ouChildren.Value;
}
}
return indexedChildren[Domain.DistinguishedName];
}
#endregion
private ADSearchResult RetrieveBySamAccountName(string Id, string LdapFilterTemplate, string[] LoadProperties)
{
var slashIndex = Id.IndexOf('\\');
if (!this.Domain.NetBiosName.Equals(Id.Substring(0, slashIndex), StringComparison.OrdinalIgnoreCase))
throw new ArgumentException(string.Format("The Id [{0}] is invalid for this domain [{1}]", Id, this.Domain.Name), "Id");
var ldapFilter = string.Format(LdapFilterTemplate, Id.Substring(slashIndex + 1));
return this.SearchEntireDomain(ldapFilter, LoadProperties, ActiveDirectory.SingleSearchResult).FirstOrDefault();
}
#endregion
#region Actions
public bool IsReachable()
{
using (Ping p = new Ping())
{
var pr = p.Send(this.Name, 1000);
return (pr.Status == IPStatus.Success);
}
}
public string OfflineDomainJoinProvision(string ComputerSamAccountName, string OrganisationalUnit, ref ADMachineAccount MachineAccount, out string DiagnosticInformation)
{
if (MachineAccount != null && MachineAccount.IsCriticalSystemObject)
throw new InvalidOperationException(string.Format("This account {0} is a Critical System Active Directory Object and Disco refuses to modify it", MachineAccount.DistinguishedName));
if (!this.IsWritable)
throw new InvalidOperationException(string.Format("The domain controller [{0}] is not writable. This action (Offline Domain Join Provision) requires a writable domain controller.", this.Name));
StringBuilder diagnosticInfo = new StringBuilder();
string DJoinResult = null;
if (!string.IsNullOrWhiteSpace(ComputerSamAccountName))
ComputerSamAccountName = ComputerSamAccountName.TrimEnd('$');
if (!string.IsNullOrWhiteSpace(ComputerSamAccountName) && ComputerSamAccountName.Contains('\\'))
ComputerSamAccountName = ComputerSamAccountName.Substring(ComputerSamAccountName.IndexOf('\\') + 1);
if (string.IsNullOrWhiteSpace(ComputerSamAccountName) || ComputerSamAccountName.Length > 24)
throw new System.ArgumentException("Invalid Computer Name; > 0 and <= 24", "ComputerName");
// Ensure Specified OU Exists
if (!string.IsNullOrEmpty(OrganisationalUnit))
{
try
{
using (var deOU = this.RetrieveDirectoryEntry(OrganisationalUnit, new string[] { "distinguishedName" }))
{
if (deOU == null)
throw new Exception(string.Format("OU's Directory Entry couldn't be found at [{0}]", OrganisationalUnit));
}
}
catch (Exception ex)
{
throw new ArgumentException(string.Format("An error occurred while trying to locate the specified OU: {0}", OrganisationalUnit), "OrganisationalUnit", ex);
}
}
if (MachineAccount != null)
MachineAccount.DeleteAccount(this);
string tempFileName = System.IO.Path.GetTempFileName();
string argumentOU = (!string.IsNullOrWhiteSpace(OrganisationalUnit)) ? string.Format(" /MACHINEOU \"{0}\"", OrganisationalUnit) : string.Empty;
string arguments = string.Format("/PROVISION /DOMAIN \"{0}\" /DCNAME \"{1}\" /MACHINE \"{2}\"{3} /REUSE /SAVEFILE \"{4}\"",
this.Domain.Name,
this.Name,
ComputerSamAccountName,
argumentOU,
tempFileName
);
ProcessStartInfo commandStarter = new ProcessStartInfo("DJOIN.EXE", arguments)
{
CreateNoWindow = true,
ErrorDialog = false,
LoadUserProfile = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
diagnosticInfo.AppendFormat("{0} {1}", "DJOIN.EXE", arguments);
diagnosticInfo.AppendLine();
string stdOutput;
string stdError;
using (Process commandProc = Process.Start(commandStarter))
{
commandProc.WaitForExit(20000);
stdOutput = commandProc.StandardOutput.ReadToEnd();
stdError = commandProc.StandardError.ReadToEnd();
}
if (!string.IsNullOrWhiteSpace(stdOutput))
diagnosticInfo.AppendLine(stdOutput);
if (!string.IsNullOrWhiteSpace(stdError))
diagnosticInfo.AppendLine(stdError);
if (System.IO.File.Exists(tempFileName))
{
DJoinResult = System.Convert.ToBase64String(System.IO.File.ReadAllBytes(tempFileName));
System.IO.File.Delete(tempFileName);
}
if (string.IsNullOrWhiteSpace(DJoinResult))
throw new System.InvalidOperationException(string.Format("Domain Join Unsuccessful{0}Error: {1}{0}Output: {2}", System.Environment.NewLine, stdError, stdOutput));
DiagnosticInformation = diagnosticInfo.ToString();
// Reload Machine Account
MachineAccount = this.RetrieveADMachineAccount(string.Format(@"{0}\{1}", this.Domain.NetBiosName, ComputerSamAccountName), (MachineAccount == null ? null : MachineAccount.LoadedProperties.Keys.ToArray()));
return DJoinResult;
}
#endregion
public override string ToString()
{
return this.Name;
}
public override bool Equals(object obj)
{
if (obj == null || !(obj is ADDomainController))
return false;
else
return this.Name == ((ADDomainController)obj).Name;
}
public override int GetHashCode()
{
return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this.Name);
}
}
}