File: System\DirectoryServices\ActiveDirectory\ActiveDirectorySchema.cs
Web Access
Project: src\src\runtime\src\libraries\System.DirectoryServices\src\System.DirectoryServices.csproj (System.DirectoryServices)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Text;

namespace System.DirectoryServices.ActiveDirectory
{
    public enum SchemaClassType : int
    {
        Type88 = 0,
        Structural = 1,
        Abstract = 2,
        Auxiliary = 3
    }

    [Flags]
    public enum PropertyTypes : int
    {
        Indexed = 2,
        InGlobalCatalog = 4
    }

    public class ActiveDirectorySchema : ActiveDirectoryPartition
    {
        private bool _disposed;
        private DirectoryEntry _schemaEntry;
        private DirectoryEntry? _abstractSchemaEntry;
        private DirectoryServer? _cachedSchemaRoleOwner;

        #region constructors
        internal ActiveDirectorySchema(DirectoryContext context, string distinguishedName)
            : base(context, distinguishedName)
        {
            this.directoryEntryMgr = new DirectoryEntryManager(context);
            _schemaEntry = DirectoryEntryManager.GetDirectoryEntry(context, distinguishedName);
        }

        internal ActiveDirectorySchema(DirectoryContext context, string distinguishedName, DirectoryEntryManager directoryEntryMgr)
            : base(context, distinguishedName)
        {
            this.directoryEntryMgr = directoryEntryMgr;
            _schemaEntry = DirectoryEntryManager.GetDirectoryEntry(context, distinguishedName);
        }
        #endregion constructors

        #region IDisposable
        // private Dispose method
        protected override void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                try
                {
                    // if there are any managed or unmanaged
                    // resources to be freed, those should be done here
                    // if not an explicit dispose only unmanaged resources should
                    // be disposed
                    if (disposing)
                    {
                        // dispose schema entry
                        if (_schemaEntry != null)
                        {
                            _schemaEntry.Dispose();
                            _schemaEntry = null!;
                        }
                        // dispose the abstract schema entry
                        if (_abstractSchemaEntry != null)
                        {
                            _abstractSchemaEntry.Dispose();
                            _abstractSchemaEntry = null;
                        }
                    }
                    _disposed = true;
                }
                finally
                {
                    base.Dispose();
                }
            }
        }
        #endregion IDisposable

        #region public methods
        public static ActiveDirectorySchema GetSchema(DirectoryContext context)
        {
            ArgumentNullException.ThrowIfNull(context);

            // contexttype should be Forest, DirectoryServer or ConfigurationSet
            if ((context.ContextType != DirectoryContextType.Forest) &&
                (context.ContextType != DirectoryContextType.ConfigurationSet) &&
                (context.ContextType != DirectoryContextType.DirectoryServer))
            {
                throw new ArgumentException(SR.NotADOrADAM, nameof(context));
            }

            if ((context.Name == null) && (!context.isRootDomain()))
            {
                throw new ActiveDirectoryObjectNotFoundException(SR.ContextNotAssociatedWithDomain, typeof(ActiveDirectorySchema), null);
            }

            if (context.Name != null)
            {
                // the target should be a valid forest name or a server
                if (!((context.isRootDomain()) || (context.isADAMConfigSet()) || (context.isServer())))
                {
                    if (context.ContextType == DirectoryContextType.Forest)
                    {
                        throw new ActiveDirectoryObjectNotFoundException(SR.ForestNotFound, typeof(ActiveDirectorySchema), context.Name);
                    }
                    else if (context.ContextType == DirectoryContextType.ConfigurationSet)
                    {
                        throw new ActiveDirectoryObjectNotFoundException(SR.ConfigSetNotFound, typeof(ActiveDirectorySchema), context.Name);
                    }
                    else
                    {
                        throw new ActiveDirectoryObjectNotFoundException(SR.Format(SR.ServerNotFound, context.Name), typeof(ActiveDirectorySchema), null);
                    }
                }
            }

            //  work with copy of the context
            context = new DirectoryContext(context);

            DirectoryEntryManager directoryEntryMgr = new DirectoryEntryManager(context);
            string schemaNC;
            try
            {
                DirectoryEntry rootDSE = directoryEntryMgr.GetCachedDirectoryEntry(WellKnownDN.RootDSE);

                if ((context.isServer()) && (!Utils.CheckCapability(rootDSE, Capability.ActiveDirectoryOrADAM)))
                {
                    throw new ActiveDirectoryObjectNotFoundException(SR.Format(SR.ServerNotFound, context.Name), typeof(ActiveDirectorySchema), null);
                }

                schemaNC = (string)PropertyManager.GetPropertyValue(context, rootDSE, PropertyManager.SchemaNamingContext)!;
            }
            catch (COMException e)
            {
                int errorCode = e.ErrorCode;

                if (errorCode == unchecked((int)0x8007203a))
                {
                    if (context.ContextType == DirectoryContextType.Forest)
                    {
                        throw new ActiveDirectoryObjectNotFoundException(SR.ForestNotFound, typeof(ActiveDirectorySchema), context.Name);
                    }
                    else if (context.ContextType == DirectoryContextType.ConfigurationSet)
                    {
                        throw new ActiveDirectoryObjectNotFoundException(SR.ConfigSetNotFound, typeof(ActiveDirectorySchema), context.Name);
                    }
                    else
                    {
                        throw new ActiveDirectoryObjectNotFoundException(SR.Format(SR.ServerNotFound, context.Name), typeof(ActiveDirectorySchema), null);
                    }
                }
                else
                {
                    throw ExceptionHelper.GetExceptionFromCOMException(context, e);
                }
            }
            catch (ActiveDirectoryObjectNotFoundException)
            {
                if (context.ContextType == DirectoryContextType.ConfigurationSet)
                {
                    // this is the case where the context is a config set and we could not find an ADAM instance in that config set
                    throw new ActiveDirectoryObjectNotFoundException(SR.ConfigSetNotFound, typeof(ActiveDirectorySchema), context.Name);
                }
                else
                    throw;
            }

            return new ActiveDirectorySchema(context, schemaNC, directoryEntryMgr);
        }

        public void RefreshSchema()
        {
            CheckIfDisposed();

            // Refresh the schema on the server
            DirectoryEntry? rootDSE = null;
            try
            {
                rootDSE = DirectoryEntryManager.GetDirectoryEntry(context, WellKnownDN.RootDSE);
                rootDSE.Properties[PropertyManager.SchemaUpdateNow].Value = 1;
                rootDSE.CommitChanges();

                // refresh the schema on the client
                // bind to the abstract schema
                _abstractSchemaEntry ??= directoryEntryMgr.GetCachedDirectoryEntry("Schema");
                _abstractSchemaEntry.RefreshCache();
            }
            catch (COMException e)
            {
                throw ExceptionHelper.GetExceptionFromCOMException(context, e);
            }
            finally
            {
                rootDSE?.Dispose();
            }
        }

        //
        // This method finds only among non-defunct classes
        //
        public ActiveDirectorySchemaClass FindClass(string ldapDisplayName)
        {
            CheckIfDisposed();
            return ActiveDirectorySchemaClass.FindByName(context, ldapDisplayName);
        }

        //
        // This method finds only among defunct classes
        //
        public ActiveDirectorySchemaClass FindDefunctClass(string commonName)
        {
            CheckIfDisposed();

            ArgumentNullException.ThrowIfNull(commonName);

            if (commonName.Length == 0)
            {
                throw new ArgumentException(SR.EmptyStringParameter, nameof(commonName));
            }

            // this will bind to the schema container and load the properties of this class
            // (will also check whether or not the class exists)
            Hashtable propertiesFromServer = ActiveDirectorySchemaClass.GetPropertiesFromSchemaContainer(context, _schemaEntry, commonName, true /* isDefunctOnServer */);
            ActiveDirectorySchemaClass schemaClass = new ActiveDirectorySchemaClass(context, commonName, propertiesFromServer, _schemaEntry);

            return schemaClass;
        }

        //
        // This method returns  only non-defunct classes
        //
        public ReadOnlyActiveDirectorySchemaClassCollection FindAllClasses()
        {
            CheckIfDisposed();
            string filter = "(&(" + PropertyManager.ObjectCategory + "=classSchema)" +
                            "(!(" + PropertyManager.IsDefunct + "=TRUE)))";
            return GetAllClasses(context, _schemaEntry, filter);
        }

        //
        // This method returns only non-defunct classes of the specified type
        //
        public ReadOnlyActiveDirectorySchemaClassCollection FindAllClasses(SchemaClassType type)
        {
            CheckIfDisposed();

            // validate the type
            if (type < SchemaClassType.Type88 || type > SchemaClassType.Auxiliary)
            {
                throw new InvalidEnumArgumentException(nameof(type), (int)type, typeof(SchemaClassType));
            }

            string filter = "(&(" + PropertyManager.ObjectCategory + "=classSchema)" +
                            "(" + PropertyManager.ObjectClassCategory + "=" + (int)type + ")" +
                            "(!(" + PropertyManager.IsDefunct + "=TRUE)))";
            return GetAllClasses(context, _schemaEntry, filter);
        }

        //
        // This method returns only defunct classes
        //
        public ReadOnlyActiveDirectorySchemaClassCollection FindAllDefunctClasses()
        {
            CheckIfDisposed();

            string filter = "(&(" + PropertyManager.ObjectCategory + "=classSchema)" +
                "(" + PropertyManager.IsDefunct + "=TRUE))";
            return GetAllClasses(context, _schemaEntry, filter);
        }

        //
        // This method finds only among non-defunct properties
        //
        public ActiveDirectorySchemaProperty FindProperty(string ldapDisplayName)
        {
            CheckIfDisposed();
            return ActiveDirectorySchemaProperty.FindByName(context, ldapDisplayName);
        }

        //
        // This method finds only among defunct properties
        //
        public ActiveDirectorySchemaProperty FindDefunctProperty(string commonName)
        {
            CheckIfDisposed();

            ArgumentNullException.ThrowIfNull(commonName);

            if (commonName.Length == 0)
            {
                throw new ArgumentException(SR.EmptyStringParameter, nameof(commonName));
            }

            // this will bind to the schema container and load the properties of this property
            // (will also check whether or not the property exists)
            SearchResult propertiesFromServer = ActiveDirectorySchemaProperty.GetPropertiesFromSchemaContainer(context, _schemaEntry, commonName, true /* isDefunctOnServer */);
            ActiveDirectorySchemaProperty schemaProperty = new ActiveDirectorySchemaProperty(context, commonName, propertiesFromServer, _schemaEntry);

            return schemaProperty;
        }

        //
        // This method returns  only non-defunct properties
        //
        public ReadOnlyActiveDirectorySchemaPropertyCollection FindAllProperties()
        {
            CheckIfDisposed();

            string filter = "(&(" + PropertyManager.ObjectCategory + "=attributeSchema)" +
                            "(!(" + PropertyManager.IsDefunct + "=TRUE)))";
            return GetAllProperties(context, _schemaEntry, filter);
        }

        //
        // This method returns  only non-defunct properties meeting the specified criteria
        //
        public ReadOnlyActiveDirectorySchemaPropertyCollection FindAllProperties(PropertyTypes type)
        {
            CheckIfDisposed();

            // check validity of type
            if ((type & (~(PropertyTypes.Indexed | PropertyTypes.InGlobalCatalog))) != 0)
            {
                throw new ArgumentException(SR.InvalidFlags, nameof(type));
            }

            // start the filter
            StringBuilder str = new StringBuilder(25);
            str.Append("(&(");
            str.Append(PropertyManager.ObjectCategory);
            str.Append("=attributeSchema)");
            str.Append("(!(");
            str.Append(PropertyManager.IsDefunct);
            str.Append("=TRUE))");

            if (((int)type & (int)PropertyTypes.Indexed) != 0)
            {
                str.Append('(');
                str.Append(PropertyManager.SearchFlags);
                str.Append(":1.2.840.113556.1.4.804:=");
                str.Append((int)SearchFlags.IsIndexed);
                str.Append(')');
            }

            if (((int)type & (int)PropertyTypes.InGlobalCatalog) != 0)
            {
                str.Append('(');
                str.Append(PropertyManager.IsMemberOfPartialAttributeSet);
                str.Append("=TRUE)");
            }

            str.Append(')'); // end filter
            return GetAllProperties(context, _schemaEntry, str.ToString());
        }

        //
        // This method returns only defunct properties
        //
        public ReadOnlyActiveDirectorySchemaPropertyCollection FindAllDefunctProperties()
        {
            CheckIfDisposed();

            string filter = "(&(" + PropertyManager.ObjectCategory + "=attributeSchema)" +
                            "(" + PropertyManager.IsDefunct + "=TRUE))";
            return GetAllProperties(context, _schemaEntry, filter);
        }

        public override DirectoryEntry GetDirectoryEntry()
        {
            CheckIfDisposed();
            return DirectoryEntryManager.GetDirectoryEntry(context, Name);
        }

        public static ActiveDirectorySchema GetCurrentSchema()
        {
            return ActiveDirectorySchema.GetSchema(new DirectoryContext(DirectoryContextType.Forest));
        }

        #endregion public methods

        #region public properties

        public DirectoryServer SchemaRoleOwner
        {
            get
            {
                CheckIfDisposed();
                return _cachedSchemaRoleOwner ??= GetSchemaRoleOwner();
            }
        }

        #endregion public properties

        #region private methods
        internal static ReadOnlyActiveDirectorySchemaPropertyCollection GetAllProperties(DirectoryContext context, DirectoryEntry schemaEntry, string filter)
        {
            ArrayList propertyList = new ArrayList();

            string[] propertiesToLoad = new string[3];
            propertiesToLoad[0] = PropertyManager.LdapDisplayName;
            propertiesToLoad[1] = PropertyManager.Cn;
            propertiesToLoad[2] = PropertyManager.IsDefunct;

            ADSearcher searcher = new ADSearcher(schemaEntry, filter, propertiesToLoad, SearchScope.OneLevel);
            SearchResultCollection? resCol = null;
            try
            {
                resCol = searcher.FindAll();
                foreach (SearchResult res in resCol)
                {
                    string ldapDisplayName = (string)PropertyManager.GetSearchResultPropertyValue(res, PropertyManager.LdapDisplayName)!;
                    DirectoryEntry directoryEntry = res.GetDirectoryEntry();

                    directoryEntry.AuthenticationType = Utils.DefaultAuthType;
                    directoryEntry.Username = context.UserName;
                    directoryEntry.Password = context.Password;

                    bool isDefunct = false;

                    if ((res.Properties[PropertyManager.IsDefunct] != null) && (res.Properties[PropertyManager.IsDefunct].Count > 0))
                    {
                        isDefunct = (bool)res.Properties[PropertyManager.IsDefunct][0]!;
                    }

                    if (isDefunct)
                    {
                        string commonName = (string)PropertyManager.GetSearchResultPropertyValue(res, PropertyManager.Cn)!;
                        propertyList.Add(new ActiveDirectorySchemaProperty(context, commonName, ldapDisplayName, directoryEntry, schemaEntry));
                    }
                    else
                    {
                        propertyList.Add(new ActiveDirectorySchemaProperty(context, ldapDisplayName, directoryEntry, schemaEntry));
                    }
                }
            }
            catch (COMException e)
            {
                throw ExceptionHelper.GetExceptionFromCOMException(context, e);
            }
            finally
            {
                // dispose off the result collection
                resCol?.Dispose();
            }

            return new ReadOnlyActiveDirectorySchemaPropertyCollection(propertyList);
        }

        internal static ReadOnlyActiveDirectorySchemaClassCollection GetAllClasses(DirectoryContext context, DirectoryEntry schemaEntry, string filter)
        {
            ArrayList classList = new ArrayList();

            string[] propertiesToLoad = new string[3];
            propertiesToLoad[0] = PropertyManager.LdapDisplayName;
            propertiesToLoad[1] = PropertyManager.Cn;
            propertiesToLoad[2] = PropertyManager.IsDefunct;

            ADSearcher searcher = new ADSearcher(schemaEntry, filter, propertiesToLoad, SearchScope.OneLevel);
            SearchResultCollection? resCol = null;
            try
            {
                resCol = searcher.FindAll();
                foreach (SearchResult res in resCol)
                {
                    string ldapDisplayName = (string)PropertyManager.GetSearchResultPropertyValue(res, PropertyManager.LdapDisplayName)!;
                    DirectoryEntry directoryEntry = res.GetDirectoryEntry();

                    directoryEntry.AuthenticationType = Utils.DefaultAuthType;
                    directoryEntry.Username = context.UserName;
                    directoryEntry.Password = context.Password;

                    bool isDefunct = false;

                    if ((res.Properties[PropertyManager.IsDefunct] != null) && (res.Properties[PropertyManager.IsDefunct].Count > 0))
                    {
                        isDefunct = (bool)res.Properties[PropertyManager.IsDefunct][0]!;
                    }

                    if (isDefunct)
                    {
                        string commonName = (string)PropertyManager.GetSearchResultPropertyValue(res, PropertyManager.Cn)!;
                        classList.Add(new ActiveDirectorySchemaClass(context, commonName, ldapDisplayName, directoryEntry, schemaEntry));
                    }
                    else
                    {
                        classList.Add(new ActiveDirectorySchemaClass(context, ldapDisplayName, directoryEntry, schemaEntry));
                    }
                }
            }
            catch (COMException e)
            {
                throw ExceptionHelper.GetExceptionFromCOMException(context, e);
            }
            finally
            {
                // dispose off the result collection
                resCol?.Dispose();
            }

            return new ReadOnlyActiveDirectorySchemaClassCollection(classList);
        }

        private DirectoryServer GetSchemaRoleOwner()
        {
            try
            {
                _schemaEntry.RefreshCache();

                if (context.isADAMConfigSet())
                {
                    // ADAM
                    string adamInstName = Utils.GetAdamDnsHostNameFromNTDSA(context, (string)PropertyManager.GetPropertyValue(context, _schemaEntry, PropertyManager.FsmoRoleOwner)!);
                    DirectoryContext adamInstContext = Utils.GetNewDirectoryContext(adamInstName, DirectoryContextType.DirectoryServer, context);
                    return new AdamInstance(adamInstContext, adamInstName);
                }
                else
                {
                    // could be AD or adam server

                    DirectoryServer? server = null;
                    DirectoryEntry rootDSE = directoryEntryMgr.GetCachedDirectoryEntry(WellKnownDN.RootDSE);

                    if (Utils.CheckCapability(rootDSE, Capability.ActiveDirectory))
                    {
                        string dcName = Utils.GetDnsHostNameFromNTDSA(context, (string)PropertyManager.GetPropertyValue(context, _schemaEntry, PropertyManager.FsmoRoleOwner)!);
                        DirectoryContext dcContext = Utils.GetNewDirectoryContext(dcName, DirectoryContextType.DirectoryServer, context);
                        server = new DomainController(dcContext, dcName);
                    }
                    else
                    {
                        // ADAM case again
                        string adamInstName = Utils.GetAdamDnsHostNameFromNTDSA(context, (string)PropertyManager.GetPropertyValue(context, _schemaEntry, PropertyManager.FsmoRoleOwner)!);
                        DirectoryContext adamInstContext = Utils.GetNewDirectoryContext(adamInstName, DirectoryContextType.DirectoryServer, context);
                        server = new AdamInstance(adamInstContext, adamInstName);
                    }
                    return server;
                }
            }
            catch (COMException e)
            {
                throw ExceptionHelper.GetExceptionFromCOMException(context, e);
            }
        }

        #endregion private methods
    }
}