File: System\DirectoryServices\AccountManagement\AD\ADStoreCtx_LoadStore.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;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.DirectoryServices;
using System.Globalization;
using System.Net;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Cryptography.X509Certificates;
using System.Security.Principal;
using System.Text;
using MACLPrinc = System.Security.Principal;

namespace System.DirectoryServices.AccountManagement
{
    internal partial class ADStoreCtx : StoreCtx
    {
        //
        // 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 override object PushChangesToNative(Principal p)
        {
            GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "Entering PushChangesToNative, type={0}", p.GetType());

            try
            {
                DirectoryEntry de = (DirectoryEntry)p.UnderlyingObject;

                if (de == null)
                {
                    // Must be a newly-inserted Principal for which PushChangesToNative has not yet
                    // been called.

                    // Determine the objectClass of the AD entry we'll be creating
                    string objectClass;
                    Type principalType = p.GetType();
                    // read the rdnPrefix off of the class attribute.  This defaults to CN for the internal classes
                    string rdnPrefix = p.ExtensionHelper.RdnPrefix;
                    string rdnValue = null;
                    string baseObjectRdnPrefix = null;

                    if (principalType == typeof(UserPrincipal))
                        objectClass = "user";
                    else if (principalType == typeof(GroupPrincipal))
                        objectClass = "group";
                    else if (principalType == typeof(ComputerPrincipal))
                        objectClass = "computer";
                    else
                    {
                        objectClass = p.ExtensionHelper.StructuralObjectClass;

                        if (null == objectClass || null == rdnPrefix)
                        {
                            throw new InvalidOperationException(SR.ExtensionInvalidClassAttributes);
                        }

                        // We need to determine if this class is dervived from one of the base classes but has a different RdnPrefix
                        // For the base objects ( User, Computer and Group ) Their RDNPrefix is a required field along with the RDNPrefix for the
                        // derived object.  This is only done for classes that derive from User, Computer or Group.  If a user derives their own class from AuthPrincipal
                        // they are responsible for setting all required base class properties.

                        if (principalType.IsSubclassOf(typeof(GroupPrincipal)) ||
                             principalType.IsSubclassOf(typeof(UserPrincipal)) ||
                             principalType.IsSubclassOf(typeof(ComputerPrincipal)))
                        {
                            DirectoryRdnPrefixAttribute[] MyAttribute =
                            (DirectoryRdnPrefixAttribute[])Attribute.GetCustomAttributes(principalType.BaseType, typeof(DirectoryRdnPrefixAttribute), false);

                            if (MyAttribute == null)
                                throw new InvalidOperationException(SR.ExtensionInvalidClassAttributes);

                            string defaultRdn = null;

                            // Search for the rdn prefix.  This will use either the prefix that has a context type
                            // that matches the principals context or the first rdnPrefix that has a null context type
                            for (int i = 0; i < MyAttribute.Length; i++)
                            {
                                if ((MyAttribute[i].Context == null && null == defaultRdn) ||
                                    (p.ContextType == MyAttribute[i].Context))
                                {
                                    defaultRdn = MyAttribute[i].RdnPrefix;
                                }
                            }

                            // If the base objects RDN prefix is not the same as the derived class then we need to set both
                            if (defaultRdn != rdnPrefix)
                            {
                                baseObjectRdnPrefix = defaultRdn;
                            }
                        }
                    }

                    // Determine a RDN for the AD entry we'll be creating.
                    // In order, try:
                    //  (1) The NAme is they set one.
                    //  (2) The SAM account name if they set one
                    string rdn = null;

                    if (p.GetChangeStatusForProperty(PropertyNames.PrincipalName))
                    {
                        // They set a display name.
                        string name = (string)p.GetValueForProperty(PropertyNames.PrincipalName);

                        if ((name != null) && (name.Length > 0))
                            rdnValue = ADUtils.EscapeDNComponent(name);
                    }

                    if (rdnValue == null)
                    {
                        if (p.GetChangeStatusForProperty(PropertyNames.PrincipalSamAccountName))
                        {
                            // They set a sAMAccountName.  If it's an invalid claim, we'll just ignore it here.
                            // The error will get picked up in IdentClaimToLdapConverter.
                            string samAccountName = (string)p.GetValueForProperty(PropertyNames.PrincipalSamAccountName);

                            int index = samAccountName.IndexOf('\\');

                            if (index != samAccountName.Length - 1)
                            {
                                samAccountName = (index != -1) ? samAccountName.Substring(index + 1) :    // +1 to skip the '/'
                                                                     samAccountName;
                            }

                            if ((samAccountName != null) && (samAccountName.Length > 0))
                            {
                                rdnValue = ADUtils.EscapeDNComponent(samAccountName);
                            }
                        }
                    }

                    if (rdnPrefix == null)
                    {
                        // There was no rdn prefix attribute set on the principal class
                        throw new InvalidOperationException(SR.ExtensionInvalidClassAttributes);
                    }

                    if (rdnValue == null)
                    {
                        // They didn't set a display name or SAM IdentityClaim (or set an empty/invalid value for those).
                        throw new InvalidOperationException(SR.NameMustBeSetToPersistPrincipal);
                    }

                    rdn = rdnPrefix + "=" + rdnValue;

                    lock (_ctxBaseLock)
                    {
                        de = this.ctxBase.Children.Add(rdn, objectClass);
                    }

                    if (null != baseObjectRdnPrefix)
                    {
                        de.Properties[baseObjectRdnPrefix].Value = rdnValue;
                    }

                    InitializeNewDirectoryOptions(de);

                    p.UnderlyingObject = de;

                    // set the default user account control bits for authenticable principals
                    if (principalType.IsSubclassOf(typeof(AuthenticablePrincipal)))
                    {
                        InitializeUserAccountControl((AuthenticablePrincipal)p);
                    }

                    GlobalDebug.WriteLineIf(
                        GlobalDebug.Info, "ADStoreCtx", "PushChangesToNative: created fresh DE, oc={0}, path={1}",
                        objectClass, de.Path);
                }

                // propertyMappingTableByProperty only has entries for writable properties,
                // including writable properties which we don't support in AD.
                // If we don't support the property, the PropertyMappingTableEntry will map
                // it to a converter which will throw an appropriate exception.
                Hashtable mappingTableByProp = (Hashtable)s_propertyMappingTableByProperty[this.MappingTableIndex];

                foreach (DictionaryEntry dictEntry in mappingTableByProp)
                {
                    ArrayList propertyEntries = (ArrayList)dictEntry.Value;

                    foreach (PropertyMappingTableEntry propertyEntry in propertyEntries)
                    {
                        if (p.GetChangeStatusForProperty(propertyEntry.propertyName))
                        {
                            // The property was set.  Write it to the DirectoryEntry.
                            Debug.Assert(propertyEntry.propertyName == (string)dictEntry.Key);

                            GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "PushChangesToNative: pushing {0}", propertyEntry.propertyName);

                            propertyEntry.papiToLdapConverter(p, propertyEntry.propertyName, de, propertyEntry.suggestedADPropertyName);
                        }
                    }
                }

                return de;
            }
            catch (System.Runtime.InteropServices.COMException e)
            {
                throw ExceptionHelper.GetExceptionFromCOMException(e);
            }
        }

        // 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.

        // This method will either be passed a DirectoryEntry or SearchResult object if this is the result of a search.
        // We need to determine the type and then use the appropriate object.
        internal override Principal GetAsPrincipal(object storeObject, object discriminant)
        {
            Debug.Assert(storeObject != null);

            DirectoryEntry de = null;
            SearchResult sr = null;
            string path;
            string distinguishedName;

            if (storeObject is DirectoryEntry)
            {
                de = (DirectoryEntry)storeObject;
                path = de.Path;
                distinguishedName = (string)de.Properties["distinguishedName"].Value;
            }
            else
            {
                Debug.Assert(storeObject is SearchResult);
                sr = (SearchResult)storeObject;
                path = sr.Path;
                distinguishedName = (string)sr.Properties["distinguishedName"][0];
            }

            GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "GetAsPrincipal: using path={0}", (null != de ? de.Path : "searchResult"));

            // Construct an appropriate Principal object.

            bool targetIsFromGC = SDSUtils.IsObjectFromGC(path);
            try
            {
                Principal p;

                DirectoryEntry dcEntry = null;
                PrincipalContext constructedContext = null;

                // if the object was obtained from a GC, we have to construct a new context, and build a new DirectoryEntry
                // if the object is not from a GC but belongs to another domain, we just have to construct a new context. We can still use the storeObject or searchresult's DirectoryEntry.
                // if our context is not a domain (that is, it's ADLDS) we don't build a new context unless the object was obtained from a GC.
                if (targetIsFromGC || OwningContext.ContextType == ContextType.Domain)
                {
                    string dnsDomainName = SDSUtils.ConstructDnsDomainNameFromDn(distinguishedName);
                    if (targetIsFromGC ||
                        (!string.IsNullOrEmpty(this.domainDnsName) && !string.Equals(this.DnsDomainName, dnsDomainName, StringComparison.OrdinalIgnoreCase)))
                    {
                        constructedContext = SDSCache.Domain.GetContext(dnsDomainName, this.Credentials, this.OwningContext.Options);
                    }

                    if (targetIsFromGC)
                    {
                        dcEntry = SDSUtils.BuildDirectoryEntry("LDAP://" + dnsDomainName + "/" + GetEscapedDN(distinguishedName), this.Credentials, this.authTypes);
                        this.InitializeNewDirectoryOptions(dcEntry);
                    }
                }

                if (de != null)
                {
                    //NOTE: If target is GC then we do NOT use variable "de". So, need to dispose this at the end.
                    p = SDSUtils.DirectoryEntryToPrincipal((targetIsFromGC ? dcEntry : de), constructedContext ?? this.OwningContext, (Type)discriminant);
                }
                else
                {
                    //NOTE: If input storeObject is searchResult then variable "de" gets used in all cases.
                    p = SDSUtils.SearchResultToPrincipal(sr, constructedContext ?? this.OwningContext, (Type)discriminant);
                    p.UnderlyingObject = (targetIsFromGC ? dcEntry : sr.GetDirectoryEntry());
                }

                Debug.Assert(p != null);

                Guid g;

                if (de != null)
                    g = de.Guid;
                else
                {
                    byte[] guid = (byte[])sr.Properties["objectGuid"][0];
                    g = new Guid(guid);
                }

                ADStoreKey key = new ADStoreKey(g);
                p.Key = key;

                return p;
            }
            catch (System.Runtime.InteropServices.COMException e)
            {
                throw ExceptionHelper.GetExceptionFromCOMException(e);
            }
            finally
            {
                if (targetIsFromGC && de != null)
                {
                    //Since target is GC, variable "de" is not getting used. Disposing it.
                    de.Dispose();
                }
            }
        }

        /// <summary>
        /// This Calls the Native API to Escape the DN
        /// </summary>
        /// <param name="dn"></param>
        /// <returns>Escaped DN</returns>
        private string GetEscapedDN(string dn)
        {
            UnsafeNativeMethods.Pathname pathNameObj = new UnsafeNativeMethods.Pathname();
            UnsafeNativeMethods.IADsPathname pathCracker = (UnsafeNativeMethods.IADsPathname)pathNameObj;

            // Set the Escape mode On
            pathCracker.EscapedMode = 2 /* ADS_ESCAPEDMODE_ON */;

            // Set the type of path to DN
            pathCracker.Set(dn, 4 /* ADS_SETTYPE_DN */);

            // Get back the escaped DN
            return pathCracker.Retrieve(7 /* ADS_FORMAT_X500_DN */);
        }

        internal override void Load(Principal p, string principalPropertyName)
        {
            Debug.Assert(p != null);
            Debug.Assert(p.UnderlyingObject != null);
            Debug.Assert(p.UnderlyingObject is DirectoryEntry);

            dSPropertyCollection props;

            SearchResult sr = (SearchResult)p.UnderlyingSearchObject;
            if (null == sr)
            {
                DirectoryEntry de = (DirectoryEntry)p.UnderlyingObject;
                Debug.Assert(de != null);
                Debug.Assert(p.GetUnderlyingObjectType() == typeof(DirectoryEntry));

                props = new dSPropertyCollection(de.Properties);
            }
            else
            {
                props = new dSPropertyCollection(sr.Properties);
            }

            Hashtable propertyMappingTable = (Hashtable)s_propertyMappingTableByPropertyFull[this.MappingTableIndex];

            ArrayList entries = (ArrayList)propertyMappingTable[principalPropertyName];

            // We don't support this property and cannot load it.  To maintain backward compatibility with the old code just return.
            if (entries == null)
                return;

            Debug.Assert(null != entries);

            try
            {
                foreach (PropertyMappingTableEntry entry in entries)
                {
                    if (null != entry.ldapToPapiConverter)
                    {
                        GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "Load_PropertyName: loading {0}", entry.propertyName);
                        entry.ldapToPapiConverter(props, entry.suggestedADPropertyName, p, entry.propertyName);
                    }
                }
            }
            catch (System.Runtime.InteropServices.COMException e)
            {
                throw ExceptionHelper.GetExceptionFromCOMException(e);
            }
        }

        // Loads the store values from p.UnderlyingObject into p, performing schema mapping as needed.
        internal override void Load(Principal p)
        {
            Debug.Assert(p != null);
            Debug.Assert(p.UnderlyingObject != null);
            Debug.Assert(p.UnderlyingObject is DirectoryEntry);

            DirectoryEntry de = (DirectoryEntry)p.UnderlyingObject;
            Debug.Assert(de != null);

            GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "Entering Load, type={0}, path={1}", p.GetType(), de.Path);

            Hashtable ldapMappingTable = (Hashtable)s_propertyMappingTableByLDAP[this.MappingTableIndex];
            foreach (DictionaryEntry Dictentry in ldapMappingTable)
            {
                ArrayList entries = (ArrayList)Dictentry.Value;
                try
                {
                    foreach (PropertyMappingTableEntry entry in entries)
                    {
                        GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "Load: loading {0}", entry.propertyName);
                        entry.ldapToPapiConverter(new dSPropertyCollection(de.Properties), entry.suggestedADPropertyName, p, entry.propertyName);
                    }
                }
                catch (System.Runtime.InteropServices.COMException e)
                {
                    throw ExceptionHelper.GetExceptionFromCOMException(e);
                }
            }

            p.Loaded = true;
        }

        // 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 override Principal FindPrincipalByIdentRef(
                                    Type principalType, string urnScheme, string urnValue, DateTime referenceDate)
        {
            return FindPrincipalByIdentRefHelper(principalType, urnScheme, urnValue, referenceDate, false);
        }

        // Normally, doing a FindByIdentity for a SID doesn't include the SID History.  This method provides a
        // means to include the sidHistory as well as the objectSid in the search.
        internal Principal FindPrincipalBySID(
                                    Type principalType, IdentityReference ir, bool useSidHistory)
        {
            return FindPrincipalByIdentRefHelper(principalType,
                                                 ir.UrnScheme,
                                                 ir.UrnValue,
                                                 DateTime.UtcNow,
                                                 useSidHistory);
        }

        // Handles all the work required to implement FindPrincipalByIdentRef (and FindPrincipalBySID).
        private Principal FindPrincipalByIdentRefHelper(
                                    Type principalType,
                                    string urnScheme,
                                    string urnValue,
                                    DateTime referenceDate,
                                    bool useSidHistory)
        {
            GlobalDebug.WriteLineIf(GlobalDebug.Info,
                                    "ADStoreCtx",
                                    "FindPrincipalByIdentRefHelper: type={0}, scheme={1}, value={2}, useSidHistory={3}",
                                    principalType.ToString(),
                                    urnScheme ?? "NULL",
                                    urnValue ?? "NULL",
                                    useSidHistory);

            //
            // Set up a DirectorySearcher
            //
            DirectorySearcher ds = new DirectorySearcher(this.ctxBase);
            SearchResultCollection src = null;

            try
            {
                ds.SizeLimit = 2;   // so we can efficiently check for duplicates

                // If we are searching for AuthPrincpal or Principal in the end we will construct the actual type
                // i.e. if the objects objectClass is User we will construct a UserPrincipal even though they searched for Principal.FindByIdentity
                // At this time we don't know the actual object type so we have to ask AD for all the attributes of the derived types so they are there
                // when we go to load the principal.
                if (principalType == typeof(Principal) || principalType == typeof(AuthenticablePrincipal))
                {
                    BuildPropertySet(typeof(UserPrincipal), ds.PropertiesToLoad);
                    BuildPropertySet(typeof(GroupPrincipal), ds.PropertiesToLoad);
                    BuildPropertySet(typeof(ComputerPrincipal), ds.PropertiesToLoad);

                    if (principalType == typeof(Principal))
                    {
                        BuildPropertySet(typeof(AuthenticablePrincipal), ds.PropertiesToLoad);
                    }
                }

                BuildPropertySet(principalType, ds.PropertiesToLoad);

                //
                // Build an appropriate filter
                //

                StringBuilder ldapFilter = new StringBuilder();

                // Limit the results returned to principalType by specifying the corresponding objectClass/objectCategory
                ldapFilter.Append(GetObjectClassPortion(principalType));

                // Build the rest of the filter based off of the user's specified IdentityReference.
                if (urnScheme != null)
                {
                    // If they're searching by SID for a SID corresponding to a fake group, construct
                    // and return the fake group
                    if ((urnScheme == UrnScheme.SidScheme) &&
                         (principalType == typeof(Principal) || principalType == typeof(GroupPrincipal) || principalType.IsSubclassOf(typeof(GroupPrincipal))))
                    {
                        SecurityIdentifier sid = new SecurityIdentifier(urnValue);
                        byte[] sidb = new byte[sid.BinaryLength];
                        sid.GetBinaryForm(sidb, 0);
                        //                        = Utils.StringToByteArray(urnValue);

                        if (sid == null)
                            throw new ArgumentException(SR.StoreCtxSecurityIdentityClaimBadFormat);

                        IntPtr pSid = IntPtr.Zero;

                        try
                        {
                            pSid = Utils.ConvertByteArrayToIntPtr(sidb);

                            if (Interop.Advapi32.IsValidSid(pSid) && (Utils.ClassifySID(pSid) == SidType.FakeObject))
                            {
                                GlobalDebug.WriteLineIf(GlobalDebug.Info,
                                                        "ADStoreCtx",
                                                        "FindPrincipalByIdentRefHelper: fake principal, SID Scheme, {0}",
                                                        sid.ToString());

                                return ConstructFakePrincipalFromSID(sidb);
                            }
                        }
                        finally
                        {
                            if (pSid != IntPtr.Zero)
                                Marshal.FreeHGlobal(pSid);
                        }
                    }

                    // This is the simple case --- we got a specific UrnScheme, so we'll just build
                    // a filter for it.

                    // Ignore referenceDate --- all IdentityClaims in AD are forever
                    string innerLdapFilter = null;
                    BuildLdapFilterFromIdentityClaim(urnValue, urnScheme, ref innerLdapFilter, useSidHistory, true);

                    ldapFilter.Append(innerLdapFilter);
                }
                else
                {
                    // Are they perhaps searching for a fake group?
                    // If they passed in a valid SID for a fake group, construct and return the fake
                    // group.
                    if (principalType == typeof(Principal) || principalType == typeof(GroupPrincipal) || principalType.IsSubclassOf(typeof(GroupPrincipal)))
                    {
                        SecurityIdentifier sid = null;
                        byte[] sidb = null;
                        try
                        {
                            sid = new SecurityIdentifier(urnValue);
                            sidb = new byte[sid.BinaryLength];
                            sid.GetBinaryForm(sidb, 0);
                        }
                        catch (ArgumentException)
                        {
                        }

                        if (sidb != null)
                        {
                            // They passed in a hex string, is it a valid SID, and if so, does it correspond to a fake
                            // principal?
                            IntPtr pSid = IntPtr.Zero;

                            try
                            {
                                pSid = Utils.ConvertByteArrayToIntPtr(sidb);

                                if (Interop.Advapi32.IsValidSid(pSid) && (Utils.ClassifySID(pSid) == SidType.FakeObject))
                                {
                                    GlobalDebug.WriteLineIf(GlobalDebug.Info,
                                                            "ADStoreCtx",
                                                            "FindPrincipalByIdentRefHelper: fake principal, null Scheme, {0}",
                                                            sid.ToString());

                                    return ConstructFakePrincipalFromSID(sidb);
                                }
                            }
                            finally
                            {
                                if (pSid != IntPtr.Zero)
                                    Marshal.FreeHGlobal(pSid);
                            }
                        }
                    }

                    // This is the tricky case.  They didn't specify a UrnScheme, so we need to
                    // try all of them.

                    ReadOnlySpan<string> urnSchemesToTry =
                    [
                        UrnScheme.SamAccountScheme,
                        UrnScheme.UpnScheme,
                        UrnScheme.DistinguishedNameScheme,
                        UrnScheme.SidScheme,
                        UrnScheme.GuidScheme,
                        UrnScheme.NameScheme
                    ];

                    StringBuilder innerLdapFilter = new StringBuilder();

                    innerLdapFilter.Append("(|");

                    string filterVal = null;

                    foreach (string urnSchemeToTry in urnSchemesToTry)
                    {
                        if (BuildLdapFilterFromIdentityClaim(urnValue, urnSchemeToTry, ref filterVal, useSidHistory, false))
                            if (null != filterVal)
                                innerLdapFilter.Append(filterVal);
                    }

                    innerLdapFilter.Append(')');

                    ldapFilter.Append(innerLdapFilter);
                }

                // Wrap off the filter
                ldapFilter.Append(')');

                ds.Filter = ldapFilter.ToString();
                GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "FindPrincipalByIdentRefHelper: using LDAP filter {0}", ds.Filter);

                //
                // Perform the actual search
                //
                src = ds.FindAll();

                Debug.Assert(src != null);

                if (src == null)
                    return null;

                // Did we find a match?
                int count = src.Count;

                GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "FindPrincipalByIdentRefHelper: found {0} matches", count);

                // Did we find more than one match?
                if (count > 1)
                    throw new MultipleMatchesException(SR.MultipleMatchingPrincipals);

                if (count == 0)
                    return null;

                return GetAsPrincipal(src[0], principalType);
            }
            catch (System.Runtime.InteropServices.COMException e)
            {
                throw ExceptionHelper.GetExceptionFromCOMException(e);
            }
            finally
            {
                ds.Dispose();
                src?.Dispose();
            }
        }

        // 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 override Type NativeType(Principal p)
        {
            Debug.Assert(p != null);

            return typeof(DirectoryEntry);
        }

        //
        // Property mapping tables
        //

        // We only list properties we map in this table.  At run-time, if we detect they set a
        // property that's not listed here when writing to AD, we throw an exception.
        //
        // When updating this table, be sure to also update LoadDirectoryEntryAttributes() to load
        // in any newly-added attributes.

        private static readonly object[,] s_propertyMappingTableRaw =
        {
            // PropertyName                          AD property             Converter(LDAP->PAPI)                                    Converter(PAPI->LDAP)
            {PropertyNames.PrincipalDisplayName,     "displayname",          new FromLdapConverterDelegate(StringFromLdapConverter),  new ToLdapConverterDelegate(StringToLdapConverter)},
            {PropertyNames.PrincipalDescription,     "description",          new FromLdapConverterDelegate(StringFromLdapConverter),  new ToLdapConverterDelegate(StringToLdapConverter)},
            {PropertyNames.PrincipalDistinguishedName,  "distinguishedname",    new FromLdapConverterDelegate(StringFromLdapConverter), new ToLdapConverterDelegate(StringToLdapConverter)},
            {PropertyNames.PrincipalSid,  "objectsid",            new FromLdapConverterDelegate(SidFromLdapConverter),  null},
            {PropertyNames.PrincipalSamAccountName,  "samaccountname",       new FromLdapConverterDelegate(StringFromLdapConverter), new ToLdapConverterDelegate(StringToLdapConverter)},
            {PropertyNames.PrincipalUserPrincipalName,  "userprincipalname",    new FromLdapConverterDelegate(StringFromLdapConverter),  new ToLdapConverterDelegate(StringToLdapConverter)},
            {PropertyNames.PrincipalGuid,  "objectguid",           new FromLdapConverterDelegate(GuidFromLdapConverter),   null},
            {PropertyNames.PrincipalStructuralObjectClass,  "objectclass",           new FromLdapConverterDelegate(ObjectClassFromLdapConverter),   null},
            {PropertyNames.PrincipalName,  "name",           new FromLdapConverterDelegate(StringFromLdapConverter), new ToLdapConverterDelegate(StringToLdapConverter)},
            {PropertyNames.PrincipalExtensionCache,  null,  null, new ToLdapConverterDelegate(ExtensionCacheToLdapConverter)},

            {PropertyNames.AuthenticablePrincipalEnabled,      "useraccountcontrol", new FromLdapConverterDelegate(UACFromLdapConverter),  new ToLdapConverterDelegate(UACToLdapConverter)},
            {PropertyNames.AuthenticablePrincipalCertificates, "usercertificate",    new FromLdapConverterDelegate(CertFromLdapConverter), new ToLdapConverterDelegate(CertToLdap)},

            {PropertyNames.GroupIsSecurityGroup,   "grouptype", new FromLdapConverterDelegate(GroupTypeFromLdapConverter), new ToLdapConverterDelegate(GroupTypeToLdapConverter)},
            {PropertyNames.GroupGroupScope, "grouptype", new FromLdapConverterDelegate(GroupTypeFromLdapConverter), new ToLdapConverterDelegate(GroupTypeToLdapConverter)},

            {PropertyNames.UserGivenName,             "givenname",        new FromLdapConverterDelegate(StringFromLdapConverter),  new ToLdapConverterDelegate(StringToLdapConverter)},
            {PropertyNames.UserMiddleName,            "middlename",       new FromLdapConverterDelegate(StringFromLdapConverter),  new ToLdapConverterDelegate(StringToLdapConverter)},
            {PropertyNames.UserSurname,               "sn",               new FromLdapConverterDelegate(StringFromLdapConverter),  new ToLdapConverterDelegate(StringToLdapConverter)},
            {PropertyNames.UserEmailAddress,          "mail",             new FromLdapConverterDelegate(StringFromLdapConverter),  new ToLdapConverterDelegate(StringToLdapConverter)},
            {PropertyNames.UserVoiceTelephoneNumber,  "telephonenumber",  new FromLdapConverterDelegate(StringFromLdapConverter),  new ToLdapConverterDelegate(StringToLdapConverter)},
            {PropertyNames.UserEmployeeID,            "employeeid",       new FromLdapConverterDelegate(StringFromLdapConverter),  new ToLdapConverterDelegate(StringToLdapConverter)},

            {PropertyNames.ComputerServicePrincipalNames, "serviceprincipalname", new FromLdapConverterDelegate(MultiStringFromLdapConverter), new ToLdapConverterDelegate(MultiStringToLdapConverter)},

            {PropertyNames.AcctInfoAcctLockoutTime,       "lockouttime",        new FromLdapConverterDelegate(GenericDateTimeFromLdapConverter), null},
            {PropertyNames.AcctInfoLastLogon,             "lastlogon",          new FromLdapConverterDelegate(LastLogonFromLdapConverter),       null},
            {PropertyNames.AcctInfoLastLogon,             "lastlogontimestamp", new FromLdapConverterDelegate(LastLogonFromLdapConverter),       null},
            {PropertyNames.AcctInfoPermittedWorkstations, "userworkstations",   new FromLdapConverterDelegate(CommaStringFromLdapConverter),     new ToLdapConverterDelegate(CommaStringToLdapConverter)},
            {PropertyNames.AcctInfoPermittedLogonTimes,   "logonhours",         new FromLdapConverterDelegate(BinaryFromLdapConverter),          new ToLdapConverterDelegate(BinaryToLdapConverter)},
            {PropertyNames.AcctInfoExpirationDate,        "accountexpires",     new FromLdapConverterDelegate(AcctExpirFromLdapConverter),       new ToLdapConverterDelegate(AcctExpirToLdapConverter)},
            {PropertyNames.AcctInfoSmartcardRequired,     "useraccountcontrol", new FromLdapConverterDelegate(UACFromLdapConverter),             new ToLdapConverterDelegate(UACToLdapConverter)},
            {PropertyNames.AcctInfoDelegationPermitted,   "useraccountcontrol", new FromLdapConverterDelegate(UACFromLdapConverter),             new ToLdapConverterDelegate(UACToLdapConverter)},
            {PropertyNames.AcctInfoBadLogonCount,         "badpwdcount",        new FromLdapConverterDelegate(IntFromLdapConverter),             null},
            {PropertyNames.AcctInfoHomeDirectory,         "homedirectory",      new FromLdapConverterDelegate(StringFromLdapConverter),          new ToLdapConverterDelegate(StringToLdapConverter)},
            {PropertyNames.AcctInfoHomeDrive,             "homedrive",          new FromLdapConverterDelegate(StringFromLdapConverter),          new ToLdapConverterDelegate(StringToLdapConverter)},
            {PropertyNames.AcctInfoScriptPath,            "scriptpath",         new FromLdapConverterDelegate(StringFromLdapConverter),          new ToLdapConverterDelegate(StringToLdapConverter)},

            {PropertyNames.PwdInfoLastPasswordSet,        "pwdlastset",           new FromLdapConverterDelegate(GenericDateTimeFromLdapConverter),     null},
            {PropertyNames.PwdInfoLastBadPasswordAttempt, "badpasswordtime",      new FromLdapConverterDelegate(GenericDateTimeFromLdapConverter),     null},
            {PropertyNames.PwdInfoPasswordNotRequired,    "useraccountcontrol",   new FromLdapConverterDelegate(UACFromLdapConverter),                 new ToLdapConverterDelegate(UACToLdapConverter)},
            {PropertyNames.PwdInfoPasswordNeverExpires,   "useraccountcontrol",   new FromLdapConverterDelegate(UACFromLdapConverter),                 new ToLdapConverterDelegate(UACToLdapConverter)},
            {PropertyNames.PwdInfoCannotChangePassword,   null, null,     new ToLdapConverterDelegate(CannotChangePwdToLdapConverter)},
            {PropertyNames.PwdInfoAllowReversiblePasswordEncryption,     "useraccountcontrol",    new FromLdapConverterDelegate(UACFromLdapConverter), new ToLdapConverterDelegate(UACToLdapConverter)}
        };
        /*/
        ///*******  Mapping table for perf testing...
                static object[,] propertyMappingTableRaw =
                {
                    // PropertyName                          AD property             Converter(LDAP->PAPI)                                    Converter(PAPI->LDAP)
                    {PropertyNames.PrincipalDisplayName,     "displayName",          new FromLdapConverterDelegate(StringFromLdapConverter),  new ToLdapConverterDelegate(StringToLdapConverter)},
                    {PropertyNames.PrincipalDescription,     "description",          new FromLdapConverterDelegate(StringFromLdapConverter),  new ToLdapConverterDelegate(StringToLdapConverter)},
        //            {PropertyNames.PrincipalDistinguishedName,  "distinguishedName",    null, null},
                    {PropertyNames.PrincipalSid,  "objectSid",            new FromLdapConverterDelegate(SidFromLdapConverter),  null},
                    {PropertyNames.PrincipalSamAccountName,  "samAccountName",       new FromLdapConverterDelegate(StringFromLdapConverter), new ToLdapConverterDelegate(StringToLdapConverter)},
                    {PropertyNames.PrincipalUserPrincipalName,  "userPrincipalName",    new FromLdapConverterDelegate(StringFromLdapConverter),  new ToLdapConverterDelegate(StringToLdapConverter)},
                    {PropertyNames.PrincipalGuid,  "objectGuid",           new FromLdapConverterDelegate(GuidFromLdapConverter),   null},
                    {PropertyNames.PrincipalStructuralObjectClass,  "objectClass",           null, null},
                    {PropertyNames.PrincipalName,  "name",           new FromLdapConverterDelegate(StringFromLdapConverter), new ToLdapConverterDelegate(StringToLdapConverter)},
                    {PropertyNames.PrincipalExtensionCache,  null,  null, null},

                    {PropertyNames.AuthenticablePrincipalEnabled,      "userAccountControl", null, null},
                  {PropertyNames.AuthenticablePrincipalCertificates, "userCertificate",    null, null},

                    {PropertyNames.GroupIsSecurityGroup,   "groupType", null, null},
                                {PropertyNames.GroupGroupScope, "groupType", null, null},
                   //
                              {PropertyNames.UserGivenName,             "givenName",        null, null},
                               {PropertyNames.UserMiddleName,            "middleName",       null, null},
                                {PropertyNames.UserSurname,               "sn",               null, null},
                                {PropertyNames.UserEmailAddress,          "mail",             null, null},
                                {PropertyNames.UserVoiceTelephoneNumber,  "telephoneNumber",  null, null},
                                {PropertyNames.UserEmployeeID,            "employeeID",       null, null},

                                {PropertyNames.ComputerServicePrincipalNames, "servicePrincipalName", null, null},
                                {PropertyNames.AcctInfoAcctLockoutTime,       "lockoutTime",        null, null},
                                {PropertyNames.AcctInfoLastLogon,             "lastLogon",          null, null},
                                {PropertyNames.AcctInfoLastLogon,             "lastLogonTimestamp", null, null},
                                {PropertyNames.AcctInfoPermittedWorkstations, "userWorkstations",   null, null},
                                {PropertyNames.AcctInfoPermittedLogonTimes,   "logonHours",         null, null},
                    {PropertyNames.AcctInfoExpirationDate,        "accountExpires",     new FromLdapConverterDelegate(AcctExpirFromLdapConverter),       new ToLdapConverterDelegate(AcctExpirToLdapConverter)},
                                {PropertyNames.AcctInfoSmartcardRequired,     "userAccountControl", null, null},
                                {PropertyNames.AcctInfoDelegationPermitted,   "userAccountControl", null, null},
                                {PropertyNames.AcctInfoBadLogonCount,         "badPwdCount",        null, null},
                                {PropertyNames.AcctInfoHomeDirectory,         "homeDirectory",      null, null},
                                {PropertyNames.AcctInfoHomeDrive,             "homeDrive",          null, null},
                                {PropertyNames.AcctInfoScriptPath,            "scriptPath",         null, null},

                                {PropertyNames.PwdInfoLastPasswordSet,        "pwdLastSet",           null, null},
                                {PropertyNames.PwdInfoLastBadPasswordAttempt, "badPasswordTime",      null, null},
                                {PropertyNames.PwdInfoPasswordNotRequired,    "userAccountControl",   null, null},
                                {PropertyNames.PwdInfoPasswordNeverExpires,   "userAccountControl",   null, null},
        //            {PropertyNames.PwdInfoCannotChangePassword,   "ntSecurityDescriptor", null,     new ToLdapConverterDelegate(CannotChangePwdToLdapConverter)},
                                {PropertyNames.PwdInfoAllowReversiblePasswordEncryption,     "userAccountControl",    null, null}
                };

        /************************/
        // This table only includes properties that are writeable.
        private static Hashtable s_propertyMappingTableByProperty;
        private static Hashtable s_propertyMappingTableByLDAP;
        protected static Dictionary<string, bool> NonPresentAttrDefaultStateMapping;
        private static Hashtable s_propertyMappingTableByPropertyFull;

        protected static Dictionary<int, Dictionary<Type, StringCollection>> TypeToLdapPropListMap;

        private sealed class PropertyMappingTableEntry
        {
            internal string propertyName;               // PAPI name
            internal string suggestedADPropertyName;    // LDAP attribute name
            internal FromLdapConverterDelegate ldapToPapiConverter;
            internal ToLdapConverterDelegate papiToLdapConverter;
        }

        //
        // Conversion routines
        //

        // Loads the specified attribute of the DirectoryEntry into the specified property of the Principal
        protected delegate void FromLdapConverterDelegate(dSPropertyCollection properties, string suggestedAdProperty, Principal p, string propertyName);

        // Loads the specified property of the Principal into the specified attribute of the DirectoryEntry.
        // For multivalued attributes, must test to make sure the value hasn't already been loaded into the DirectoryEntry
        // (to maintain idempotency when PushChangesToNative is called multiple times).
        protected delegate void ToLdapConverterDelegate(Principal p, string propertyName, DirectoryEntry de, string suggestedAdProperty);

        //
        // Conversion: LDAP --> PAPI
        //
        protected static void SidFromLdapConverter(dSPropertyCollection properties, string suggestedAdProperty, Principal p, string propertyName)
        {
            if (properties["objectSid"].Count > 0)
            {
                byte[] sid = (byte[])properties["objectSid"][0];

                SecurityIdentifier SecurityId = new SecurityIdentifier(sid, 0);

                p.LoadValueIntoProperty(propertyName, (object)SecurityId);
            }
            else
            {
                p.LoadValueIntoProperty(propertyName, null);
            }
        }

        protected static void GuidFromLdapConverter(dSPropertyCollection properties, string suggestedAdProperty, Principal p, string propertyName)
        {
            Debug.Assert(properties["objectGuid"].Count == 1);

            if (properties["objectGuid"].Count == 1)
            {
                byte[] guid = (byte[])properties["objectGuid"][0];

                Guid g = new Guid(guid);

                p.LoadValueIntoProperty(propertyName, g);
            }
            else
            {
                p.LoadValueIntoProperty(propertyName, null);
            }
        }

        protected static void StringFromLdapConverter(dSPropertyCollection properties, string suggestedAdProperty, Principal p, string propertyName)
        {
            SDSUtils.SingleScalarFromDirectoryEntry<string>(properties, suggestedAdProperty, p, propertyName);
        }

        protected static void MultiStringFromLdapConverter(dSPropertyCollection properties, string suggestedAdProperty, Principal p, string propertyName)
        {
            // ValueCollection<string> is Load'ed from a List<string>
            SDSUtils.MultiScalarFromDirectoryEntry<string>(properties, suggestedAdProperty, p, propertyName);
        }

        protected static void BoolFromLdapConverter(dSPropertyCollection properties, string suggestedAdProperty, Principal p, string propertyName)
        {
            SDSUtils.SingleScalarFromDirectoryEntry<bool>(properties, suggestedAdProperty, p, propertyName);
        }

        // This function is converting the disabled directory status into the enabled principal property
        // the boolan value needs to be negated
        protected static void AcctDisabledFromLdapConverter(dSPropertyCollection properties, string suggestedAdProperty, Principal p, string propertyName)
        {
            if (properties[suggestedAdProperty].Count > 0)
            {
                // We're intended to handle single-valued scalar properties
                Debug.Assert(properties[suggestedAdProperty].Count == 1);
                Debug.Assert(properties[suggestedAdProperty][0] is bool);

                p.LoadValueIntoProperty(propertyName, !(bool)properties[suggestedAdProperty][0]);
            }
        }

        protected static void CommaStringFromLdapConverter(dSPropertyCollection properties, string suggestedAdProperty, Principal p, string propertyName)
        {
            Debug.Assert(string.Equals(suggestedAdProperty, "userWorkstations", StringComparison.OrdinalIgnoreCase));

            // The userWorkstations attribute is odd.  Rather than being a multivalued string attribute, it's a single-valued
            // string of comma-separated values.

            dSPropertyValueCollection values = properties[suggestedAdProperty];

            if (values.Count != 0)
            {
                Debug.Assert(values.Count == 1);
                Debug.Assert(values[0] is string);

                string commaSeparatedValues = (string)values[0];
                string[] individualValues = commaSeparatedValues.Split(s_comma);

                // ValueCollection<string> is Load'ed from a List<string>
                List<string> list = new List<string>(individualValues.Length);

                foreach (string s in individualValues)
                {
                    list.Add(s);
                }

                p.LoadValueIntoProperty(propertyName, list);
            }
        }

        protected static void IntFromLdapConverter(dSPropertyCollection properties, string suggestedAdProperty, Principal p, string propertyName)
        {
            SDSUtils.SingleScalarFromDirectoryEntry<int>(properties, suggestedAdProperty, p, propertyName);
        }

        protected static void BinaryFromLdapConverter(dSPropertyCollection properties, string suggestedAdProperty, Principal p, string propertyName)
        {
            SDSUtils.SingleScalarFromDirectoryEntry<byte[]>(properties, suggestedAdProperty, p, propertyName);
        }

        protected static void CertFromLdapConverter(dSPropertyCollection properties, string suggestedAdProperty, Principal p, string propertyName)
        {
            // Cert collection is Load'ed via a list of byte[], each representing a cert
            SDSUtils.MultiScalarFromDirectoryEntry<byte[]>(properties, suggestedAdProperty, p, propertyName);
        }

        protected static void UACFromLdapConverter(dSPropertyCollection properties, string suggestedAdProperty, Principal p, string propertyName)
        {
            Debug.Assert(string.Equals(suggestedAdProperty, "userAccountControl", StringComparison.OrdinalIgnoreCase));

            SDSUtils.AccountControlFromDirectoryEntry(properties, suggestedAdProperty, p, propertyName, false);
        }

        protected static void GenericDateTimeFromLdapConverter(dSPropertyCollection properties, string suggestedAdProperty, Principal p, string propertyName)
        {
            DateTimeFromLdapConverter(properties, suggestedAdProperty, p, propertyName, false);
        }

        protected static void ObjectClassFromLdapConverter(dSPropertyCollection properties, string suggestedAdProperty, Principal p, string propertyName)
        {
            dSPropertyValueCollection values = properties[suggestedAdProperty];

            if (values.Count > 0)
            {
                // This is a multivalued attribute and we want the last element.  The most specialized object class...
                Debug.Assert(values[values.Count - 1] is string);
                p.LoadValueIntoProperty(propertyName, (string)values[values.Count - 1]);
            }
        }

        protected static void LastLogonFromLdapConverter(dSPropertyCollection properties, string suggestedAdProperty, Principal p, string propertyName)
        {
            // W2k DCs support just "lastLogon".  W2k3 DCs also support "lastLogonTimestamp".  The latter is replicated, and
            // preferred over the former.
            if (string.Equals(suggestedAdProperty, "lastLogon", StringComparison.OrdinalIgnoreCase))
            {
                // Is "lastLogonTimestamp" available instead?

                if (properties["lastLogonTimestamp"].Count != 0)
                {
                    GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "LastLogonFromLdapConverter: found lastLogonTimestamp");

                    // "lastLogonTimestamp" is available.  So ignore "lastLogon" and do nothing.  We'll be called again
                    // when Load() gets to the "lastLogonTimestamp" attribute (or it may already have).
                    return;
                }
            }

            // Either we're processing "lastLogonTimestamp", or there is no "lastLogonTimestamp" and we're processing "lastLogon".
            // Either way, it's handled like a generic date.
            DateTimeFromLdapConverter(properties, suggestedAdProperty, p, propertyName, false);
        }

        protected static void AcctExpirFromLdapConverter(dSPropertyCollection properties, string suggestedAdProperty, Principal p, string propertyName)
        {
            DateTimeFromLdapConverter(properties, suggestedAdProperty, p, propertyName, true);
        }

        protected static void DateTimeFromLdapConverter(dSPropertyCollection properties, string suggestedAdProperty, Principal p, string propertyName, bool useAcctExpLogic)
        {
            dSPropertyValueCollection values = properties[suggestedAdProperty];

            if (values.Count != 0)
            {
                Debug.Assert(values.Count == 1);

                long filetime;

                if (values[0] is long)
                    filetime = (long)values[0];
                else
                    filetime = ADUtils.LargeIntToInt64((UnsafeNativeMethods.IADsLargeInteger)values[0]);

                Nullable<DateTime> dt;

                // Filetimes use "0" to mean "no value, except for accountExpires,
                // which uses both "0" and "0x7fffffffffffffff"  to mean "no expiration date"
                if ((!useAcctExpLogic) && (filetime == 0x0))
                    dt = null;
                else if ((useAcctExpLogic) && (filetime == 0x0 || filetime == 0x7fffffffffffffff))
                    dt = null;
                else
                    dt = ADUtils.ADFileTimeToDateTime(filetime);

                p.LoadValueIntoProperty(propertyName, dt);
            }
        }

        protected static void GroupTypeFromLdapConverter(dSPropertyCollection properties, string suggestedAdProperty, Principal p, string propertyName)
        {
            Debug.Assert(string.Equals(suggestedAdProperty, "groupType", StringComparison.OrdinalIgnoreCase));

            dSPropertyValueCollection values = properties[suggestedAdProperty];

            if (values.Count != 0)
            {
                Debug.Assert(values.Count == 1);
                Debug.Assert(values[0] is int);

                int groupTypeCombined = (int)values[0];

                switch (propertyName)
                {
                    case PropertyNames.GroupIsSecurityGroup:

                        bool isSecurityEnabled = false;

                        // GROUP_TYPE_SECURITY_ENABLED
                        if ((groupTypeCombined & 0x80000000) != 0)
                            isSecurityEnabled = true;

                        // isSecurityEnabled --> group IS enabled
                        p.LoadValueIntoProperty(propertyName, isSecurityEnabled);
                        break;

                    case PropertyNames.GroupGroupScope:

                        GroupScope groupType = GroupScope.Universal;

                        if ((groupTypeCombined & ADGroupScope.Global) != 0) // ADS_GROUP_TYPE_GLOBAL_GROUP
                            groupType = GroupScope.Global;
                        else if ((groupTypeCombined & ADGroupScope.Local) != 0) // ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP
                            groupType = GroupScope.Local;
                        else
                            Debug.Assert((groupTypeCombined & ADGroupScope.Universal) != 0); // ADS_GROUP_TYPE_UNIVERSAL_GROUP

                        p.LoadValueIntoProperty(propertyName, groupType);
                        break;

                    default:
                        Debug.Fail("ADStoreCtx.GroupTypeFromLdapConverter: Fell off end looking for " + propertyName);
                        break;
                }
            }
        }

        protected static bool CannotChangePwdFromLdapConverter(DirectoryEntry de)
        {
            // We use the same logic as the AD Users & Computers snapin.  We scan the DACL,
            // looking for ALLOW or DENY ACEs for the "user can change password" right.  We scan
            // for both self and world ACEs.  If we find neither explitic allow nor explicit deny ACEs,
            // we (like the AD U&C snapin) default to assuming the user can change their password.

            // *******************************
            // retrieving ObjectSecurity after
            // previously modifying the ACL will return null unless we force a cache refresh.  We have to do this always,
            // even before we call ObjectSecurity to see if it would return null, because once ObjectSecurity returns null the
            // first time, it'll keep returning null even if we refresh the cache.

            if (!de.Properties.Contains("nTSecurityDescriptor"))
                de.RefreshCache(s_nTSecurityDescriptor);

            ActiveDirectorySecurity adsSecurity = de.ObjectSecurity;

            bool denySelfFound;
            bool denyWorldFound;
            bool allowSelfFound;
            bool allowWorldFound;

            // Perform the scan
            ScanACLForChangePasswordRight(adsSecurity, out denySelfFound, out denyWorldFound, out allowSelfFound, out allowWorldFound);

            // Map the ACEs found to "user cannot change password" status
            bool userCannotChangePassword;

            if (denySelfFound || denyWorldFound)
            {
                // Explicit deny --> user can't change password
                userCannotChangePassword = true;
            }
            else if ((!denySelfFound && !denyWorldFound) && (allowSelfFound || allowWorldFound))
            {
                // Explitic allow --> user can change password
                userCannotChangePassword = false;
            }
            else
            {
                // As with the AD U&C snapin, if the explitic ACEs don't tell us anything, we
                // fallback to assuming the user can change their password
                userCannotChangePassword = false;
                GlobalDebug.WriteLineIf(GlobalDebug.Warn, "ADStoreCtx", "CannotChangePwdFromLdapConverter: fallback, assume user can change pwd");
            }

            return userCannotChangePassword;
        }

        private const string SelfSddl = "S-1-5-10";  // NT AUTHORITY\SELF
        private const string WorldSddl = "S-1-1-0";   // EVERYONE
        private static readonly Guid s_changePasswordGuid = new Guid("{ab721a53-1e2f-11d0-9819-00aa0040529b}");

        protected static void ScanACLForChangePasswordRight(
                                        ActiveDirectorySecurity adsSecurity,
                                        out bool denySelfFound,
                                        out bool denyWorldFound,
                                        out bool allowSelfFound,
                                        out bool allowWorldFound)
        {
            denySelfFound = false;
            denyWorldFound = false;
            allowSelfFound = false;
            allowWorldFound = false;

            MACLPrinc.SecurityIdentifier trustee;

            foreach (ActiveDirectoryAccessRule rule in adsSecurity.GetAccessRules(true, true, typeof(MACLPrinc.SecurityIdentifier)))
            {
                trustee = (MACLPrinc.SecurityIdentifier)rule.IdentityReference;
                string sidSddl = trustee.Value;

                if (rule.ObjectType == s_changePasswordGuid)
                {
                    if (rule.AccessControlType == AccessControlType.Deny)
                    {
                        if (sidSddl == SelfSddl)
                        {
                            denySelfFound = true;
                        }
                        else if (sidSddl == WorldSddl)
                        {
                            denyWorldFound = true;
                        }
                    }
                    else if (rule.AccessControlType == AccessControlType.Allow)
                    {
                        if (sidSddl == SelfSddl)
                        {
                            allowSelfFound = true;
                        }
                        else if (sidSddl == WorldSddl)
                        {
                            allowWorldFound = true;
                        }
                    }
                }
            }
        }

        //
        // Conversion: PAPI --> LDAP
        //

        protected static void StringToLdapConverter(Principal p, string propertyName, DirectoryEntry de, string suggestedAdProperty)
        {
            string value = (string)p.GetValueForProperty(propertyName);

            if (p.unpersisted && value == null)
                return;

            if ((value == null) || (value.Length > 0))
                de.Properties[suggestedAdProperty].Value = value;
            else
                throw new ArgumentException(SR.Format(SR.InvalidStringValueForStore, propertyName));
        }

        protected static void BinaryToLdapConverter(Principal p, string propertyName, DirectoryEntry de, string suggestedAdProperty)
        {
            byte[] value = (byte[])p.GetValueForProperty(propertyName);

            if (p.unpersisted && value == null)
                return;

            if (value != null && value.Length != 0)
                de.Properties[suggestedAdProperty].Value = value;
            else
                de.Properties[suggestedAdProperty].Value = null;
        }

        protected static void MultiStringToLdapConverter(Principal p, string propertyName, DirectoryEntry de, string suggestedAdProperty)
        {
            SDSUtils.MultiStringToDirectoryEntryConverter(p, propertyName, de, suggestedAdProperty);
        }

        protected static void BoolToLdapConverter(Principal p, string propertyName, DirectoryEntry de, string suggestedAdProperty)
        {
            object value = (bool)p.GetValueForProperty(propertyName);

            if (p.unpersisted && value == null)
                return;

            if (value != null)
                de.Properties[suggestedAdProperty].Value = (bool)value;
            else
                de.Properties[suggestedAdProperty].Value = null;
        }

        // This function is converting the disabled directory status into the enabled principal property
        // the boolan value needs to be negated
        protected static void AcctDisabledToLdapConverter(Principal p, string propertyName, DirectoryEntry de, string suggestedAdProperty)
        {
            // Only modify disabled property if we are talking to an already persisted user.
            // We need to set the password before we can enable the user on new objects.
            if (!p.unpersisted)
            {
                object value = (bool)p.GetValueForProperty(propertyName);
                if (value != null)
                    de.Properties[suggestedAdProperty].Value = !(bool)value;
                else
                    de.Properties[suggestedAdProperty].Value = null;
            }
        }

        protected static void CommaStringToLdapConverter(Principal p, string propertyName, DirectoryEntry de, string suggestedAdProperty)
        {
            PrincipalValueCollection<string> trackingList = (PrincipalValueCollection<string>)p.GetValueForProperty(propertyName);

            StringBuilder sb = new StringBuilder();

            foreach (string value in trackingList)
            {
                // Preexisting values that have not been removed.
                // This also includes inserted values.
                sb.Append(value);
                sb.Append(',');
            }

            // We have an extra comma at the end (assuming we added any values to the string).  Remove it.
            if (sb.Length > 0)
                sb.Remove(sb.Length - 1, 1);

            string s = (sb.Length > 0) ? sb.ToString() : null;

            if (p.unpersisted && s == null)
                return;

            de.Properties[suggestedAdProperty].Value = s;
        }

        protected static void CertToLdap(Principal p, string propertyName, DirectoryEntry de, string suggestedAdProperty)
        {
            X509Certificate2Collection certificates = (X509Certificate2Collection)p.GetValueForProperty(propertyName);

            if (certificates.Count == 0)
            {
                // Clear out the certificates
                de.Properties[suggestedAdProperty].Value = null;
            }
            else
            {
                // Replace the existing certs with the modified certs.  Note that this replaces all the certs ---
                // X509CertificateExCollection doesn't expose a finer-grained change-tracking mechanism
                byte[][] rawCerts = new byte[certificates.Count][];

                for (int i = 0; i < certificates.Count; i++)
                {
                    rawCerts[i] = certificates[i].RawData;
                }

                de.Properties[suggestedAdProperty].Value = null;        // remove the old
                de.Properties[suggestedAdProperty].Value = rawCerts;    // and put in the new
            }
        }

        protected static void UACToLdapConverter(Principal p, string propertyName, DirectoryEntry de, string suggestedAdProperty)
        {
            Debug.Assert(string.Equals(suggestedAdProperty, "userAccountControl", StringComparison.OrdinalIgnoreCase));

            SDSUtils.AccountControlToDirectoryEntry(p, propertyName, de, suggestedAdProperty, false, p.unpersisted);
        }

        protected static void AcctExpirToLdapConverter(Principal p, string propertyName, DirectoryEntry de, string suggestedAdProperty)
        {
            Nullable<DateTime> dt = (Nullable<DateTime>)p.GetValueForProperty(propertyName);

            if (p.unpersisted && dt == null)
                return;

            UnsafeNativeMethods.ADsLargeInteger largeIntObj = new UnsafeNativeMethods.ADsLargeInteger();
            UnsafeNativeMethods.IADsLargeInteger largeInt = (UnsafeNativeMethods.IADsLargeInteger)largeIntObj;

            if (!dt.HasValue)
            {
                // no expiration date
                largeInt.LowPart = unchecked((int)0xffffffff);
                largeInt.HighPart = (int)0x7fffffff;
            }
            else
            {
                long filetime = ADUtils.DateTimeToADFileTime(dt.Value);

                uint lowPart = (uint)(((ulong)filetime) & ((ulong)0x00000000ffffffff));
                uint highPart = (uint)((((ulong)filetime) & ((ulong)0xffffffff00000000)) >> 32);

                largeInt.LowPart = (int)lowPart;
                largeInt.HighPart = (int)highPart;
            }

            de.Properties[suggestedAdProperty].Value = largeInt;
        }

        // Supported types
        // ICollection where an item of the collection is not an ICollection or IList
        // object[]
        // object
        protected static void ExtensionCacheToLdapConverter(Principal p, string propertyName, DirectoryEntry de, string suggestedAdProperty)
        {
            ExtensionCache cacheValues = (ExtensionCache)p.GetValueForProperty(propertyName);
            GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "ExtensionCacheToLdapConverter");

            foreach (KeyValuePair<string, ExtensionCacheValue> kvp in cacheValues.properties)
            {
                if (!kvp.Value.Filter && null != kvp.Value.Value && kvp.Value.Value.Length != 0)
                {
                    // array of objects   ( .Length > 1 && typeof(array[0] != ICollection or IList )
                    // Single collection ( .Length == 1 ) &&  typeof(array[0] == ICollection ) && typeof(array[0][0] != ICollection or IList )

                    GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "ExtensionCacheToLdapConverter - Value Type " + kvp.Value.Value.GetType().ToString());

                    if ((kvp.Value.Value.Length == 1 && kvp.Value.Value[0] is ICollection) || (kvp.Value.Value.Length > 1))
                    {
                        if (kvp.Value.Value.Length > 1 && (kvp.Value.Value[0] is ICollection))
                            throw new ArgumentException(SR.InvalidExtensionCollectionType);

                        GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "ExtensionCacheToLdapConverter - Value implements ICollection");

                        ICollection valueCollection;

                        // byte[] gets special treatment (following S.DS and ADSI) - we don't treat it as ICollection but rather as a whole
                        if (kvp.Value.Value.Length == 1 && kvp.Value.Value[0] is ICollection && !(kvp.Value.Value[0] is byte[]))
                        {
                            valueCollection = (ICollection)kvp.Value.Value[0];
                        }
                        else
                        {
                            valueCollection = (ICollection)kvp.Value.Value;
                        }

                        // We make a local copy of all elements to set, instead of adding them to the real property
                        // directly. This allows us to override all existing elements without using Clear() and then Add(),
                        // as that order sends a Clear operation and then a number of Append operations, which will fail.
                        // Instead, setting the new list all at once will send a Clear operation and then an Update operation.
                        var propertyValueList = new List<object>();

                        foreach (object oVal in valueCollection)
                        {
                            if (null != oVal)
                            {
                                if ((oVal is ICollection || oVal is IList) && !(oVal is byte[]))
                                    throw new ArgumentException(SR.InvalidExtensionCollectionType);

                                GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "ExtensionCacheToLdapConverter - Element Value Type " + oVal.GetType().ToString());
                                GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "ExtensionCacheToLdapConverter - Adding  Element " + oVal.ToString());
                            }

                            // Do nothing if we are not persisted and the value is null.  We can't delete a property that
                            // has not already been set.
                            if (p.unpersisted && null == oVal)
                                continue;

                            propertyValueList.Add(oVal);
                        }

                        de.Properties[kvp.Key].Value = propertyValueList.ToArray();

                        GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "ExtensionCacheToLdapConverter - Collection complete");
                    }
                    else
                    {
                        GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "ExtensionCacheToLdapConverter - Adding " + kvp.Value.Value.ToString());
                        // Do nothing if we are not persisted and the value is null.  We can't delete a property that
                        // has not already been set.
                        if (p.unpersisted && (null == kvp.Value.Value[0]))
                            continue;

                        de.Properties[kvp.Key].Value = kvp.Value.Value[0];
                    }
                }
            }
        }

        protected static void GroupTypeToLdapConverter(Principal p, string propertyName, DirectoryEntry de, string suggestedAdProperty)
        {
            Debug.Assert(propertyName == PropertyNames.GroupIsSecurityGroup || propertyName == PropertyNames.GroupGroupScope);

            int groupTypeCombined;

            // We want to get the current value, so we can flip the appropriate bit while leaving the other bits as-is.
            // If this is a to-be-inserted Principal, we may not have an existing groupType, so we'll use AD's default value
            // for new groups.
            if (de.Properties[suggestedAdProperty].Count > 0)
            {
                Debug.Assert(de.Properties[suggestedAdProperty].Count == 1);

                groupTypeCombined = (int)de.Properties[suggestedAdProperty][0];
            }
            else
            {
                if (!p.unpersisted)
                {
                    // It's not an unpersisted principal, so we should have the property.  Perhaps we don't have access
                    // to it.  In that case, we don't want to blindly overwrite whatever other bits might be there.
                    throw new PrincipalOperationException(
                                    SR.ADStoreCtxUnableToReadExistingGroupTypeFlagsForUpdate);
                }

                // initial default value
                groupTypeCombined = unchecked((int)(0x80000000 | 0x00000002));  //  GROUP_TYPE_SECURITY_ENABLED | GROUP_TYPE_ACCOUNT_GROUP
            }

            switch (propertyName)
            {
                case PropertyNames.GroupIsSecurityGroup:
                    bool groupEnabled = (bool)p.GetValueForProperty(propertyName);

                    // Flip the bit without touching the other bits.
                    if (!groupEnabled)
                        Utils.ClearBit(ref groupTypeCombined, 0x80000000);   // disabled --> clear GROUP_TYPE_SECURITY_ENABLED
                    else
                        Utils.SetBit(ref groupTypeCombined, 0x80000000);     // enabled --> set GROUP_TYPE_SECURITY_ENABLED

                    break;

                case PropertyNames.GroupGroupScope:

                    GroupScope groupType = (GroupScope)p.GetValueForProperty(propertyName);

                    // Remove the bits indicating the group type it currently is...
                    Utils.ClearBit(ref groupTypeCombined, ADGroupScope.Local);
                    Utils.ClearBit(ref groupTypeCombined, ADGroupScope.Global);
                    Utils.ClearBit(ref groupTypeCombined, ADGroupScope.Universal);

                    // ...and set the bit for the group type we want it to be
                    if (groupType == GroupScope.Local)
                    {
                        Utils.SetBit(ref groupTypeCombined, ADGroupScope.Local);
                    }
                    else if (groupType == GroupScope.Global)
                    {
                        Utils.SetBit(ref groupTypeCombined, ADGroupScope.Global);
                    }
                    else
                    {
                        Debug.Assert(groupType == GroupScope.Universal);
                        Utils.SetBit(ref groupTypeCombined, ADGroupScope.Universal);
                    }

                    break;

                default:
                    Debug.Fail("ADStoreCtx.GroupTypeToLdapConverter: Fell off end looking for " + propertyName);
                    break;
            }

            de.Properties[suggestedAdProperty].Value = groupTypeCombined;
        }

        // {PropertyNames.GroupMembers,  "members",   null,                                                      new ToLdapConverterDelegate(GroupMembersToLdapConverter)},

        protected static void UpdateGroupMembership(Principal group, DirectoryEntry de, NetCred credentials, AuthenticationTypes authTypes)
        {
            Debug.Assert(!group.fakePrincipal);

            PrincipalCollection members = (PrincipalCollection)group.GetValueForProperty(PropertyNames.GroupMembers);

            DirectoryEntry groupDe = null;

            try
            {
                //
                // Process clear
                //
                if (members.Cleared)
                {
                    DirectoryEntry copyOfDe = null;

                    try
                    {
                        GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "UpdateGroupMembership: clearing {0}", de.Path);

                        copyOfDe = SDSUtils.BuildDirectoryEntry(
                                                de.Path,
                                                credentials,
                                                authTypes);

                        Debug.Assert(copyOfDe != null);
                        copyOfDe.Properties["member"].Clear();
                        copyOfDe.CommitChanges();
                    }
                    finally
                    {
                        copyOfDe?.Dispose();
                    }
                }

                //
                // Process inserted members
                //

                List<Principal> insertedMembers = members.Inserted;
                List<Principal> removedMembers = members.Removed;

                if (insertedMembers.Count > 0 || removedMembers.Count > 0)
                {
                    groupDe = SDSUtils.BuildDirectoryEntry(
                                            de.Path,
                                            credentials,
                                            authTypes);
                }

                // First, validate the members to be added
                foreach (Principal member in insertedMembers)
                {
                    Type memberType = member.GetType();
                    if ((memberType != typeof(UserPrincipal)) && (!memberType.IsSubclassOf(typeof(UserPrincipal))) &&
                        (memberType != typeof(ComputerPrincipal)) && (!memberType.IsSubclassOf(typeof(ComputerPrincipal))) &&
                        (memberType != typeof(GroupPrincipal)) && (!memberType.IsSubclassOf(typeof(GroupPrincipal))) &&
                        (!memberType.IsSubclassOf(typeof(AuthenticablePrincipal))))
                    {
                        throw new InvalidOperationException(
                                        SR.Format(SR.StoreCtxUnsupportedPrincipalTypeForGroupInsert, memberType));
                    }
                    // Can't inserted unpersisted principal
                    if (member.unpersisted)
                        throw new InvalidOperationException(SR.StoreCtxGroupHasUnpersistedInsertedPrincipal);

                    Debug.Assert(member.Context != null);

                    // Can only insert AD principals (no reg-SAM/MSAM principals)
                    if (member.ContextType == ContextType.Machine)
                        throw new InvalidOperationException(SR.ADStoreCtxUnsupportedPrincipalContextForGroupInsert);
                }

                // Now add each member to the group
                foreach (Principal member in insertedMembers)
                {
                    // For objects in the current domain or any other domains in the forest we need to use the objects DN
                    // SID path would work for current domain but would not work for child or parent domains.
                    // For foreign objects we must use SID path e.g.  "<SID=...>" so that the necessary FPO gets autocreated by AD.
                    // It also works in the "fake principal" case (which are always represented as FPOs).
                    if (!member.fakePrincipal && ADUtils.ArePrincipalsInSameForest(group, member))
                    {
                        GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "UpdateGroupMembership: add {0}", member.DistinguishedName);

                        groupDe.Properties["member"].Add(member.DistinguishedName);
                    }
                    else
                    {
                        // Build a SID DN.  This needs to be a DN path not an ADSI sid path with the LDAP prefix.
                        string memberSidDN = GetSidPathFromPrincipal(member);

                        if (memberSidDN == null)
                            throw new PrincipalOperationException(SR.ADStoreCtxCouldntGetSIDForGroupMember);

                        GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "UpdateGroupMembership: add {0}", memberSidDN);

                        // Add the member to the group
                        groupDe.Properties["member"].Add(memberSidDN);
                    }
                }

                // If we had any members then commit them.
                if (insertedMembers.Count > 0)
                    groupDe.CommitChanges();

                //
                // Process removed members
                //

                foreach (Principal member in removedMembers)
                {
                    // Since we don't allow any of these to be inserted, none of them should ever
                    // show up in the removal list
                    Debug.Assert(!member.unpersisted);
                    Debug.Assert(member.ContextType == ContextType.Domain || member.ContextType == ContextType.ApplicationDirectory);

                    // If the collection was cleared, there should be no original members to remove
                    Debug.Assert(!members.Cleared);

                    // Since we are using PropertyValueCollection to do the item removal we are constrainted to items that are in the collection
                    // For principals that are in the same forest just use their DN to do the removal.  This is how they are represented in the member attr.
                    // For foreign principals we must represent them with their SID binding string since they are locally represented by an FSP object.
                    if (!member.fakePrincipal && ADUtils.ArePrincipalsInSameForest(group, member))
                    {
                        GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "UpdateGroupMembership: remove via DN {0}", member.DistinguishedName);

                        // Remove the member from the group

                        groupDe.Properties["member"].Remove(member.DistinguishedName);
                    }
                    else
                    {
                        // SID DN case

                        // Build a SID DN.
                        string memberSidDN = GetSidPathFromPrincipal(member);

                        if (memberSidDN == null)
                            throw new PrincipalOperationException(SR.ADStoreCtxCouldntGetSIDForGroupMember);

                        GlobalDebug.WriteLineIf(GlobalDebug.Info, "ADStoreCtx", "UpdateGroupMembership: remove via SID {0}", memberSidDN);

                        // Remove the member from the group
                        groupDe.Properties["member"].Remove(memberSidDN);
                    }
                }

                // If we used the collection to do a modification then commit it.
                if (removedMembers.Count > 0)
                    groupDe.CommitChanges();
            }
            catch (System.Runtime.InteropServices.COMException e)
            {
                throw ExceptionHelper.GetExceptionFromCOMException(e);
            }
            finally
            {
                groupDe?.Dispose();
            }
        }

        private static readonly string[] s_objectSid = new string[] { "objectSid" };

        // Builds a SID dn for the principal <SID=...>
        protected static string GetSidPathFromPrincipal(Principal p)
        {
            Debug.Assert(!p.unpersisted);

            if (p.fakePrincipal)
            {
                SecurityIdentifier Sid = p.Sid;
                if (Sid == null)
                {
                    GlobalDebug.WriteLineIf(GlobalDebug.Warn, "ADStoreCtx", "GetSidADsPathFromPrincipal: no SID IC (fake principal)");
                    throw new InvalidOperationException(SR.StoreCtxNeedValueSecurityIdentityClaimToQuery);
                }

                return @"<SID=" + Utils.SecurityIdentifierToLdapHexBindingString(Sid) + ">";
            }
            else
            {
                // Retrieve the member's SID
                DirectoryEntry de = (DirectoryEntry)p.UnderlyingObject;
                Debug.Assert(de != null); // since member can't be unpersisted

                // Force it to load if it hasn't been already loaded
                if (!de.Properties.Contains("objectSid"))
                    de.RefreshCache(s_objectSid);

                byte[] sid = (byte[])de.Properties["objectSid"].Value;

                if (sid == null)
                {
                    GlobalDebug.WriteLineIf(GlobalDebug.Warn, "ADStoreCtx", "GetSidADsPathFromPrincipal: no SID");
                    return null;
                }

                // Build a SID DN
                return @"<SID=" + Utils.ByteArrayToString(sid) + ">";
            }
        }

        protected static void CannotChangePwdToLdapConverter(Principal p, string propertyName, DirectoryEntry de, string suggestedAdProperty)
        {
            Debug.Assert(propertyName == PropertyNames.PwdInfoCannotChangePassword);

            // Only modify disabled property if we are talking to an already persisted user.
            // We need to set the password before we can enable the user on new objects.
            // We can't read the sec desc of an unpersisted object so just return here.  this will be set later.
            if (p.unpersisted)
            {
                return;
            }

            SetCannotChangePasswordStatus(p, (bool)p.GetValueForProperty(propertyName), false);
        }

        protected bool BuildLdapFilterFromIdentityClaim(string urnValue, string urnScheme, ref string filter, bool useSidHistory, bool throwOnFail)
        {
            // To build the filter, we'll use the same IdentityClaimConverter routine as the QBE mechanism.
            // This routine takes an IdentityClaimFilter as input, which in turn wraps an IdentityClaim.
            // So we'll build an IdentityClaim from the user's parameters.

            IdentityClaim ic = new IdentityClaim();
            ic.UrnValue = urnValue;
            ic.UrnScheme = urnScheme;

            IdentityClaimFilter icFilterBase = new IdentityClaimFilter();
            icFilterBase.Value = ic;

            if (useSidHistory)
            {
                // Special handling if we want to include the SID History in the search
                Debug.Assert(urnScheme == UrnScheme.SidScheme);
                StringBuilder sb = new StringBuilder();
                if (!SecurityIdentityClaimConverterHelper(urnValue, useSidHistory, sb, throwOnFail))
                {
                    return false;
                }
                filter = sb.ToString();
            }
            else
            {
                if (!IdentityClaimToFilter(urnValue, urnScheme, ref filter, throwOnFail))
                {
                    return false;
                }
            }

            return true;
        }
    }
}