using Disco.Models.Repository; using System; using System.Collections.Generic; using System.Linq; using System.Security.Principal; using System.Text; namespace Disco.Services.Interop.ActiveDirectory { public class ADMachineAccount : IADObject { internal static readonly string[] LoadProperties = { "name", "distinguishedName", "sAMAccountName", "objectSid", "userAccountControl", "dNSHostName", "description", "netbootGUID", "isCriticalSystemObject" }; internal const string LdapSamAccountNameFilterTemplate = "(&(objectCategory=computer)(sAMAccountName={0}))"; internal const string LdapNetbootGuidSingleFilterTemplate = "(&(objectCategory=computer)(netbootGUID={0}))"; internal const string LdapNetbootGuidDoubleFilterTemplate = "(&(objectCategory=computer)(|(netbootGUID={0})(netbootGUID={1})))"; public ADDomain Domain { get; private set; } public string DistinguishedName { get; private set; } public SecurityIdentifier SecurityIdentifier { get; private set; } public string Id { get { return $@"{Domain.NetBiosName}\{SamAccountName}"; } } public string SamAccountName { get; private set; } public string Name { get; private set; } public string DisplayName { get { return Name; } } public string Description { get; private set; } public string DnsName { get; private set; } public Guid NetbootGUID { get; private set; } public ADUserAccountControlFlags UserAccountControl { get; private set; } public bool IsCriticalSystemObject { get; private set; } public Dictionary LoadedProperties { get; private set; } public bool IsDisabled { get { return UserAccountControl.HasFlag(ADUserAccountControlFlags.ADS_UF_ACCOUNTDISABLE); } } public string ParentDistinguishedName { get { return DistinguishedName.Substring(DistinguishedName.IndexOf(',') + 1); } } private ADMachineAccount(ADDomain Domain, string DistinguishedName, SecurityIdentifier SecurityIdentifier, string SamAccountName, string Name, string Description, string DnsName, Guid NetbootGUID, ADUserAccountControlFlags UserAccountControl, bool IsCriticalSystemObject, Dictionary LoadedProperties) { this.Domain = Domain; this.DistinguishedName = DistinguishedName; this.SecurityIdentifier = SecurityIdentifier; this.SamAccountName = SamAccountName; this.Name = Name; this.Description = Description; this.DnsName = DnsName; this.NetbootGUID = NetbootGUID; this.UserAccountControl = UserAccountControl; this.IsCriticalSystemObject = IsCriticalSystemObject; this.LoadedProperties = LoadedProperties; } public static ADMachineAccount FromSearchResult(ADSearchResult SearchResult, string[] AdditionalProperties) { if (SearchResult == null) throw new ArgumentNullException("SearchResult"); var name = SearchResult.Value("name"); var description = SearchResult.Value("description"); var sAMAccountName = SearchResult.Value("sAMAccountName"); var distinguishedName = SearchResult.Value("distinguishedName"); var objectSid = new SecurityIdentifier(SearchResult.Value("objectSid"), 0); var dNSName = SearchResult.Value("dNSHostName"); if (dNSName == null) dNSName = $"{sAMAccountName.TrimEnd('$')}.{SearchResult.Domain.Name}"; var userAccountControl = (ADUserAccountControlFlags)SearchResult.Value("userAccountControl"); var isCriticalSystemObject = SearchResult.Value("isCriticalSystemObject"); var netbootGUID = default(Guid); var netbootGuidBytes = SearchResult.Value("netbootGUID"); if (netbootGuidBytes != null) netbootGUID = new Guid(netbootGuidBytes); // Additional Properties Dictionary additionalProperties; if (AdditionalProperties != null) additionalProperties = AdditionalProperties .Select(p => Tuple.Create(p, SearchResult.Values(p).ToArray())) .ToDictionary(t => t.Item1, t => t.Item2); else { additionalProperties = new Dictionary(); } return new ADMachineAccount( SearchResult.Domain, distinguishedName, objectSid, sAMAccountName, name, description, dNSName, netbootGUID, userAccountControl, isCriticalSystemObject, 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("name"); var description = properties.Value("description"); var sAMAccountName = properties.Value("sAMAccountName"); var distinguishedName = properties.Value("distinguishedName"); var objectSid = new SecurityIdentifier(properties.Value("objectSid"), 0); var dNSName = properties.Value("dNSHostName"); if (dNSName == null) dNSName = $"{sAMAccountName.TrimEnd('$')}.{directoryEntry.Domain.Name}"; var userAccountControl = (ADUserAccountControlFlags)properties.Value("userAccountControl"); var isCriticalSystemObject = properties.Value("isCriticalSystemObject"); var netbootGUID = default(Guid); var netbootGuidBytes = properties.Value("netbootGUID"); if (netbootGuidBytes != null) netbootGUID = new Guid(netbootGuidBytes); // Additional Properties Dictionary additionalProps; if (additionalProperties != null) additionalProps = additionalProperties .Select(p => Tuple.Create(p, properties.Values(p).ToArray())) .ToDictionary(t => t.Item1, t => t.Item2); else { additionalProps = new Dictionary(); } return new ADMachineAccount( directoryEntry.Domain, distinguishedName, objectSid, sAMAccountName, name, description, dNSName, netbootGUID, userAccountControl, isCriticalSystemObject, additionalProps); } public User ToRepositoryUser() { return new User { UserId = Id, DisplayName = Name }; } [Obsolete("Use generic equivalents: GetPropertyValue(string PropertyName)")] public object GetPropertyValue(string PropertyName, int Index = 0) { return GetPropertyValues(PropertyName).Skip(Index).FirstOrDefault(); } public T GetPropertyValue(string PropertyName) { return GetPropertyValues(PropertyName).FirstOrDefault(); } public IEnumerable GetPropertyValues(string PropertyName) { switch (PropertyName.ToLower()) { case "name": return new string[] { Name }.OfType(); case "description": return new string[] { Description }.OfType(); case "samaccountname": return new string[] { SamAccountName }.OfType(); case "distinguishedname": return new string[] { DistinguishedName }.OfType(); case "objectsid": return new SecurityIdentifier[] { SecurityIdentifier }.OfType(); case "netbootguid": return new Guid[] { NetbootGUID }.OfType(); case "userAccountControl": return new int[] { (int)UserAccountControl }.OfType(); default: if (LoadedProperties.TryGetValue(PropertyName, out var adProperty)) return adProperty.OfType(); else return Enumerable.Empty(); } } #region Actions public void RenameAccount(ADDomainController writableDomainController, string newName) { if (IsCriticalSystemObject) throw new InvalidOperationException($"This account [{DistinguishedName}] is a Critical System Active Directory Object and Disco ICT refuses to modify it"); if (!writableDomainController.IsWritable) throw new InvalidOperationException($"The domain controller [{Name}] is not writable. This action (Delete Account) requires a writable domain controller."); using (ADDirectoryEntry adEntry = writableDomainController.RetrieveDirectoryEntry(DistinguishedName)) { var entry = adEntry.Entry; entry.Properties["dNSHostName"][0] = $"{newName}.{Domain.Name}"; entry.Properties["sAMAccountName"][0] = $"{newName}$"; entry.CommitChanges(); entry.Rename($"CN={newName}"); entry.CommitChanges(); // Update Distinguished Name Name = newName; DistinguishedName = entry.Properties["distinguishedName"][0].ToString(); } } public void DeleteAccount(ADDomainController WritableDomainController) { if (IsCriticalSystemObject) throw new InvalidOperationException($"This account [{DistinguishedName}] is a Critical System Active Directory Object and Disco ICT refuses to modify it"); if (!WritableDomainController.IsWritable) throw new InvalidOperationException($"The domain controller [{Name}] is not writable. This action (Delete Account) requires a writable domain controller."); using (ADDirectoryEntry entry = WritableDomainController.RetrieveDirectoryEntry(DistinguishedName)) { entry.Entry.DeleteTree(); } } public void DeleteAccount() { DeleteAccount(Domain.GetAvailableDomainController(RequireWritable: true)); } private void SetNetbootGUID(ADDomainController WritableDomainController, Guid updatedNetbootGUID) { if (IsCriticalSystemObject) throw new InvalidOperationException($"This account {DistinguishedName} is a Critical System Active Directory Object and Disco ICT refuses to modify it"); if (NetbootGUID != updatedNetbootGUID) { using (var deAccount = WritableDomainController.RetrieveDirectoryEntry(DistinguishedName)) { var netbootGUIDProp = deAccount.Entry.Properties["netbootGUID"]; bool flag = netbootGUIDProp.Count > 0; if (flag) { netbootGUIDProp.Clear(); } netbootGUIDProp.Add(updatedNetbootGUID.ToByteArray()); deAccount.Entry.CommitChanges(); NetbootGUID = updatedNetbootGUID; } } } public void SetDescription(ADDomainController WritableDomainController, string Description) { if (IsCriticalSystemObject) throw new InvalidOperationException($"This account {DistinguishedName} is a Critical System Active Directory Object and Disco ICT refuses to modify it"); if (this.Description != Description) { using (var deAccount = WritableDomainController.RetrieveDirectoryEntry(DistinguishedName)) { var descriptionProp = deAccount.Entry.Properties["description"]; if (descriptionProp.Count != 1 || (descriptionProp[0] as string) != Description) { if (descriptionProp.Count > 0) { descriptionProp.Clear(); } if (!string.IsNullOrEmpty(Description)) { descriptionProp.Add(Description); } deAccount.Entry.CommitChanges(); this.Description = Description; } } } } public void SetDescription(string Description) { if (Description != this.Description) { SetDescription(Domain.GetAvailableDomainController(RequireWritable: true), Description); } } public void SetDescription(ADDomainController WritableDomainController, Device Device) { var descriptionBuilder = new StringBuilder(); if (Device.DecommissionedDate.HasValue) { descriptionBuilder.Append("Decommissioned: ") .Append(Device.DecommissionReason.ReasonMessage()) .Append(" (").Append(Device.DecommissionedDate.Value.ToString("yyyy-MM-dd")).Append(')'); } else { if (Device.AssignedUserId != null) { descriptionBuilder.Append(Device.AssignedUser.UserId).Append(" (").Append(Device.AssignedUser.DisplayName).Append("); "); } if (Device.DeviceModelId.HasValue) { descriptionBuilder.Append(Device.DeviceModel.Description).Append("; "); } descriptionBuilder.Append(Device.DeviceProfile.Description).Append(";"); } string description = descriptionBuilder.ToString().Trim(); if (description.Length > 1024) description = description.Substring(0, 1024); if (description != Description) { SetDescription(WritableDomainController, description); } } public void SetDescription(Device Device) { SetDescription(Domain.GetAvailableDomainController(RequireWritable: true), Device); } public void DisableAccount(ADDomainController WritableDomainController) { if (IsCriticalSystemObject) throw new InvalidOperationException($"This account {DistinguishedName} is a Critical System Active Directory Object and Disco ICT refuses to modify it"); if (!IsDisabled) { using (var deAccount = WritableDomainController.RetrieveDirectoryEntry(DistinguishedName)) { var accountControl = (ADUserAccountControlFlags)deAccount.Entry.Properties["userAccountControl"][0]; if (!accountControl.HasFlag(ADUserAccountControlFlags.ADS_UF_ACCOUNTDISABLE)) { var updatedAccountControl = (accountControl | ADUserAccountControlFlags.ADS_UF_ACCOUNTDISABLE); deAccount.Entry.Properties["userAccountControl"][0] = (int)updatedAccountControl; deAccount.Entry.CommitChanges(); UserAccountControl = updatedAccountControl; } } } } public void DisableAccount() { if (!IsDisabled) { DisableAccount(Domain.GetAvailableDomainController(RequireWritable: true)); } } public void EnableAccount(ADDomainController WritableDomainController) { if (IsCriticalSystemObject) throw new InvalidOperationException($"This account {DistinguishedName} is a Critical System Active Directory Object and Disco ICT refuses to modify it"); if (IsDisabled) { using (var deAccount = WritableDomainController.RetrieveDirectoryEntry(DistinguishedName)) { var accountControl = (ADUserAccountControlFlags)deAccount.Entry.Properties["userAccountControl"][0]; if (accountControl.HasFlag(ADUserAccountControlFlags.ADS_UF_ACCOUNTDISABLE)) { var updatedAccountControl = (accountControl ^ ADUserAccountControlFlags.ADS_UF_ACCOUNTDISABLE); deAccount.Entry.Properties["userAccountControl"][0] = (int)updatedAccountControl; deAccount.Entry.CommitChanges(); UserAccountControl = updatedAccountControl; } } } } public void EnableAccount() { if (IsDisabled) { EnableAccount(Domain.GetAvailableDomainController(RequireWritable: true)); } } public bool UpdateNetbootGUID(ADDomainController WritableDomainController, string UUID, string MACAddress) { if (IsCriticalSystemObject) throw new InvalidOperationException($"This account {DistinguishedName} is a Critical System Active Directory Object and Disco ICT refuses to modify it"); Guid netbootGUID = Guid.Empty; if (!string.IsNullOrWhiteSpace(UUID)) { netbootGUID = NetbootGUIDFromUUID(UUID); } else if (!string.IsNullOrWhiteSpace(MACAddress)) { netbootGUID = NetbootGUIDFromMACAddress(MACAddress); } if (netbootGUID != Guid.Empty && netbootGUID != NetbootGUID) { SetNetbootGUID(WritableDomainController, netbootGUID); return true; } else { return false; } } public void UpdateNetbootGUID(string UUID, string MACAddress) { UpdateNetbootGUID(Domain.GetAvailableDomainController(RequireWritable: true), UUID, MACAddress); } public static Guid NetbootGUIDFromUUID(string UUID) { return new Guid(UUID); } public static Guid NetbootGUIDFromMACAddress(string MACAddress) { string strippedMACAddress = MACAddress.Trim().Replace(":", string.Empty).Replace("-", string.Empty); bool flag = strippedMACAddress.Length == 12; Guid NetbootGUIDFromMACAddress; if (flag) { Guid guid = new Guid($"00000000-0000-0000-0000-{strippedMACAddress}"); NetbootGUIDFromMACAddress = guid; } else { NetbootGUIDFromMACAddress = Guid.Empty; } return NetbootGUIDFromMACAddress; } public void MoveOrganisationalUnit(ADDomainController WritableDomainController, string NewOrganisationUnit) { if (IsCriticalSystemObject) throw new InvalidOperationException($"This account {DistinguishedName} is a Critical System Active Directory Object and Disco ICT refuses to modify it"); var parentDistinguishedName = ParentDistinguishedName; if (parentDistinguishedName != null && !parentDistinguishedName.Equals(NewOrganisationUnit, StringComparison.OrdinalIgnoreCase)) { // If no OU provided, place in default Computers container if (string.IsNullOrWhiteSpace(NewOrganisationUnit)) NewOrganisationUnit = Domain.DefaultComputerContainer; if (!NewOrganisationUnit.EndsWith(Domain.DistinguishedName, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException($"Unable to move AD Account from one domain [{DistinguishedName}] to another [{NewOrganisationUnit}]."); using (ADDirectoryEntry ou = WritableDomainController.RetrieveDirectoryEntry(NewOrganisationUnit)) { using (ADDirectoryEntry i = WritableDomainController.RetrieveDirectoryEntry(DistinguishedName)) { i.Entry.UsePropertyCache = false; i.Entry.MoveTo(ou.Entry); // Update Distinguished Name DistinguishedName = i.Entry.Properties["distinguishedName"][0].ToString(); } } } } public void MoveOrganisationalUnit(string NewOrganisationUnit) { MoveOrganisationalUnit(Domain.GetAvailableDomainController(RequireWritable: true), NewOrganisationUnit); } #endregion public override string ToString() { return Id; } public override bool Equals(object obj) { if (obj == null || !(obj is ADMachineAccount)) return false; else return DistinguishedName == ((ADMachineAccount)obj).DistinguishedName; } public override int GetHashCode() { return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(DistinguishedName); } } }