File: System\DirectoryServices\AccountManagement\AD\SDSUtils.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.DirectoryServices;
using System.Globalization;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;

namespace System.DirectoryServices.AccountManagement
{
    internal static class SDSUtils
    {
        internal static Principal SearchResultToPrincipal(SearchResult sr, PrincipalContext owningContext, Type principalType)
        {
            Principal p;

            // Construct an appropriate Principal object.
            // Make* constructs a Principal that is marked persisted
            // and not loaded (p.unpersisted = false, p.loaded = false).

            // Since there should be no more multistore contexts, the owning context IS
            // the specific context

            // If we know the type we should just construct it ourselves so that we don't need to incur the costs of reflection.
            // If this is an extension type then we must reflect teh constructor to create the object.

            if (typeof(UserPrincipal) == principalType)
            {
                p = UserPrincipal.MakeUser(owningContext);
            }
            else if (typeof(ComputerPrincipal) == principalType)
            {
                p = ComputerPrincipal.MakeComputer(owningContext);
            }
            else if (typeof(GroupPrincipal) == principalType)
            {
                p = GroupPrincipal.MakeGroup(owningContext);
            }
            else if (null == principalType ||
                         typeof(AuthenticablePrincipal) == principalType ||
                         typeof(Principal) == principalType)
            {
                if (SDSUtils.IsOfObjectClass(sr, "computer"))
                {
                    p = ComputerPrincipal.MakeComputer(owningContext);
                }
                else if (SDSUtils.IsOfObjectClass(sr, "user"))
                {
                    p = UserPrincipal.MakeUser(owningContext);
                }
                else if (SDSUtils.IsOfObjectClass(sr, "group"))
                {
                    p = GroupPrincipal.MakeGroup(owningContext);
                }
                else
                {
                    p = AuthenticablePrincipal.MakeAuthenticablePrincipal(owningContext);
                }
            }
            else
            {
                p = Principal.MakePrincipal(owningContext, principalType);
            }

            // The DirectoryEntry we're constructing the Principal from
            // will serve as the underlying object for that Principal.
            p.UnderlyingSearchObject = sr;

            // It's up to our caller to assign an appropriate StoreKey.
            // Caller must also populate the underlyingObject field if the P is to be used R/W
            return p;
        }
        // Used to implement StoreCtx.GetAsPrincipal for AD and SAM
        internal static Principal DirectoryEntryToPrincipal(DirectoryEntry de, PrincipalContext owningContext, Type principalType)
        {
            Principal p;

            // Construct an appropriate Principal object.
            // Make* constructs a Principal that is marked persisted
            // and not loaded (p.unpersisted = false, p.loaded = false).

            // Since there should be no more multistore contexts, the owning context IS
            // the specific context

            if (typeof(UserPrincipal) == principalType)
            {
                p = UserPrincipal.MakeUser(owningContext);
            }
            else if (typeof(ComputerPrincipal) == principalType)
            {
                p = ComputerPrincipal.MakeComputer(owningContext);
            }
            else if (typeof(GroupPrincipal) == principalType)
            {
                p = GroupPrincipal.MakeGroup(owningContext);
            }
            else if (null == principalType ||
                         typeof(AuthenticablePrincipal) == principalType ||
                         typeof(Principal) == principalType)
            {
                if (SDSUtils.IsOfObjectClass(de, "computer"))
                {
                    p = ComputerPrincipal.MakeComputer(owningContext);
                }
                else if (SDSUtils.IsOfObjectClass(de, "user"))
                {
                    p = UserPrincipal.MakeUser(owningContext);
                }
                else if (SDSUtils.IsOfObjectClass(de, "group"))
                {
                    p = GroupPrincipal.MakeGroup(owningContext);
                }
                else
                {
                    p = AuthenticablePrincipal.MakeAuthenticablePrincipal(owningContext);
                }
            }
            else
            {
                p = Principal.MakePrincipal(owningContext, principalType);
            }
            // The DirectoryEntry we're constructing the Principal from
            // will serve as the underlying object for that Principal.
            p.UnderlyingObject = de;

            // It's up to our caller to assign an appropriate StoreKey.

            return p;
        }

        private static bool IsOfObjectClass(SearchResult sr, string className)
        {
            Debug.Assert(sr.Path.StartsWith("LDAP:", StringComparison.Ordinal) || sr.Path.StartsWith("GC:", StringComparison.Ordinal));
            return ADUtils.IsOfObjectClass(sr, className);
        }

        private static bool IsOfObjectClass(DirectoryEntry de, string className)
        {
            if (de.Path.StartsWith("WinNT:", StringComparison.Ordinal))
            {
                return SAMUtils.IsOfObjectClass(de, className);
            }
            else
            {
                Debug.Assert(de.Path.StartsWith("LDAP:", StringComparison.Ordinal));
                return ADUtils.IsOfObjectClass(de, className);
            }
        }
        internal static AuthenticationTypes MapOptionsToAuthTypes(ContextOptions options)
        {
            AuthenticationTypes authTypes = AuthenticationTypes.Secure;

            if ((options & ContextOptions.SimpleBind) != 0)
                authTypes = AuthenticationTypes.None;

            if ((options & ContextOptions.ServerBind) != 0)
                authTypes |= AuthenticationTypes.ServerBind;

            if ((options & ContextOptions.SecureSocketLayer) != 0)
                authTypes |= AuthenticationTypes.SecureSocketsLayer;

            if ((options & ContextOptions.Signing) != 0)
                authTypes |= AuthenticationTypes.Signing;

            if ((options & ContextOptions.Sealing) != 0)
                authTypes |= AuthenticationTypes.Sealing;

            return authTypes;
        }

        internal static void MoveDirectoryEntry(DirectoryEntry deToMove, DirectoryEntry newParent, string newName)
        {
            if (newName != null)
                deToMove.MoveTo(newParent, newName);
            else
                deToMove.MoveTo(newParent);
        }

        internal static void DeleteDirectoryEntry(DirectoryEntry deToDelete)
        {
            DirectoryEntry deParent = deToDelete.Parent;

            try
            {
                deParent.Children.Remove(deToDelete);
            }
            finally
            {
                deParent.Dispose();
            }
        }

        internal static void InsertPrincipal(
                                    Principal p,
                                    StoreCtx storeCtx,
                                    GroupMembershipUpdater updateGroupMembership,
                                    NetCred credentials,
                                    AuthenticationTypes authTypes,
                                    bool needToSetPassword)
        {
            GlobalDebug.WriteLineIf(GlobalDebug.Info, "SDSUtils", "Entering InsertPrincipal");

            Debug.Assert(storeCtx != null);
            Debug.Assert(storeCtx is ADStoreCtx || storeCtx is SAMStoreCtx);
            Debug.Assert(p != null);

            if ((!(p is UserPrincipal)) &&
                 (!(p is GroupPrincipal)) &&
                 (!(p is AuthenticablePrincipal)) &&
                 (!(p is ComputerPrincipal)))
            {
                // It's not a type of Principal that we support
                GlobalDebug.WriteLineIf(GlobalDebug.Warn, "SDSUtils", "InsertPrincipal: Bad principal type:" + p.GetType().ToString());

                throw new InvalidOperationException(SR.Format(SR.StoreCtxUnsupportedPrincipalTypeForSave, p.GetType()));
            }

            // Commit the properties
            SDSUtils.ApplyChangesToDirectory(
                                        p,
                                        storeCtx,
                                        updateGroupMembership,
                                        credentials,
                                        authTypes
                                        );

            // Handle any saved-off operations

            // For SAM, we set password elsewhere prior to creating the principal, so needToSetPassword == false
            // For AD, we have to set the password after creating the principal, so needToSetPassword == true
            if (needToSetPassword && p.GetChangeStatusForProperty(PropertyNames.PwdInfoPassword))
            {
                GlobalDebug.WriteLineIf(GlobalDebug.Info, "SDSUtils", "InsertPrincipal: Setting password");

                // Only AuthenticablePrincipals can have PasswordInfo
                Debug.Assert(p is AuthenticablePrincipal);

                string password = (string)p.GetValueForProperty(PropertyNames.PwdInfoPassword);
                Debug.Assert(password != null); // if null, PasswordInfo should not have indicated it was changed

                storeCtx.SetPassword((AuthenticablePrincipal)p, password);
            }

            if (p.GetChangeStatusForProperty(PropertyNames.PwdInfoExpireImmediately))
            {
                // Only AuthenticablePrincipals can have PasswordInfo
                Debug.Assert(p is AuthenticablePrincipal);

                bool expireImmediately = (bool)p.GetValueForProperty(PropertyNames.PwdInfoExpireImmediately);

                if (expireImmediately)
                {
                    GlobalDebug.WriteLineIf(GlobalDebug.Info, "SDSUtils", "InsertPrincipal: Setting pwd expired");

                    storeCtx.ExpirePassword((AuthenticablePrincipal)p);
                }
            }
        }

        internal delegate void GroupMembershipUpdater(Principal p, DirectoryEntry de, NetCred credentials, AuthenticationTypes authTypes);

        internal static void ApplyChangesToDirectory(
                                                Principal p,
                                                StoreCtx storeCtx,
                                                GroupMembershipUpdater updateGroupMembership,
                                                NetCred credentials,
                                                AuthenticationTypes authTypes)
        {
            GlobalDebug.WriteLineIf(GlobalDebug.Info, "SDSUtils", "Entering ApplyChangesToDirectory");
            Debug.Assert(storeCtx != null);
            Debug.Assert(storeCtx is ADStoreCtx || storeCtx is SAMStoreCtx || storeCtx is ADAMStoreCtx);
            Debug.Assert(p != null);
            Debug.Assert(updateGroupMembership != null);

            // Update the properties in the DirectoryEntry.  Note that this does NOT
            // update group membership.
            DirectoryEntry de = (DirectoryEntry)storeCtx.PushChangesToNative(p);
            Debug.Assert(de == p.UnderlyingObject);

            // Commit the property update
            try
            {
                de.CommitChanges();
            }
            catch (System.Runtime.InteropServices.COMException e)
            {
                GlobalDebug.WriteLineIf(GlobalDebug.Error, "SDSUtils", "ApplyChangesToDirectory: caught COMException with message " + e.Message);

                throw (ExceptionHelper.GetExceptionFromCOMException(e));
            }

            if ((p is GroupPrincipal) && (p.GetChangeStatusForProperty(PropertyNames.GroupMembers)))
            {
                GlobalDebug.WriteLineIf(GlobalDebug.Info, "SDSUtils", "ApplyChangesToDirectory: Updating group membership");

                // It's a group, and it's membership has changed.  Commit those membership changes.
                // Note that this is an immediate operation, because it goes through IADsGroup,
                // and does not require a call to de.CommitChanges().
                updateGroupMembership(p, de, credentials, authTypes);
            }
        }

        internal static void SetPassword(DirectoryEntry de, string newPassword)
        {
            Debug.Assert(newPassword != null);  // but it could be an empty string
            Debug.Assert(de != null);

            try
            {
                de.Invoke("SetPassword", new object[] { newPassword });
            }
            catch (System.Reflection.TargetInvocationException e)
            {
                GlobalDebug.WriteLineIf(GlobalDebug.Error, "SDSUtils", "SetPassword: caught TargetInvocationException with message " + e.Message);

                if (e.InnerException is System.Runtime.InteropServices.COMException)
                {
                    if (((System.Runtime.InteropServices.COMException)e.InnerException).ErrorCode == unchecked((int)ExceptionHelper.ERROR_HRESULT_CONSTRAINT_VIOLATION))
                    {
                        // We have a special case of constraint violation here.  We know this is a password failure to convert to this
                        // specialized type instead of the generic InvalidOperationException
                        throw (new PasswordException(((System.Runtime.InteropServices.COMException)e.InnerException).Message, (System.Runtime.InteropServices.COMException)e.InnerException));
                    }
                    else
                    {
                        throw (ExceptionHelper.GetExceptionFromCOMException((System.Runtime.InteropServices.COMException)e.InnerException));
                    }
                }

                // Unknown exception.  We don't want to suppress it.
                throw;
            }
        }

        internal static void ChangePassword(DirectoryEntry de, string oldPassword, string newPassword)
        {
            Debug.Assert(newPassword != null);  // but it could be an empty string
            Debug.Assert(oldPassword != null);  // but it could be an empty string

            Debug.Assert(de != null);

            try
            {
                de.Invoke("ChangePassword", new object[] { oldPassword, newPassword });
            }
            catch (System.Reflection.TargetInvocationException e)
            {
                GlobalDebug.WriteLineIf(GlobalDebug.Error, "SDSUtils", "ChangePassword: caught TargetInvocationException with message " + e.Message);

                if (e.InnerException is System.Runtime.InteropServices.COMException)
                {
                    if (((System.Runtime.InteropServices.COMException)e.InnerException).ErrorCode == unchecked((int)ExceptionHelper.ERROR_HRESULT_CONSTRAINT_VIOLATION))
                    {
                        // We have a special case of constraint violation here.  We know this is a password failure to convert to this
                        // specialized type instead of the generic InvalidOperationException
                        throw (new PasswordException(((System.Runtime.InteropServices.COMException)e.InnerException).Message, (System.Runtime.InteropServices.COMException)e.InnerException));
                    }
                    else
                    {
                        throw (ExceptionHelper.GetExceptionFromCOMException((System.Runtime.InteropServices.COMException)e.InnerException));
                    }
                }

                // Unknown exception.  We don't want to suppress it.
                throw;
            }
        }

        internal static DirectoryEntry BuildDirectoryEntry(string path, NetCred credentials, AuthenticationTypes authTypes)
        {
            DirectoryEntry de = new DirectoryEntry(path,
                                                   credentials?.UserName,
                                                   credentials?.Password,
                                                   authTypes);

            GlobalDebug.WriteLineIf(GlobalDebug.Info, "SDSUtils", "BuildDirectoryEntry (1): built DE for  " + de.Path);

            return de;
        }

        internal static DirectoryEntry BuildDirectoryEntry(NetCred credentials, AuthenticationTypes authTypes)
        {
            DirectoryEntry de = new DirectoryEntry();

            de.Username = credentials?.UserName;
            de.Password = credentials?.Password;
            de.AuthenticationType = authTypes;

            GlobalDebug.WriteLineIf(GlobalDebug.Info, "SDSUtils", "BuildDirectoryEntry (2): built DE");

            return de;
        }

        internal static void WriteAttribute<T>(string dePath, string attribute, T value, NetCred credentials, AuthenticationTypes authTypes)
        {
            Debug.Assert(attribute != null && attribute.Length > 0);

            // Ideally, we'd just like to set the property in the principal's DirectoryEntry and write
            // the changes to the store.  However, there might be other changes in the DirectoryEntry,
            // and we don't want to write all of them to the store.  So we must make
            // a fresh DirectoryEntry and write using that.

            DirectoryEntry copyOfDe = null;

            try
            {
                copyOfDe = SDSUtils.BuildDirectoryEntry(dePath, credentials, authTypes);

                Debug.Assert(copyOfDe != null);

                // So we don't do a implicit GetInfo() and retrieve every attribute
                copyOfDe.RefreshCache(new string[] { attribute });

                copyOfDe.Properties[attribute].Value = value;
                copyOfDe.CommitChanges();
            }
            catch (System.Runtime.InteropServices.COMException e)
            {
                // ADSI threw an exception trying to write the change
                throw ExceptionHelper.GetExceptionFromCOMException(e);
            }
            finally
            {
                copyOfDe?.Dispose();
            }
        }

        internal static void WriteAttribute(string dePath, string attribute, int value, NetCred credentials, AuthenticationTypes authTypes)
        {
            GlobalDebug.WriteLineIf(
                        GlobalDebug.Info,
                        "SDSUtils",
                        "WriteAttribute: writing {0} to {1} on {2}",
                        value.ToString(CultureInfo.InvariantCulture),
                        attribute,
                        dePath);

            Debug.Assert(attribute != null && attribute.Length > 0);

            // Ideally, we'd just like to set the property in the principal's DirectoryEntry and write
            // the changes to the store.  However, there might be other changes in the DirectoryEntry,
            // and we don't want to write all of them to the store.  So we must make
            // a fresh DirectoryEntry and write using that.

            DirectoryEntry copyOfDe = null;

            try
            {
                copyOfDe = SDSUtils.BuildDirectoryEntry(dePath, credentials, authTypes);

                Debug.Assert(copyOfDe != null);

                // So we don't do a implicit GetInfo() and retrieve every attribute
                copyOfDe.RefreshCache(new string[] { attribute });

                copyOfDe.Properties[attribute].Value = value;
                copyOfDe.CommitChanges();
            }
            catch (System.Runtime.InteropServices.COMException e)
            {
                GlobalDebug.WriteLineIf(
                            GlobalDebug.Error,
                            "SDSUtils",
                            "WriteAttribute: caught exception with message '{0}' writing {1} to {2} on {3}",
                            e.Message,
                            value.ToString(CultureInfo.InvariantCulture),
                            attribute,
                            dePath);

                // ADSI threw an exception trying to write the change
                throw ExceptionHelper.GetExceptionFromCOMException(e);
            }
            finally
            {
                copyOfDe?.Dispose();
            }
        }

        //
        // S.DS (LDAP or WinNT) --> PAPI conversion routines
        //
        internal static void SingleScalarFromDirectoryEntry<T>(dSPropertyCollection properties, string suggestedProperty, Principal p, string propertyName)
        {
            if (properties[suggestedProperty].Count != 0 && properties[suggestedProperty][0] != null)
            {
                // We're intended to handle single-valued scalar properties
                Debug.Assert(properties[suggestedProperty].Count == 1);
                Debug.Assert(properties[suggestedProperty][0] is T);

                p.LoadValueIntoProperty(propertyName, (T)properties[suggestedProperty][0]);
            }
        }

        internal static void MultiScalarFromDirectoryEntry<T>(dSPropertyCollection properties, string suggestedProperty, Principal p, string propertyName)
        {
            dSPropertyValueCollection values = properties[suggestedProperty];

            List<T> list = new List<T>();

            foreach (object value in values)
            {
                Debug.Assert(value is T);

                list.Add((T)value);
            }

            p.LoadValueIntoProperty(propertyName, list);
        }

        internal static bool StatusFromAccountControl(int uacValue, string propertyName)
        {
            bool flag = false;

            switch (propertyName)
            {
                case PropertyNames.AuthenticablePrincipalEnabled:
                    // UF_ACCOUNTDISABLE
                    // Note that the logic is inverted on this one.  We expose "Enabled",
                    // but AD/SAM store it as "Disabled".
                    flag = ((uacValue & 0x2) == 0);
                    break;

                case PropertyNames.AcctInfoSmartcardRequired:
                    // UF_SMARTCARD_REQUIRED
                    flag = ((uacValue & 0x40000) != 0);
                    break;

                case PropertyNames.AcctInfoDelegationPermitted:
                    // UF_NOT_DELEGATED
                    // Note that the logic is inverted on this one.  That's because we expose
                    // "delegation allowed", but AD represents it as the inverse, "delegation NOT allowed"
                    flag = ((uacValue & 0x100000) == 0);
                    break;

                case PropertyNames.PwdInfoPasswordNotRequired:
                    // UF_PASSWD_NOTREQD
                    flag = ((uacValue & 0x0020) != 0);
                    break;

                case PropertyNames.PwdInfoPasswordNeverExpires:
                    // UF_DONT_EXPIRE_PASSWD
                    flag = ((uacValue & 0x10000) != 0);
                    break;

                // This bit doesn't work in userAccountControl
                case PropertyNames.PwdInfoCannotChangePassword:
                    // UF_PASSWD_CANT_CHANGE
                    flag = ((uacValue & 0x0040) != 0);
                    break;

                case PropertyNames.PwdInfoAllowReversiblePasswordEncryption:
                    // UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED
                    flag = ((uacValue & 0x0080) != 0);
                    break;

                default:
                    Debug.Fail("SDSUtils.AccountControlFromDirectoryEntry: Fell off end looking for " + propertyName);
                    flag = false;
                    break;
            }

            return flag;
        }
        internal static void AccountControlFromDirectoryEntry(dSPropertyCollection properties, string suggestedProperty, Principal p, string propertyName, bool testCantChangePassword)
        {
            Debug.Assert(
                (!testCantChangePassword && (string.Equals(suggestedProperty, "userAccountControl", StringComparison.OrdinalIgnoreCase))) ||
                (testCantChangePassword && (string.Equals(suggestedProperty, "UserFlags", StringComparison.OrdinalIgnoreCase)))
                );

            Debug.Assert(!string.Equals(propertyName, PropertyNames.PwdInfoCannotChangePassword, StringComparison.OrdinalIgnoreCase) || testCantChangePassword);

            dSPropertyValueCollection values = properties[suggestedProperty];

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

                int uacValue = (int)values[0];
                bool flag;

                flag = StatusFromAccountControl(uacValue, propertyName);

                p.LoadValueIntoProperty(propertyName, flag);
            }
        }

        //
        // PAPI --> S.DS (LDAP or WinNT) conversion routines
        //

        internal static void MultiStringToDirectoryEntryConverter(Principal p, string propertyName, DirectoryEntry de, string suggestedProperty)
        {
            PrincipalValueCollection<string> trackingList = (PrincipalValueCollection<string>)p.GetValueForProperty(propertyName);

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

            List<string> insertedValues = trackingList.Inserted;
            List<string> removedValues = trackingList.Removed;
            List<Pair<string, string>> changedValues = trackingList.ChangedValues;

            PropertyValueCollection properties = de.Properties[suggestedProperty];

            // We test to make sure the change hasn't already been applied to the PropertyValueCollection,
            // because PushChangesToNative can be called multiple times prior to committing the changes and
            // we want to maintain idempotency
            foreach (string value in removedValues)
            {
                if (value != null && properties.Contains(value))
                    properties.Remove(value);
            }

            foreach (Pair<string, string> changedValue in changedValues)
            {
                // Remove the original value and add in the new value
                Debug.Assert(changedValue.Left != null);    // since it came from the system
                properties.Remove(changedValue.Left);

                if (changedValue.Right != null && !properties.Contains(changedValue.Right))
                    properties.Add(changedValue.Right);
            }

            foreach (string value in insertedValues)
            {
                if (value != null && !properties.Contains(value))
                    properties.Add(value);
            }
        }

        internal const int AD_DefaultUAC = (int)(0x200 | 0X20 | 0x2);  // UF_NORMAL_ACCOUNT  | UF_PASSWD_NOTREQD | UF_ACCOUNTDISABLE
        internal const int AD_DefaultUAC_Machine = (int)(0x1000 | 0X20 | 0x2);  // UF_WORKSTATION_TRUST_ACCOUNT | UF_PASSWD_NOTREQD | UF_ACCOUNTDISABLE
        internal const int SAM_DefaultUAC = (int)(0x200 | 0x1);        // UF_NORMAL_ACCOUNT | UF_SCRIPT

        internal static void AccountControlToDirectoryEntry(
                                        Principal p,
                                        string propertyName,
                                        DirectoryEntry de,
                                        string suggestedProperty,
                                        bool isSAM,
                                        bool isUnpersisted)
        {
            Debug.Assert(
                (!isSAM && (string.Equals(suggestedProperty, "userAccountControl", StringComparison.OrdinalIgnoreCase))) ||
                (isSAM && (string.Equals(suggestedProperty, "UserFlags", StringComparison.OrdinalIgnoreCase)))
                );

            bool flag = (bool)p.GetValueForProperty(propertyName);

            int uacValue = 0;

            // 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 should have already initialized the userAccountControl property
            if (de.Properties[suggestedProperty].Count > 0)
            {
                Debug.Assert(de.Properties[suggestedProperty].Count == 1);

                uacValue = (int)de.Properties[suggestedProperty][0];

                // When we write to userAccountControl, we must OR the new value with the value of msDS-User-Account-Control-Computed.
                // Otherwise we might mistakenly clear computed bits like UF_LOCKOUT.
                if (!isSAM && de.Properties["msDS-User-Account-Control-Computed"].Count > 0)
                {
                    Debug.Assert(de.Properties["msDS-User-Account-Control-Computed"].Count == 1);
                    uacValue |= (int)de.Properties["msDS-User-Account-Control-Computed"][0];
                }
            }
            else
            {
                // We don't have the userAccountControl property, this must be a persisted principal.  Perhaps we don't have access
                // to it.  In that case, we don't want to blindly overwrite whatever other bits might be there.
                Debug.Assert(!p.unpersisted);
                throw new PrincipalOperationException(
                                SR.ADStoreCtxUnableToReadExistingAccountControlFlagsForUpdate);
            }

            uint bitmask;

            // Get the right bitmask for the property
            switch (propertyName)
            {
                case PropertyNames.AuthenticablePrincipalEnabled:
                    if (!isUnpersisted || isSAM)
                    {
                        // UF_ACCOUNTDISABLE
                        // Note that the logic is inverted on this one.  We expose "Enabled",
                        // but AD/SAM store it as "Disabled".
                        bitmask = 0x2;
                        flag = !flag;
                    }
                    else
                    {
                        // We're writing to an unpersisted AD principal
                        Debug.Assert(!isSAM && isUnpersisted);

                        // Nothing to do here.  The ADStoreCtx will take care of enabling the
                        // principal after it's persisted.
                        bitmask = 0;
                    }
                    break;

                case PropertyNames.AcctInfoSmartcardRequired:
                    // UF_SMARTCARD_REQUIRED
                    bitmask = 0x40000;
                    break;

                case PropertyNames.AcctInfoDelegationPermitted:
                    // UF_NOT_DELEGATED
                    // Note that the logic is inverted on this one.  That's because we expose
                    // "delegation allowed", but AD represents it as the inverse, "delegation NOT allowed"
                    bitmask = 0x100000;
                    flag = !flag;
                    break;

                case PropertyNames.PwdInfoPasswordNotRequired:
                    // UF_PASSWD_NOTREQD
                    bitmask = 0x0020;
                    break;

                case PropertyNames.PwdInfoPasswordNeverExpires:
                    // UF_DONT_EXPIRE_PASSWD
                    bitmask = 0x10000;
                    break;

                case PropertyNames.PwdInfoAllowReversiblePasswordEncryption:
                    // UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED
                    bitmask = 0x0080;
                    break;

                // This bit doesn't work in userAccountControl
                case PropertyNames.PwdInfoCannotChangePassword:
                    if (isSAM)
                    {
                        // UF_PASSWD_CANT_CHANGE
                        bitmask = 0x0040;
                        break;
                    }
                    else
                    {
                        Debug.Fail(
                            "SDSUtils.AccountControlToDirectoryEntry: At PwdInfoCannotChangePassword but isSAM==false, path=" + de.Path
                            );

                        goto default;
                    }

                default:
                    Debug.Fail("SDSUtils.AccountControlToDirectoryEntry: Fell off end looking for " + propertyName);
                    bitmask = 0;
                    break;
            }

            // Set/clear the "bitmask" bit in "uacValue", based on the value of "flag"
            if (flag)
                Utils.SetBit(ref uacValue, bitmask);
            else
                Utils.ClearBit(ref uacValue, bitmask);

            de.Properties[suggestedProperty].Value = uacValue;
        }

        internal static DirectorySearcher ConstructSearcher(DirectoryEntry de)
        {
            DirectorySearcher ds = new DirectorySearcher(de);
            ds.ClientTimeout = new TimeSpan(0, 0, 30);
            ds.PageSize = 256;
            return ds;
        }

        internal static bool IsObjectFromGC(string path)
        {
            return path.StartsWith("GC:", StringComparison.OrdinalIgnoreCase);
        }

        internal static string ConstructDnsDomainNameFromDn(string dn)
        {
            // Split the DN into its RDNs
            string[] ncComponents = dn.Split(ADStoreCtx.s_comma);

            StringBuilder sb = new StringBuilder();

            // Reassemble the RDNs (minus the tag) into the DNS domain name
            foreach (string component in ncComponents)
            {
                if ((component.Length > 3) &&
                    string.Equals(component.Substring(0, 3), "DC=", StringComparison.OrdinalIgnoreCase))
                {
                    sb.Append(component, 3, component.Length - 3);
                    sb.Append('.');
                }
            }

            if (sb.Length > 0)
                sb.Remove(sb.Length - 1, 1);

            return sb.ToString();
        }
    }
}