File: System\DirectoryServices\ActiveDirectory\GlobalCatalog.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.Diagnostics;
using System.Runtime.InteropServices;

namespace System.DirectoryServices.ActiveDirectory
{
    public class GlobalCatalog : DomainController
    {
        // private variables
        private ActiveDirectorySchema? _schema;
        private bool _disabled;

        #region constructors
        internal GlobalCatalog(DirectoryContext context, string globalCatalogName)
            : base(context, globalCatalogName)
        { }

        internal GlobalCatalog(DirectoryContext context, string globalCatalogName, DirectoryEntryManager directoryEntryMgr)
            : base(context, globalCatalogName, directoryEntryMgr)
        { }
        #endregion constructors

        #region public methods

        public static GlobalCatalog GetGlobalCatalog(DirectoryContext context)
        {
            string? gcDnsName = null;
            bool isGlobalCatalog = false;
            DirectoryEntryManager? directoryEntryMgr = null;

            // check that the context argument is not null
            if (context == null)
                throw new ArgumentNullException(nameof(context));

            // target should be GC
            if (context.ContextType != DirectoryContextType.DirectoryServer)
            {
                throw new ArgumentException(SR.TargetShouldBeGC, nameof(context));
            }

            // target should be a server
            if (!(context.isServer()))
            {
                throw new ActiveDirectoryObjectNotFoundException(SR.Format(SR.GCNotFound, context.Name), typeof(GlobalCatalog), context.Name);
            }

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

            try
            {
                // Get dns name of the dc
                // by binding to root dse and getting the "dnsHostName" attribute
                // (also check that the "isGlobalCatalogReady" attribute is true)
                directoryEntryMgr = new DirectoryEntryManager(context);
                DirectoryEntry rootDSE = DirectoryEntryManager.GetDirectoryEntry(context, WellKnownDN.RootDSE);
                if (!Utils.CheckCapability(rootDSE, Capability.ActiveDirectory))
                {
                    throw new ActiveDirectoryObjectNotFoundException(SR.Format(SR.GCNotFound, context.Name), typeof(GlobalCatalog), context.Name);
                }

                gcDnsName = (string)PropertyManager.GetPropertyValue(context, rootDSE, PropertyManager.DnsHostName)!;
                isGlobalCatalog = (bool)bool.Parse((string)PropertyManager.GetPropertyValue(context, rootDSE, PropertyManager.IsGlobalCatalogReady)!);
                if (!isGlobalCatalog)
                {
                    throw new ActiveDirectoryObjectNotFoundException(SR.Format(SR.GCNotFound, context.Name), typeof(GlobalCatalog), context.Name);
                }
            }
            catch (COMException e)
            {
                int errorCode = e.ErrorCode;

                if (errorCode == unchecked((int)0x8007203a))
                {
                    throw new ActiveDirectoryObjectNotFoundException(SR.Format(SR.GCNotFound, context.Name), typeof(GlobalCatalog), context.Name);
                }
                else
                {
                    throw ExceptionHelper.GetExceptionFromCOMException(context, e);
                }
            }

            return new GlobalCatalog(context, gcDnsName, directoryEntryMgr);
        }

        public static new GlobalCatalog FindOne(DirectoryContext context)
        {
            ArgumentNullException.ThrowIfNull(context);

            if (context.ContextType != DirectoryContextType.Forest)
            {
                throw new ArgumentException(SR.TargetShouldBeForest, nameof(context));
            }

            return FindOneWithCredentialValidation(context, null, 0);
        }

        public static new GlobalCatalog FindOne(DirectoryContext context, string siteName)
        {
            ArgumentNullException.ThrowIfNull(context);

            if (context.ContextType != DirectoryContextType.Forest)
            {
                throw new ArgumentException(SR.TargetShouldBeForest, nameof(context));
            }

            ArgumentNullException.ThrowIfNull(siteName);

            return FindOneWithCredentialValidation(context, siteName, 0);
        }

        public static new GlobalCatalog FindOne(DirectoryContext context, LocatorOptions flag)
        {
            ArgumentNullException.ThrowIfNull(context);

            if (context.ContextType != DirectoryContextType.Forest)
            {
                throw new ArgumentException(SR.TargetShouldBeForest, nameof(context));
            }

            return FindOneWithCredentialValidation(context, null, flag);
        }

        public static new GlobalCatalog FindOne(DirectoryContext context, string siteName, LocatorOptions flag)
        {
            ArgumentNullException.ThrowIfNull(context);

            if (context.ContextType != DirectoryContextType.Forest)
            {
                throw new ArgumentException(SR.TargetShouldBeForest, nameof(context));
            }

            ArgumentNullException.ThrowIfNull(siteName);

            return FindOneWithCredentialValidation(context, siteName, flag);
        }

        public static new GlobalCatalogCollection FindAll(DirectoryContext context)
        {
            ArgumentNullException.ThrowIfNull(context);

            if (context.ContextType != DirectoryContextType.Forest)
            {
                throw new ArgumentException(SR.TargetShouldBeForest, nameof(context));
            }

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

            return FindAllInternal(context, null);
        }

        public static new GlobalCatalogCollection FindAll(DirectoryContext context, string siteName)
        {
            ArgumentNullException.ThrowIfNull(context);

            if (context.ContextType != DirectoryContextType.Forest)
            {
                throw new ArgumentException(SR.TargetShouldBeForest, nameof(context));
            }

            ArgumentNullException.ThrowIfNull(siteName);

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

            return FindAllInternal(context, siteName);
        }

        public override GlobalCatalog EnableGlobalCatalog()
        {
            CheckIfDisposed();
            throw new InvalidOperationException(SR.CannotPerformOnGCObject);
        }

        public DomainController DisableGlobalCatalog()
        {
            CheckIfDisposed();
            CheckIfDisabled();

            // bind to the server object
            DirectoryEntry serverNtdsaEntry = directoryEntryMgr.GetCachedDirectoryEntry(NtdsaObjectName);

            // reset the NTDSDSA_OPT_IS_GC flag on the "options" property
            int options = 0;

            try
            {
                if (serverNtdsaEntry.Properties[PropertyManager.Options].Value != null)
                {
                    options = (int)serverNtdsaEntry.Properties[PropertyManager.Options].Value!;
                }

                serverNtdsaEntry.Properties[PropertyManager.Options].Value = options & (~1);
                serverNtdsaEntry.CommitChanges();
            }
            catch (COMException e)
            {
                throw ExceptionHelper.GetExceptionFromCOMException(context, e);
            }

            // mark as disbaled
            _disabled = true;

            // return a domain controller object
            return new DomainController(context, Name);
        }

        public override bool IsGlobalCatalog()
        {
            CheckIfDisposed();
            CheckIfDisabled();

            // since this is a global catalog object, this should always return true
            return true;
        }

        public ReadOnlyActiveDirectorySchemaPropertyCollection FindAllProperties()
        {
            CheckIfDisposed();
            CheckIfDisabled();

            // create an ActiveDirectorySchema object
            if (_schema == null)
            {
                string? schemaNC = null;
                try
                {
                    schemaNC = directoryEntryMgr.ExpandWellKnownDN(WellKnownDN.SchemaNamingContext);
                }
                catch (COMException e)
                {
                    throw ExceptionHelper.GetExceptionFromCOMException(context, e);
                }
                _ = Utils.GetNewDirectoryContext(Name, DirectoryContextType.DirectoryServer, context);
                _schema = new ActiveDirectorySchema(context, schemaNC);
            }

            // return the global catalog replicated properties
            return _schema.FindAllProperties(PropertyTypes.InGlobalCatalog);
        }

        public override DirectorySearcher GetDirectorySearcher()
        {
            CheckIfDisposed();
            CheckIfDisabled();

            return InternalGetDirectorySearcher();
        }

        #endregion public methods

        #region private methods

        private void CheckIfDisabled()
        {
            if (_disabled)
            {
                throw new InvalidOperationException(SR.GCDisabled);
            }
        }

        internal static new GlobalCatalog FindOneWithCredentialValidation(DirectoryContext context, string? siteName, LocatorOptions flag)
        {
            GlobalCatalog gc;
            bool retry = false;
            bool credsValidated = false;

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

            // authenticate against this GC to validate the credentials
            gc = FindOneInternal(context, context.Name, siteName, flag);
            try
            {
                ValidateCredential(gc, context);
                credsValidated = true;
            }
            catch (COMException e)
            {
                if (e.ErrorCode == unchecked((int)0x8007203a))
                {
                    // server is down , so try again with force rediscovery if the flags did not already contain force rediscovery
                    if ((flag & LocatorOptions.ForceRediscovery) == 0)
                    {
                        retry = true;
                    }
                    else
                    {
                        throw new ActiveDirectoryObjectNotFoundException(SR.Format(SR.GCNotFoundInForest, context.Name), typeof(GlobalCatalog), null);
                    }
                }
                else
                {
                    throw ExceptionHelper.GetExceptionFromCOMException(context, e);
                }
            }
            finally
            {
                if (!credsValidated)
                {
                    gc.Dispose();
                }
            }

            if (retry)
            {
                credsValidated = false;
                gc = FindOneInternal(context, context.Name, siteName, flag | LocatorOptions.ForceRediscovery);
                try
                {
                    ValidateCredential(gc, context);
                    credsValidated = true;
                }
                catch (COMException e)
                {
                    if (e.ErrorCode == unchecked((int)0x8007203a))
                    {
                        // server is down
                        throw new ActiveDirectoryObjectNotFoundException(SR.Format(SR.GCNotFoundInForest, context.Name), typeof(GlobalCatalog), null);
                    }
                    else
                    {
                        throw ExceptionHelper.GetExceptionFromCOMException(context, e);
                    }
                }
                finally
                {
                    if (!credsValidated)
                    {
                        gc.Dispose();
                    }
                }
            }

            return gc;
        }

        internal static new GlobalCatalog FindOneInternal(DirectoryContext context, string? forestName, string? siteName, LocatorOptions flag)
        {
            DomainControllerInfo domainControllerInfo;
            int errorCode = 0;

            if (siteName != null && siteName.Length == 0)
            {
                throw new ArgumentException(SR.EmptyStringParameter, nameof(siteName));
            }

            // check that the flags passed have only the valid bits set
            if (((long)flag & (~((long)LocatorOptions.AvoidSelf | (long)LocatorOptions.ForceRediscovery | (long)LocatorOptions.KdcRequired | (long)LocatorOptions.TimeServerRequired | (long)LocatorOptions.WriteableRequired))) != 0)
            {
                throw new ArgumentException(SR.InvalidFlags, nameof(flag));
            }

            if (forestName == null)
            {
                // get the dns name of the logged on forest
                DomainControllerInfo tempDomainControllerInfo;
                int error = Locator.DsGetDcNameWrapper(null, DirectoryContext.GetLoggedOnDomain(), null, (long)PrivateLocatorFlags.DirectoryServicesRequired, out tempDomainControllerInfo);

                if (error == Interop.Errors.ERROR_NO_SUCH_DOMAIN)
                {
                    // throw not found exception
                    throw new ActiveDirectoryObjectNotFoundException(SR.ContextNotAssociatedWithDomain, typeof(GlobalCatalog), null);
                }
                else if (error != 0)
                {
                    throw ExceptionHelper.GetExceptionFromErrorCode(errorCode);
                }

                Debug.Assert(tempDomainControllerInfo.DnsForestName != null);
                forestName = tempDomainControllerInfo.DnsForestName;
            }

            // call DsGetDcName
            errorCode = Locator.DsGetDcNameWrapper(null, forestName, siteName, (long)flag | (long)(PrivateLocatorFlags.GCRequired | PrivateLocatorFlags.DirectoryServicesRequired), out domainControllerInfo);

            if (errorCode == Interop.Errors.ERROR_NO_SUCH_DOMAIN)
            {
                throw new ActiveDirectoryObjectNotFoundException(SR.Format(SR.GCNotFoundInForest, forestName), typeof(GlobalCatalog), null);
            }
            // this can only occur when flag is being explicitly passed (since the flags that we pass internally are valid)
            if (errorCode == Interop.Errors.ERROR_INVALID_FLAGS)
            {
                throw new ArgumentException(SR.InvalidFlags, nameof(flag));
            }
            else if (errorCode != 0)
            {
                throw ExceptionHelper.GetExceptionFromErrorCode(errorCode);
            }

            // create a GlobalCatalog object
            // the name is returned in the form "\\servername", so skip the "\\"
            Debug.Assert(domainControllerInfo.DomainControllerName.Length > 2);
            string globalCatalogName = domainControllerInfo.DomainControllerName.Substring(2);

            // create a new context object for the global catalog
            DirectoryContext gcContext = Utils.GetNewDirectoryContext(globalCatalogName, DirectoryContextType.DirectoryServer, context);

            return new GlobalCatalog(gcContext, globalCatalogName);
        }

        internal static GlobalCatalogCollection FindAllInternal(DirectoryContext context, string? siteName)
        {
            ArrayList gcList = new ArrayList();

            if (siteName != null && siteName.Length == 0)
            {
                throw new ArgumentException(SR.EmptyStringParameter, nameof(siteName));
            }

            foreach (string gcName in Utils.GetReplicaList(context, null /* not specific to any partition */, siteName, false /* isDefaultNC */, false /* isADAM */, true /* mustBeGC */))
            {
                DirectoryContext gcContext = Utils.GetNewDirectoryContext(gcName, DirectoryContextType.DirectoryServer, context);
                gcList.Add(new GlobalCatalog(gcContext, gcName));
            }

            return new GlobalCatalogCollection(gcList);
        }

        private DirectorySearcher InternalGetDirectorySearcher()
        {
            DirectoryEntry de = new DirectoryEntry("GC://" + Name);

            de.AuthenticationType = Utils.DefaultAuthType | AuthenticationTypes.ServerBind;

            de.Username = context.UserName;
            de.Password = context.Password;

            return new DirectorySearcher(de);
        }

        #endregion
    }
}