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

using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace System.DirectoryServices.AccountManagement
{
    [DirectoryRdnPrefix("CN")]
    public class GroupPrincipal : Principal
    {
        //
        // Public constructors
        //
        public GroupPrincipal(PrincipalContext context)
        {
            if (context == null)
                throw new ArgumentException(SR.NullArguments);

            this.ContextRaw = context;
            this.unpersisted = true;
        }

        public GroupPrincipal(PrincipalContext context, string samAccountName) : this(context)
        {
            if (samAccountName == null)
                throw new ArgumentException(SR.NullArguments);

            if (Context.ContextType != ContextType.ApplicationDirectory)
                this.SamAccountName = samAccountName;

            this.Name = samAccountName;
        }

        //
        // Public properties
        //

        // IsSecurityGroup property
        private bool _isSecurityGroup;          // the actual property value
        private LoadState _isSecurityGroupChanged = LoadState.NotSet;   // change-tracking

        public Nullable<bool> IsSecurityGroup
        {
            get
            {
                // Make sure we're not disposed or deleted.  Although HandleGet/HandleSet will check this,
                // we need to check these before we do anything else.
                CheckDisposedOrDeleted();

                // Different stores have different defaults as to the Enabled setting
                // (AD: creates disabled by default; SAM: creates enabled by default).
                // So if the principal is unpersisted (and thus we may not know what store it's
                // going to end up in), we'll just return null unless they previously
                // set an explicit value.
                if (this.unpersisted && (_isSecurityGroupChanged != LoadState.Changed))
                {
                    GlobalDebug.WriteLineIf(
                                    GlobalDebug.Info,
                                    "Group",
                                    "Enabled: returning null, unpersisted={0}, enabledChanged={1}",
                                    this.unpersisted,
                                    _isSecurityGroupChanged);
                    return null;
                }

                return HandleGet<bool>(ref _isSecurityGroup, PropertyNames.GroupIsSecurityGroup, ref _isSecurityGroupChanged);
            }

            set
            {
                // Make sure we're not disposed or deleted.  Although HandleGet/HandleSet will check this,
                // we need to check these before we do anything else.
                CheckDisposedOrDeleted();

                // We don't want to let them set a null value.
                if (!value.HasValue)
                    throw new ArgumentNullException(nameof(value));

                HandleSet<bool>(ref _isSecurityGroup, value.Value, ref _isSecurityGroupChanged, PropertyNames.GroupIsSecurityGroup);
            }
        }

        // GroupScope property
        private GroupScope _groupScope = System.DirectoryServices.AccountManagement.GroupScope.Local;      // the actual property value
        private LoadState _groupScopeChanged = LoadState.NotSet;              // change-tracking

        public Nullable<GroupScope> GroupScope
        {
            get
            {
                // Make sure we're not disposed or deleted.  Although HandleGet/HandleSet will check this,
                // we need to check these before we do anything else.
                CheckDisposedOrDeleted();

                // Different stores have different defaults for the GroupScope setting
                // (AD: Global; SAM: Local).
                // So if the principal is unpersisted (and thus we may not know what store it's
                // going to end up in), we'll just return null unless they previously
                // set an explicit value.
                if (this.unpersisted && (_groupScopeChanged != LoadState.Changed))
                {
                    GlobalDebug.WriteLineIf(
                                    GlobalDebug.Info,
                                    "Group",
                                    "GroupScope: returning null, unpersisted={0}, groupScopeChanged={1}",
                                    this.unpersisted,
                                    _groupScopeChanged);

                    return null;
                }

                return HandleGet<GroupScope>(ref _groupScope, PropertyNames.GroupGroupScope, ref _groupScopeChanged);
            }

            set
            {
                // Make sure we're not disposed or deleted.  Although HandleGet/HandleSet will check this,
                // we need to check these before we do anything else.
                CheckDisposedOrDeleted();

                // We don't want to let them set a null value.
                if (!value.HasValue)
                    throw new ArgumentNullException(nameof(value));

                HandleSet<GroupScope>(ref _groupScope, value.Value, ref _groupScopeChanged, PropertyNames.GroupGroupScope);
            }
        }

        // Members property
        private PrincipalCollection _members;

        public PrincipalCollection Members
        {
            get
            {
                // We don't use HandleGet<T> here because we have to load in the PrincipalCollection
                // using a special procedure.  It's not loaded as part of the regular LoadIfNeeded call.

                // Make sure we're not disposed or deleted.
                CheckDisposedOrDeleted();

                // Check that we actually support this property in our store
                //CheckSupportedProperty(PropertyNames.GroupMembers);

                if (_members == null)
                {
                    GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "Members: creating fresh PrincipalCollection");

                    if (!this.unpersisted)
                    {
                        // Retrieve the members from the store.
                        // QueryCtx because when this group was retrieved, it was
                        // assigned a _specific_ context for its store
                        GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "Members: persisted, querying group membership");

                        BookmarkableResultSet refs = ContextRaw.QueryCtx.GetGroupMembership(this, false);
                        _members = new PrincipalCollection(refs, this);
                    }
                    else
                    {
                        // unpersisted means there's no values to retrieve from the store
                        GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "Members: unpersisted, creating empty PrincipalCollection");
                        _members = new PrincipalCollection(new EmptySet(), this);
                    }
                }

                return _members;
            }
        }

        //
        // Public methods
        //
        public static new GroupPrincipal FindByIdentity(PrincipalContext context, string identityValue)
        {
            return (GroupPrincipal)FindByIdentityWithType(context, typeof(GroupPrincipal), identityValue);
        }

        public static new GroupPrincipal FindByIdentity(PrincipalContext context, IdentityType identityType, string identityValue)
        {
            return (GroupPrincipal)FindByIdentityWithType(context, typeof(GroupPrincipal), identityType, identityValue);
        }

        public PrincipalSearchResult<Principal> GetMembers()
        {
            return GetMembers(false);
        }

        public PrincipalSearchResult<Principal> GetMembers(bool recursive)
        {
            // Make sure we're not disposed or deleted.
            CheckDisposedOrDeleted();

            if (!this.unpersisted)
            {
                // Retrieve the members from the store.
                // QueryCtx because when this group was retrieved, it was
                // assigned a _specific_ context for its store
                GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "GetMembers: persisted, querying for members (recursive={0}", recursive);

                return new PrincipalSearchResult<Principal>(ContextRaw.QueryCtx.GetGroupMembership(this, recursive));
            }
            else
            {
                // unpersisted means there's no values to retrieve from the store
                GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "GetMembers: unpersisted, creating empty PrincipalSearchResult");

                return new PrincipalSearchResult<Principal>(null);
            }
        }

        private bool _disposed;

        public override void Dispose()
        {
            try
            {
                if (!_disposed)
                {
                    GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "Dispose: disposing");

                    _members?.Dispose();

                    _disposed = true;
                    GC.SuppressFinalize(this);
                }
            }
            finally
            {
                base.Dispose();
            }
        }

        //
        // Internal "constructor": Used for constructing Groups returned by a query
        //
        internal static GroupPrincipal MakeGroup(PrincipalContext ctx)
        {
            GroupPrincipal g = new GroupPrincipal(ctx);
            g.unpersisted = false;

            return g;
        }

        //
        // Load/Store implementation
        //

        //
        // Loading with query results
        //

        internal override void LoadValueIntoProperty(string propertyName, object value)
        {
            GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "LoadValueIntoProperty: name=" + propertyName + " value=" + (value != null ? value.ToString() : "null"));

            switch (propertyName)
            {
                case PropertyNames.GroupIsSecurityGroup:
                    _isSecurityGroup = (bool)value;
                    _isSecurityGroupChanged = LoadState.Loaded;
                    break;

                case PropertyNames.GroupGroupScope:
                    _groupScope = (GroupScope)value;
                    _groupScopeChanged = LoadState.Loaded;
                    break;

                case PropertyNames.GroupMembers:
                    Debug.Fail("Group.LoadValueIntoProperty: Trying to load Members, but Members is demand-loaded.");
                    break;

                default:
                    base.LoadValueIntoProperty(propertyName, value);
                    break;
            }
        }

        //
        // Getting changes to persist (or to build a query from a QBE filter)
        //

        // Given a property name, returns true if that property has changed since it was loaded, false otherwise.
        internal override bool GetChangeStatusForProperty(string propertyName)
        {
            GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "GetChangeStatusForProperty: name=" + propertyName);

            switch (propertyName)
            {
                case PropertyNames.GroupIsSecurityGroup:
                    return _isSecurityGroupChanged == LoadState.Changed;

                case PropertyNames.GroupGroupScope:
                    return _groupScopeChanged == LoadState.Changed;

                case PropertyNames.GroupMembers:
                    // If Members was never loaded, it couldn't possibly have changed
                    if (_members != null)
                        return _members.Changed;
                    else
                    {
                        GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "GetChangeStatusForProperty: members was never loaded");
                        return false;
                    }

                default:
                    return base.GetChangeStatusForProperty(propertyName);
            }
        }

        // Given a property name, returns the current value for the property.
        internal override object GetValueForProperty(string propertyName)
        {
            GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "GetValueForProperty: name=" + propertyName);

            return propertyName switch
            {
                PropertyNames.GroupIsSecurityGroup => _isSecurityGroup,
                PropertyNames.GroupGroupScope => _groupScope,
                PropertyNames.GroupMembers => _members,
                _ => base.GetValueForProperty(propertyName),
            };
        }

        // Reset all change-tracking status for all properties on the object to "unchanged".
        internal override void ResetAllChangeStatus()
        {
            GlobalDebug.WriteLineIf(GlobalDebug.Info, "Group", "ResetAllChangeStatus");

            _groupScopeChanged = (_groupScopeChanged == LoadState.Changed) ? LoadState.Loaded : LoadState.NotSet;
            _isSecurityGroupChanged = (_isSecurityGroupChanged == LoadState.Changed) ? LoadState.Loaded : LoadState.NotSet;

            _members?.ResetTracking();

            base.ResetAllChangeStatus();
        }

        /// <summary>
        /// if isSmallGroup has a value, it means we already checked if the group is small
        /// </summary>
        private bool? _isSmallGroup;

        /// <summary>
        /// cache the search result for the member attribute
        /// it will only be set for small groups!
        /// </summary>
        internal SearchResult SmallGroupMemberSearchResult { get; private set; }

        private static readonly string[] s_member = new string[] { "member" };

        /// <summary>
        ///  Finds if the group is "small", meaning that it has less than MaxValRange values (usually 1500)
        ///  The property list for the searcher of a group has "member" attribute. if there are more results than MaxValRange, there will also be a "member;range=..." attribute
        ///  we can cache the result and don't fear from changes through Add/Remove/Save because the completed/pending lists are looked up before the actual values are
        /// </summary>
        internal bool IsSmallGroup()
        {
            if (_isSmallGroup.HasValue)
            {
                return _isSmallGroup.Value;
            }

            _isSmallGroup = false;

            DirectoryEntry de = (DirectoryEntry)this.UnderlyingObject;
            Debug.Assert(de != null);
            if (de != null)
            {
                using (DirectorySearcher ds = new DirectorySearcher(de, "(objectClass=*)", s_member, SearchScope.Base))
                {
                    SearchResult sr = ds.FindOne();
                    if (sr != null)
                    {
                        bool rangePropertyFound = false;
                        foreach (string propName in sr.Properties.PropertyNames)
                        {
                            if (propName.StartsWith("member;range=", StringComparison.OrdinalIgnoreCase))
                            {
                                rangePropertyFound = true;
                                break;
                            }
                        }

                        // we only consider the group "small" if there is a "member" property but no "member;range..." property
                        if (!rangePropertyFound)
                        {
                            _isSmallGroup = true;
                            SmallGroupMemberSearchResult = sr;
                        }
                    }
                }
            }
            return _isSmallGroup.Value;
        }
    }
}