namespace DSInternals.DataStore { using DSInternals.Common.Data; using Microsoft.Database.Isam; using System; using System.Collections.Generic; using System.Linq; using System.Security.Principal; /// /// The DomainController class represents a domain controller in an Active Directory domain. /// public class DomainController : IDisposable, IDomainController { public const long UsnMinValue = 1; public const long UsnMaxValue = long.MaxValue; public const long EpochMinValue = 1; public const long EpochMaxValue = int.MaxValue; private const string CrossRefContainerRDN = "CN=Partitions"; // List of columns in the hiddentable: private const string ntdsSettingsCol = "dsa_col"; private const string osVersionMinorCol = "osminorversion_col"; private const string osVersionMajorCol = "osmajorversion_col"; private const string highestCommitedUsnCol = "usn_col"; private const string backupExpirationCol = "backupexpiration_col"; private const string backupUsnCol = "backupusn_col"; private const string usnAtIfmCol = "usnatrifm_col"; private const string stateCol = "state_col"; private const string epochCol = "epoch_col"; private const string flagsCol = "flags_col"; private Cursor systemTableCursor; // Cache for writable attributes private long highestUSNCache; private int? epochCache; private DateTime? backupExpirationCache; public DomainController(DirectoryContext context) { // TODO: Split to different methods. // Open the hiddentable this.systemTableCursor = context.OpenSystemTable(); // Go to the first and only record in the hiddentable: this.systemTableCursor.MoveToFirst(); // Load attributes from the hiddentable: this.NTDSSettingsDNT = this.systemTableCursor.RetrieveColumnAsInt(ntdsSettingsCol).Value; if(this.systemTableCursor.TableDefinition.Columns.Contains(osVersionMajorCol)) { // Some databases like the initial adamntds.dit or ntds.dit on Windows Server 2003 do not contain the OS Version this.OSVersionMinor = this.systemTableCursor.RetrieveColumnAsUInt(osVersionMinorCol); this.OSVersionMajor = this.systemTableCursor.RetrieveColumnAsUInt(osVersionMajorCol); } if (this.systemTableCursor.TableDefinition.Columns.Contains(epochCol)) { // This is a new feature since Windows Server 2008 this.epochCache = this.systemTableCursor.RetrieveColumnAsInt(epochCol); } if (this.systemTableCursor.TableDefinition.Columns.Contains(usnAtIfmCol)) { this.UsnAtIfm = this.systemTableCursor.RetrieveColumnAsLong(usnAtIfmCol); } // Load and cache the backup expiration time this.backupExpirationCache = this.systemTableCursor.RetrieveColumnAsGeneralizedTime(backupExpirationCol); this.BackupUsn = this.systemTableCursor.RetrieveColumnAsLong(backupUsnCol); this.State = (DatabaseState) this.systemTableCursor.RetrieveColumnAsInt(stateCol).Value; byte[] binaryFlags = this.systemTableCursor.RetrieveColumnAsByteArray(flagsCol); var databaseFlags = new DatabaseFlags(binaryFlags); this.IsADAM = databaseFlags.ADAMDatabase; // TODO: Export other database flags, not just IsADAM. // TODO: Load database health this.highestUSNCache = this.systemTableCursor.RetrieveColumnAsLong(highestCommitedUsnCol).Value; // Now we can load the Invocation ID and other information from the datatable: using (var dataTableCursor = context.OpenDataTable()) { // Goto NTDS Settings object: DirectorySchema schema = context.Schema; dataTableCursor.CurrentIndex = schema.FindIndexName(CommonDirectoryAttributes.DNTag); bool ntdsFound = dataTableCursor.GotoKey(Key.Compose(this.NTDSSettingsDNT)); // Load data from the NTDS Settings object this.InvocationId = dataTableCursor.RetrieveColumnAsGuid(schema.FindColumnId(CommonDirectoryAttributes.InvocationId)).Value; this.DsaGuid = dataTableCursor.RetrieveColumnAsGuid(schema.FindColumnId(CommonDirectoryAttributes.ObjectGUID)).Value; this.Options = dataTableCursor.RetrieveColumnAsDomainControllerOptions(schema.FindColumnId(CommonDirectoryAttributes.Options)); string ntdsName = dataTableCursor.RetrieveColumnAsString(schema.FindColumnId(CommonDirectoryAttributes.CommonName)); // Retrieve Configuration Naming Context this.ConfigurationNamingContextDNT = dataTableCursor.RetrieveColumnAsDNTag(schema.FindColumnId(CommonDirectoryAttributes.NamingContextDNTag)).Value; this.ConfigurationNamingContext = context.DistinguishedNameResolver.Resolve(this.ConfigurationNamingContextDNT); // Retrieve Schema Naming Context this.SchemaNamingContextDNT = dataTableCursor.RetrieveColumnAsDNTag(schema.FindColumnId(CommonDirectoryAttributes.SchemaLocation)).Value; this.SchemaNamingContext = context.DistinguishedNameResolver.Resolve(this.SchemaNamingContextDNT); // Goto DC object (parent of NTDS): bool dcFound = dataTableCursor.GotoParentObject(schema); // Load data from the DC object // Load DC name: string dcName = dataTableCursor.RetrieveColumnAsString(schema.FindColumnId(CommonDirectoryAttributes.CommonName)); // DC name is null in the initial database, so use NTDS Settings object's CN instead this.Name = dcName ?? ntdsName; // Load DNS Host Name this.DNSHostName = dataTableCursor.RetrieveColumnAsString(schema.FindColumnId(CommonDirectoryAttributes.DNSHostName)); // Load server reference to domain partition: int dcDNTag = dataTableCursor.RetrieveColumnAsDNTag(schema.FindColumnId(CommonDirectoryAttributes.DNTag)).Value; this.ServerReferenceDNT = context.LinkResolver.GetLinkedDNTag(dcDNTag, CommonDirectoryAttributes.ServerReference); // Goto Servers object (parent of DC): bool serversFound = dataTableCursor.GotoParentObject(schema); // Goto Site object (parent of servers): bool siteFound = dataTableCursor.GotoParentObject(schema); // Load data from the Site object if(siteFound) { this.SiteName = dataTableCursor.RetrieveColumnAsString(schema.FindColumnId(CommonDirectoryAttributes.CommonName)); } // Load partitions (linked multivalue attribute) // TODO: Does not return PAS partitions on RODCs IEnumerable partitionDNTags = context.LinkResolver.GetLinkedDNTags(this.NTDSSettingsDNT, CommonDirectoryAttributes.MasterNamingContexts); this.WritablePartitions = context.DistinguishedNameResolver.Resolve(partitionDNTags).Select(dn => dn.ToString()).ToArray(); // Load domain (linked multivalue attribute) // TODO: Test this against a GC and RODC: this.DomainNamingContextDNT = context.LinkResolver.GetLinkedDNTag(this.NTDSSettingsDNT, CommonDirectoryAttributes.DomainNamingContexts); if (this.DomainNamingContextDNT.HasValue) { // Move cursor to domain: bool domainObjectFound = dataTableCursor.GotoKey(Key.Compose(this.DomainNamingContextDNT.Value)); // Load domain SID this.DomainSid = dataTableCursor.RetrieveColumnAsSid(schema.FindColumnId(CommonDirectoryAttributes.ObjectSid)); // Load domain GUID this.DomainGuid = dataTableCursor.RetrieveColumnAsGuid(schema.FindColumnId(CommonDirectoryAttributes.ObjectGUID)); // Load domain naming context: this.DomainNamingContext = context.DistinguishedNameResolver.Resolve(this.DomainNamingContextDNT.Value); // Load the domain functional level this.DomainMode = dataTableCursor.RetrieveColumnAsFunctionalLevel(schema.FindColumnId(CommonDirectoryAttributes.FunctionalLevel)); } // Goto server object in domain partition if (this.ServerReferenceDNT.HasValue) { bool serverFound = dataTableCursor.GotoKey(Key.Compose(this.ServerReferenceDNT.Value)); // Load DC OS this.OSName = dataTableCursor.RetrieveColumnAsString(schema.FindColumnId(CommonDirectoryAttributes.OperatingSystemName)); // Load DC GUID this.Guid = dataTableCursor.RetrieveColumnAsGuid(schema.FindColumnId(CommonDirectoryAttributes.ObjectGUID)); // Load DC SID this.Sid = dataTableCursor.RetrieveColumnAsSid(schema.FindColumnId(CommonDirectoryAttributes.ObjectSid)); // Load DC DN this.ServerReference = context.DistinguishedNameResolver.Resolve(this.ServerReferenceDNT.Value); } // Construct crossRefContainer DN (CN=Partitions,CN=Configuration,...) var crossRefContainer = new DistinguishedName(CrossRefContainerRDN); crossRefContainer.AddParent(this.ConfigurationNamingContext); // Goto crossRefContainer var crossRefContainerDNT = context.DistinguishedNameResolver.Resolve(crossRefContainer); bool crossRefContainerFound = dataTableCursor.GotoKey(Key.Compose(crossRefContainerDNT)); // Read the forest functional level from the crossRefContainer object we just located this.ForestMode = dataTableCursor.RetrieveColumnAsFunctionalLevel(schema.FindColumnId(CommonDirectoryAttributes.FunctionalLevel)); // Go through all crossRef objects (children of the crossRefContainer) dataTableCursor.FindChildren(schema); while (dataTableCursor.MoveNext()) { // Find the directory partition that is associated with the current crossRef object var partitionNCNameDNT = dataTableCursor.RetrieveColumnAsDNTag(schema.FindColumnId(CommonDirectoryAttributes.NamingContextName)); // Note that foreign crossRef objects do not have nCName set if (partitionNCNameDNT == this.DomainNamingContextDNT) { // This must be the DC's domain crossRef object, so we can retrieve its NetBIOS name. this.NetBIOSDomainName = dataTableCursor.RetrieveColumnAsString(schema.FindColumnId(CommonDirectoryAttributes.NetBIOSName)); } if(partitionNCNameDNT == this.ConfigurationNamingContextDNT) { // This must be the configuration partition's crossRef object, so we can retrieve the forest DNS name. this.ForestName = dataTableCursor.RetrieveColumnAsString(schema.FindColumnId(CommonDirectoryAttributes.DNSRoot)); } } } } public int NTDSSettingsDNT { get; private set; } public int SchemaNamingContextDNT { get; private set; } public int ConfigurationNamingContextDNT { get; private set; } /// /// Gets the GUID associated with the DC account. /// public Guid? Guid { get; private set; } /// /// Gets the Security ID (SID) of the DC account. /// public SecurityIdentifier Sid { get; private set; } /// /// Gets the operating system version of this domain controller. /// public string OSVersion { get { if(this.OSVersionMajor == null) { return null; } return String.Format("{0}.{1}", this.OSVersionMajor, this.OSVersionMinor); } } public uint? OSVersionMajor { get; private set; } public uint? OSVersionMinor { get; private set; } /// /// Gets the name of the domain that this domain controller is a member of. /// public string DomainName { get { // DomainNamingContext in ADAM DB might be null. return (this.DomainNamingContext != null) ? this.DomainNamingContext.GetDnsName() : null; } } /// /// Gets the name of the forest that this domain controller is a member of. /// public string ForestName { get; private set; } /// /// Gets the SID of the domain. /// public SecurityIdentifier DomainSid { get; private set; } /// /// Gets the GUID of the domain. /// public Guid? DomainGuid { get; private set; } /// /// Gets the mode that this domain is operating in. /// public FunctionalLevel DomainMode { get; private set; } /// /// Gets the mode that this forest is operating in. /// public FunctionalLevel ForestMode { get; private set; } /// /// DSA Database Epoch /// public int? Epoch { get { return this.epochCache; } set { if(this.epochCache == null) { // This is a legacy DB without the epoch_col, so we cannot change it. // TODO: Extract as a resource. throw new InvalidOperationException("Current database does not support epoch information."); } // Update table this.systemTableCursor.BeginEditForUpdate(); this.systemTableCursor.EditRecord[epochCol] = value; this.systemTableCursor.AcceptChanges(); // Cache the value this.epochCache = value; } } // TODO: Nullable InvocationId? public Guid InvocationId { get; private set; } public Guid DsaGuid { get; private set; } /// /// Gets the name of the directory server. /// public string Name { get; private set; } public string DNSHostName { get; private set; } public DistinguishedName ServerReference { get; private set; } /// /// Backup expiration time. /// public DateTime? BackupExpiration { get { return this.backupExpirationCache; } set { // Update table this.systemTableCursor.BeginEditForUpdate(); this.systemTableCursor.SetValue(backupExpirationCol, value); this.systemTableCursor.AcceptChanges(); // Cache the value this.backupExpirationCache = value; } } /// /// Gets the wtitable partitions on this directory server. /// public string[] WritablePartitions { get; private set; } /// /// Gets the name of the site that this domain controller belongs to. /// public string SiteName { get; private set; } public DatabaseState State { get; private set; } public long? BackupUsn { get; private set; } public long? UsnAtIfm { get; private set; } public DomainControllerOptions Options { get; private set; } /// /// Determines if this domain controller is a global catalog server. /// public bool IsGlobalCatalog { get { return this.Options.HasFlag(DomainControllerOptions.GlobalCatalog); } } /// /// Gets or sets the highest update sequence number that has been committed to this domain controller. /// public long HighestCommittedUsn { get { return this.highestUSNCache; } set { // Update table this.systemTableCursor.BeginEditForUpdate(); this.systemTableCursor.EditRecord[highestCommitedUsnCol] = value; this.systemTableCursor.AcceptChanges(); // Cache the value this.highestUSNCache = value; } } /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { if (disposing) { if (this.systemTableCursor != null) { this.systemTableCursor.Dispose(); this.systemTableCursor = null; } } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } public int? DomainNamingContextDNT { get; private set; } public DistinguishedName DomainNamingContext { get; private set; } public DistinguishedName ConfigurationNamingContext { get; private set; } public DistinguishedName SchemaNamingContext { get; private set; } public string NetBIOSDomainName { get; private set; } public int? ServerReferenceDNT { get; private set; } public string OSName { get; private set; } public bool IsADAM { get; private set; } } }