File: System\DirectoryServices\AccountManagement\StoreCtx.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;
using System.Globalization;
using System.Security.Cryptography.X509Certificates;

namespace System.DirectoryServices.AccountManagement
{
    internal enum PrincipalAccessMask
    {
        ChangePassword
    }
    internal abstract class StoreCtx : IDisposable
    {
        //
        // StoreCtx information
        //

        // Retrieves the Path (ADsPath) of the object used as the base of the StoreCtx
        internal abstract string BasePath { get; }

        // The PrincipalContext object to which this StoreCtx belongs.  Initialized by PrincipalContext after it creates
        // this StoreCtx instance.
        private PrincipalContext _owningContext;
        internal PrincipalContext OwningContext
        {
            get
            {
                return _owningContext;
            }

            set
            {
                Debug.Assert(value != null);
                _owningContext = value;
            }
        }

        //
        // CRUD
        //

        // Used to perform the specified operation on the Principal.  They also make any needed security subsystem
        // calls to obtain digitial signatures (e..g, to sign the Principal Extension/GroupMember Relationship for
        // WinFS).
        //
        // Insert() and Update() must check to make sure no properties not supported by this StoreCtx
        // have been set, prior to persisting the Principal.
        internal abstract void Insert(Principal p);
        internal abstract void Update(Principal p);
        internal abstract void Delete(Principal p);
        internal abstract void Move(StoreCtx originalStore, Principal p);

        //
        // Native <--> Principal
        //

        // For modified object, pushes any changes (including IdentityClaim changes)
        // into the underlying store-specific object (e.g., DirectoryEntry) and returns the underlying object.
        // For unpersisted object, creates a  underlying object if one doesn't already exist (in
        // Principal.UnderlyingObject), then pushes any changes into the underlying object.
        internal abstract object PushChangesToNative(Principal p);

        // Given a underlying store object (e.g., DirectoryEntry), further narrowed down a discriminant
        // (if applicable for the StoreCtx type), returns a fresh instance of a Principal
        // object based on it.  The WinFX Principal API follows ADSI-style semantics, where you get multiple
        // in-memory objects all referring to the same store pricipal, rather than WinFS semantics, where
        // multiple searches all return references to the same in-memory object.
        // Used to implement the reverse wormhole.  Also, used internally by FindResultEnumerator
        // to construct Principals from the store objects returned by a store query.
        //
        // The Principal object produced by this method does not have all the properties
        // loaded.  The Principal object will call the Load method on demand to load its properties
        // from its Principal.UnderlyingObject.
        //
        //
        // This method works for native objects from the store corresponding to _this_ StoreCtx.
        // Each StoreCtx will also have its own internal algorithms used for dealing with cross-store objects, e.g.,
        // for use when iterating over group membership.  These routines are exposed as
        // ResolveCrossStoreRefToPrincipal, and will be called by the StoreCtx's associated ResultSet
        // classes when iterating over a representation of a "foreign" principal.
        internal abstract Principal GetAsPrincipal(object storeObject, object discriminant);

        // Loads the store values from p.UnderlyingObject into p, performing schema mapping as needed.
        internal abstract void Load(Principal p);
        // Loads only the psecified property into the principal object.  The object should have already been persisted or searched for this to happen.
        internal abstract void Load(Principal p, string principalPropertyName);

        // Performs store-specific resolution of an IdentityReference to a Principal
        // corresponding to the IdentityReference.  Returns null if no matching object found.
        // principalType can be used to scope the search to principals of a specified type, e.g., users or groups.
        // Specify typeof(Principal) to search all principal types.
        internal abstract Principal FindPrincipalByIdentRef(
                                    Type principalType, string urnScheme, string urnValue, DateTime referenceDate);

        // Returns a type indicating the type of object that would be returned as the wormhole for the specified
        // Principal.  For some StoreCtxs, this method may always return a constant (e.g., typeof(DirectoryEntry)
        // for ADStoreCtx).  For others, it may vary depending on the Principal passed in.
        internal abstract Type NativeType(Principal p);

        //
        // Special operations: the Principal classes delegate their implementation of many of the
        // special methods to their underlying StoreCtx
        //

        // methods for manipulating accounts
        internal abstract void InitializeUserAccountControl(AuthenticablePrincipal p);
        internal abstract bool IsLockedOut(AuthenticablePrincipal p);
        internal abstract void UnlockAccount(AuthenticablePrincipal p);

        // methods for manipulating passwords
        internal abstract void SetPassword(AuthenticablePrincipal p, string newPassword);
        internal abstract void ChangePassword(AuthenticablePrincipal p, string oldPassword, string newPassword);
        internal abstract void ExpirePassword(AuthenticablePrincipal p);
        internal abstract void UnexpirePassword(AuthenticablePrincipal p);

        internal abstract bool AccessCheck(Principal p, PrincipalAccessMask targetPermission);

        // the various FindBy* methods
        internal abstract ResultSet FindByLockoutTime(
            DateTime dt, MatchType matchType, Type principalType);
        internal abstract ResultSet FindByLogonTime(
            DateTime dt, MatchType matchType, Type principalType);
        internal abstract ResultSet FindByPasswordSetTime(
            DateTime dt, MatchType matchType, Type principalType);
        internal abstract ResultSet FindByBadPasswordAttempt(
            DateTime dt, MatchType matchType, Type principalType);
        internal abstract ResultSet FindByExpirationTime(
            DateTime dt, MatchType matchType, Type principalType);

        // Get groups of which p is a direct member
        internal abstract ResultSet GetGroupsMemberOf(Principal p);

        // Get groups from this ctx which contain a principal corresponding to foreignPrincipal
        // (which is a principal from foreignContext)
        internal abstract ResultSet GetGroupsMemberOf(Principal foreignPrincipal, StoreCtx foreignContext);

        // Get groups of which p is a member, using AuthZ S4U APIs for recursive membership
        internal abstract ResultSet GetGroupsMemberOfAZ(Principal p);

        // Get members of group g
        internal abstract BookmarkableResultSet GetGroupMembership(GroupPrincipal g, bool recursive);

        // Is p a member of g in the store?
        internal abstract bool SupportsNativeMembershipTest { get; }
        internal abstract bool IsMemberOfInStore(GroupPrincipal g, Principal p);

        // Can a Clear() operation be performed on the specified group?  If not, also returns
        // a string containing a human-readable explanation of why not, suitable for use in an exception.
        internal abstract bool CanGroupBeCleared(GroupPrincipal g, out string explanationForFailure);

        // Can the given member be removed from the specified group?  If not, also returns
        // a string containing a human-readable explanation of why not, suitable for use in an exception.
        internal abstract bool CanGroupMemberBeRemoved(GroupPrincipal g, Principal member, out string explanationForFailure);

        //
        // Query operations
        //

        // Returns true if this store has native support for search (and thus a wormhole).
        // Returns true for everything but SAM (both reg-SAM and MSAM).
        internal abstract bool SupportsSearchNatively { get; }

        // Returns a type indicating the type of object that would be returned as the wormhole for the specified
        // PrincipalSearcher.
        internal abstract Type SearcherNativeType();

        // Pushes the query represented by the QBE filter into the PrincipalSearcher's underlying native
        // searcher object (creating a fresh native searcher and assigning it to the PrincipalSearcher if one
        // doesn't already exist) and returns the native searcher.
        // If the PrincipalSearcher does not have a query filter set (PrincipalSearcher.QueryFilter == null),
        // produces a query that will match all principals in the store.
        //
        // For stores which don't have a native searcher (SAM), the StoreCtx
        // is free to create any type of object it chooses to use as its internal representation of the query.
        //
        // Also adds in any clauses to the searcher to ensure that only principals, not mere
        // contacts, are retrieved from the store.
        internal abstract object PushFilterToNativeSearcher(PrincipalSearcher ps);

        // The core query operation.
        // Given a PrincipalSearcher containg a query filter, transforms it into the store schema
        // and performs the query to get a collection of matching native objects (up to a maximum of sizeLimit,
        // or uses the sizelimit already set on the DirectorySearcher if sizeLimit == -1).
        // If the PrincipalSearcher does not have a query filter (PrincipalSearcher.QueryFilter == null),
        // matches all principals in the store.
        //
        // The collection may not be complete, i.e., paging - the returned ResultSet will automatically
        // page in additional results as needed.
        internal abstract ResultSet Query(PrincipalSearcher ps, int sizeLimit);

        //
        // Cross-store support
        //

        // Given a native store object that represents a "foreign" principal (e.g., a FPO object in this store that
        // represents a pointer to another store), maps that representation to the other store's StoreCtx and returns
        // a Principal from that other StoreCtx.  The implementation of this method is highly dependent on the
        // details of the particular store, and must have knowledge not only of this StoreCtx, but also of how to
        // interact with other StoreCtxs to fulfill the request.
        //
        // This method is typically used by ResultSet implementations, when they're iterating over a collection
        // (e.g., of group membership) and encounter an entry that represents a foreign principal.
        internal abstract Principal ResolveCrossStoreRefToPrincipal(object o);

        //
        // Data Validation
        //

        // Validiate the passed property name to determine if it is valid for the store and Principal type.
        // used by the principal objects to determine if a property is valid in the property before
        // save is called.
        internal abstract bool IsValidProperty(Principal p, string propertyName);

        // Returns true if AccountInfo is supported for the specified principal, false otherwise.
        // Used when an application tries to access the AccountInfo property of a newly-inserted
        // (not yet persisted) AuthenticablePrincipal, to determine whether it should be allowed.
        internal abstract bool SupportsAccounts(AuthenticablePrincipal p);

        // Returns the set of credential types supported by this store for the specified principal.
        // Used when an application tries to access the PasswordInfo property of a newly-inserted
        // (not yet persisted) AuthenticablePrincipal, to determine whether it should be allowed.
        // Also used to implement AuthenticablePrincipal.SupportedCredentialTypes.
        internal abstract CredentialTypes SupportedCredTypes(AuthenticablePrincipal p);

        //
        // Construct a fake Principal to represent a well-known SID like
        // "\Everyone" or "NT AUTHORITY\NETWORK SERVICE"
        //
        internal abstract Principal ConstructFakePrincipalFromSID(byte[] sid);

        //
        // IDisposable implementation
        //

        // Disposes of this instance of a StoreCtx.  Calling this method more than once is allowed, and all but
        // the first call should be ignored.
        public virtual void Dispose()
        {
            // Nothing to do in the base class
        }

        //
        // QBE Filter parsing
        //

        // These property sets include only the properties used to build QBE filters,
        // e.g., the Group.Members property is not included

        internal static string[] principalProperties = new string[]
        {
            PropertyNames.PrincipalDisplayName,
            PropertyNames.PrincipalDescription,
            PropertyNames.PrincipalSamAccountName,
            PropertyNames.PrincipalUserPrincipalName,
            PropertyNames.PrincipalGuid,
            PropertyNames.PrincipalSid,
            PropertyNames.PrincipalStructuralObjectClass,
            PropertyNames.PrincipalName,
            PropertyNames.PrincipalDistinguishedName,
            PropertyNames.PrincipalExtensionCache
        };

        internal static string[] authenticablePrincipalProperties = new string[]
        {
            PropertyNames.AuthenticablePrincipalEnabled,
            PropertyNames.AuthenticablePrincipalCertificates,
            PropertyNames.PwdInfoLastBadPasswordAttempt,
            PropertyNames.AcctInfoExpirationDate,
            PropertyNames.AcctInfoExpiredAccount,
            PropertyNames.AcctInfoLastLogon,
            PropertyNames.AcctInfoAcctLockoutTime,
            PropertyNames.AcctInfoBadLogonCount,
            PropertyNames.PwdInfoLastPasswordSet
        };

        // includes AccountInfo and PasswordInfo
        internal static string[] userProperties = new string[]
        {
            PropertyNames.UserGivenName,
            PropertyNames.UserMiddleName,
            PropertyNames.UserSurname,
            PropertyNames.UserEmailAddress,
            PropertyNames.UserVoiceTelephoneNumber,
            PropertyNames.UserEmployeeID,

            PropertyNames.AcctInfoPermittedWorkstations,
            PropertyNames.AcctInfoPermittedLogonTimes,
            PropertyNames.AcctInfoSmartcardRequired,
            PropertyNames.AcctInfoDelegationPermitted,
            PropertyNames.AcctInfoHomeDirectory,
            PropertyNames.AcctInfoHomeDrive,
            PropertyNames.AcctInfoScriptPath,

            PropertyNames.PwdInfoPasswordNotRequired,
            PropertyNames.PwdInfoPasswordNeverExpires,
            PropertyNames.PwdInfoCannotChangePassword,
            PropertyNames.PwdInfoAllowReversiblePasswordEncryption
        };

        internal static string[] groupProperties = new string[]
        {
            PropertyNames.GroupIsSecurityGroup,
            PropertyNames.GroupGroupScope
        };

        internal static string[] computerProperties = new string[]
        {
            PropertyNames.ComputerServicePrincipalNames
        };

        protected QbeFilterDescription BuildQbeFilterDescription(Principal p)
        {
            QbeFilterDescription qbeFilterDescription = new QbeFilterDescription();

            // We don't have to check to make sure the application didn't try to set any
            // disallowed properties (i..e, referential properties, such as Group.Members),
            // because that check was enforced by the PrincipalSearcher in its
            // FindAll() and GetUnderlyingSearcher() methods, by calling
            // PrincipalSearcher.HasReferentialPropertiesSet().

            if (p is Principal)
                BuildFilterSet(p, principalProperties, qbeFilterDescription);

            if (p is AuthenticablePrincipal)
                BuildFilterSet(p, authenticablePrincipalProperties, qbeFilterDescription);

            if (p is UserPrincipal)  // includes AccountInfo and PasswordInfo
            {
                // AcctInfoExpirationDate and AcctInfoExpiredAccount represent filters on the same property
                // check that only one is specified
                if (p.GetChangeStatusForProperty(PropertyNames.AcctInfoExpirationDate) &&
                        p.GetChangeStatusForProperty(PropertyNames.AcctInfoExpiredAccount))
                {
                    throw new InvalidOperationException(
                                       SR.Format(
                                           SR.StoreCtxMultipleFiltersForPropertyUnsupported,
                                           PropertyNamesExternal.GetExternalForm(ExpirationDateFilter.PropertyNameStatic)));
                }

                BuildFilterSet(p, userProperties, qbeFilterDescription);
            }

            if (p is GroupPrincipal)
                BuildFilterSet(p, groupProperties, qbeFilterDescription);

            if (p is ComputerPrincipal)
                BuildFilterSet(p, computerProperties, qbeFilterDescription);

            return qbeFilterDescription;
        }

        // Applies to supplied propertySet to the supplied Principal, and adds any resulting filters
        // to qbeFilterDescription.
        private void BuildFilterSet(Principal p, string[] propertySet, QbeFilterDescription qbeFilterDescription)
        {
            foreach (string propertyName in propertySet)
            {
                if (p.GetChangeStatusForProperty(propertyName))
                {
                    // Property has changed.  Add it to the filter set.
                    object value = p.GetValueForProperty(propertyName);

                    GlobalDebug.WriteLineIf(
                            GlobalDebug.Info,
                            "StoreCtx",
                            "BuildFilterSet: type={0}, property name={1}, property value={2} of type {3}",
                            p.GetType().ToString(),
                            propertyName,
                            value.ToString(),
                            value.GetType().ToString());

                    // Build the right filter based on type of the property value
                    if (value is PrincipalValueCollection<string> trackingList)
                    {
                        foreach (string s in trackingList.Inserted)
                        {
                            object filter = FilterFactory.CreateFilter(propertyName);
                            ((FilterBase)filter).Value = (string)s;
                            qbeFilterDescription.FiltersToApply.Add(filter);
                        }
                    }
                    else if (value is X509Certificate2Collection certCollection)
                    {
                        // Since QBE filter objects are always unpersisted, any certs in the collection
                        // must have been inserted by the application.
                        foreach (X509Certificate2 cert in certCollection)
                        {
                            object filter = FilterFactory.CreateFilter(propertyName);
                            ((FilterBase)filter).Value = (X509Certificate2)cert;
                            qbeFilterDescription.FiltersToApply.Add(filter);
                        }
                    }
                    else
                    {
                        // It's not one of the multivalued cases.  Try the scalar cases.

                        object filter = FilterFactory.CreateFilter(propertyName);

                        if (value == null)
                        {
                            ((FilterBase)filter).Value = null;
                        }
                        else if (value is bool)
                        {
                            ((FilterBase)filter).Value = (bool)value;
                        }
                        else if (value is string)
                        {
                            ((FilterBase)filter).Value = (string)value;
                        }
                        else if (value is GroupScope)
                        {
                            ((FilterBase)filter).Value = (GroupScope)value;
                        }
                        else if (value is byte[])
                        {
                            ((FilterBase)filter).Value = (byte[])value;
                        }
                        else if (value is Nullable<DateTime>)
                        {
                            ((FilterBase)filter).Value = (Nullable<DateTime>)value;
                        }
                        else if (value is ExtensionCache)
                        {
                            ((FilterBase)filter).Value = (ExtensionCache)value;
                        }
                        else if (value is QbeMatchType)
                        {
                            ((FilterBase)filter).Value = (QbeMatchType)value;
                        }
                        else
                        {
                            // Internal error.  Didn't match either the known multivalued or scalar cases.
                            Debug.Fail($"StoreCtx.BuildFilterSet: fell off end looking for {propertyName} of type {value.GetType()}");
                        }

                        qbeFilterDescription.FiltersToApply.Add(filter);
                    }
                }
            }
        }
    }
}