Find users who cannot change their password

一世执手 提交于 2021-02-19 06:32:29

问题


I am trying to prepare report of users who cannot change their password in AD. AD is installed on Window Server 2012.

Here is the method, which I thought to work but isn't working -

    /// <summary>
    /// Check whether password of user cannot be changed.
    /// </summary>
    /// <param name="user">The DirectoryEntry object of user.</param>
    /// <returns>Return true if password cannot be changed else false.</returns>
    public static bool IsPasswordCannotBeChanged(DirectoryEntry user)
    {
        if (user.Properties.Contains("userAccountControl") &&
            user.Properties["userAccountControl"].Value != null)
        {
            var userFlags = (UserFlags)user.Properties["userAccountControl"].Value;
            return userFlags.Contains(UserFlags.PasswordCannotChange);
        }
        return false;
    }

And here is the enum UserFlags -

[Flags]
public enum UserFlags
{
    // Reference - Chapter 10 (from The .NET Developer's Guide to Directory Services Programming)

    Script = 1,                                     // 0x1
    AccountDisabled = 2,                            // 0x2
    HomeDirectoryRequired = 8,                      // 0x8
    AccountLockedOut = 16,                          // 0x10
    PasswordNotRequired = 32,                       // 0x20
    PasswordCannotChange = 64,                      // 0x40
    EncryptedTextPasswordAllowed = 128,             // 0x80
    TempDuplicateAccount = 256,                     // 0x100
    NormalAccount = 512,                            // 0x200
    InterDomainTrustAccount = 2048,                 // 0x800
    WorkstationTrustAccount = 4096,                 // 0x1000
    ServerTrustAccount = 8192,                      // 0x2000
    PasswordDoesNotExpire = 65536,                  // 0x10000 (Also 66048 )
    MnsLogonAccount = 131072,                       // 0x20000
    SmartCardRequired = 262144,                     // 0x40000
    TrustedForDelegation = 524288,                  // 0x80000
    AccountNotDelegated = 1048576,                  // 0x100000
    UseDesKeyOnly = 2097152,                        // 0x200000
    DontRequirePreauth = 4194304,                   // 0x400000
    PasswordExpired = 8388608,                      // 0x800000 (Applicable only in Window 2000 and Window Server 2003)
    TrustedToAuthenticateForDelegation = 16777216,  // 0x1000000
    NoAuthDataRequired = 33554432                   // 0x2000000
}

Can you share why 64 (for password cannot change), is not returned for user whose password cannot be changed?

Or you have a much better approach to make this work out?

EDIT-

UserFlagExtension code for making things bit fast -

public static class UserFlagExtensions
{
    /// <summary>
    /// Check if flags contains the specific user flag.
    /// </summary>
    /// <param name="haystack">The bunch of flags</param>
    /// <param name="needle">The flag to look for.</param>
    /// <returns>Return true if flag found in flags.</returns>
    public static bool Contains(this UserFlags haystack, UserFlags needle)
    {
        return (haystack & needle) == needle;
    }
}

回答1:


After searching lot and struggling for hours, I was able to formulate working solution.

.Net 2.0 way

Please proceed to link AD .NET - User's Can't Change Password Attribute (Get/Set)

You will need to add reference to ActiveDS for making it to work. Although I hadn't get time to test it. But a lot of places it is supposed to be working. So...

Code snippet from above article- (in case article get removed)

public bool GetCantChangePassword(string userid)
 {
        bool cantChange = false;
        try
        {
            DirectoryEntry entry = new DirectoryEntry(string.Format("LDAP://{0},{1}", "OU=Standard Users,OU=Domain", "DC=domain,DC=org"));
            entry.AuthenticationType = AuthenticationTypes.Secure | AuthenticationTypes.ServerBind;
            DirectorySearcher search = new DirectorySearcher(entry);
            search.Filter = string.Format("(&(objectClass=user)(objectCategory=person)(sAMAccountName={0}))", userid);
            search.SearchScope = SearchScope.Subtree;
            SearchResult results = search.FindOne();
            if (results != null)
            {
                try
                {
                    DirectoryEntry user = results.GetDirectoryEntry();
                    ActiveDirectorySecurity userSecurity = user.ObjectSecurity;
                    SecurityDescriptor sd = (SecurityDescriptor)user.Properties["ntSecurityDescriptor"].Value;
                    AccessControlList oACL = (AccessControlList)sd.DiscretionaryAcl;

                    bool everyoneCantChange = false;
                    bool selfCantChange = false;

                    foreach (ActiveDs.AccessControlEntry ace in oACL)
                    {
                        try
                        {
                            if (ace.ObjectType.ToUpper().Equals("{AB721A53-1E2F-11D0-9819-00AA0040529B}".ToUpper()))
                            {
                                if (ace.Trustee.Equals("Everyone") && (ace.AceType == (int)ADS_ACETYPE_ENUM.ADS_ACETYPE_ACCESS_DENIED_OBJECT))
                                {
                                    everyoneCantChange = true;
                                }
                                if (ace.Trustee.Equals(@"NT AUTHORITY\SELF") && (ace.AceType == (int)ADS_ACETYPE_ENUM.ADS_ACETYPE_ACCESS_DENIED_OBJECT))
                                {
                                    selfCantChange = true;
                                }
                            }
                        }
                        catch (NullReferenceException ex)
                        {
                            //Logger.append(ex.Message);
                        }
                        catch (Exception ex)
                        {
                            Logger.append(ex);
                        }
                    }


                    if (everyoneCantChange || selfCantChange)
                    {
                        cantChange = true;
                    }
                    else
                    {
                        cantChange = false;
                    }

                    user.Close();
                }
                catch (Exception ex)
                {
                    // Log your errors!
                }
            }
            entry.Close();
        }
        catch (Exception ex)
        {
            // Log your errors!
        }
        return cantChange;
    }

.Net 4.0 way

This is how I was able to nail it down. And it was very easy to fix. However, I need to use AuthenticablePrincipal.UserCannotChangePassword Property.

Code snippet I used-

    /// <summary>
    /// Check whether password of user cannot be changed.
    /// </summary>
    /// <param name="user">The DirectoryEntry object of user.</param>
    /// <returns>Return true if password cannot be changed else false.</returns>
    public static bool IsPasswordCannotBeChanged(DirectoryEntry user)
    {
        var isUserCantChangePass = false;

        try
        {
            // 1. Get SamAccountName
            var samAccountName = Convert.ToString(user.Properties["sAMAccountName"].Value);
            if (!string.IsNullOrEmpty(samAccountName))
            {
                // 2. Prepare domain context
                using (var domainContext = new PrincipalContext(ContextType.Domain, _domain, _domainUser, _domainPass))
                {
                    // 3. Find user
                    var userPrincipal = UserPrincipal.FindByIdentity(domainContext, IdentityType.SamAccountName, samAccountName);

                    // 4. Check if user cannot change password
                    using (userPrincipal)
                        if (userPrincipal != null) isUserCantChangePass = userPrincipal.UserCannotChangePassword;
                }
            }

        }
        catch (Exception exc)
        {
            Logger.Write(exc);
        }

        return isUserCantChangePass;
    }



回答2:


Active directory doesn't use all of these flags. Specifically,

  • AccountLockedOut
  • PasswordCannotChange
  • PasswordExpired

Active Directory actually uses different mechanisms to control these account properties, so do not try to read them from userAccountControl! We discuss how to deal with the special cases in the upcoming sections.

-- From The .NET Developer's Guide to Directory Services User Account Management by Ryan Dunn and Joe Kaplan

The idea behind the PasswordCannotChange indicates that the password for the account cannot be change by the account itself, but to do that you actually have to deny this right (under the account Security tab)

Try using the msDS-User-Account-Control-Computed attribute to examine the ADS_UF_PASSWD_CANT_CHANGE flag. Like so:

DirectoryEntry user = ...

const string ATTRIBUTE_NAME= "msDS-User-Account-Control-Computed";
const ADS_UF_PASSWD_CANT_CHANGE = 64; // use enum for more robust code

using (user)
{
    user.RefreshCache(new string[]{ATTRIBUTE_NAME}); 

    int userFlags = (int)user.Properties[ATTRIBUTE_NAME].Value;

    bool userCantChangePassword = (userFlags & ADS_UF_PASSWD_CANT_CHANGE) == ADS_UF_PASSWD_CANT_CHANGE;

...
}



回答3:


In case anyone came here like me looking for how you might do this using .NET Core 3.1, here is the solution I came up with to get and set the PasswordCannotChange bit on the UserAccountControl attribute in AD.

I use the System.DirectoryServices.Protocols library to provide access to the LdapConnection class and the related classes and methods. I also use the System.Security.AccessControl library to work with the Security Descriptor.

Assuming you can successfully connect to the AD server to create the LdapConnection class, the rest should work.

Here is my solution to get:

public bool GetUserCannotChangePassword(string userDistinguishedName){
    using (var ldapConnection = CreateLdapConnection()) //Assuming you've connected using Admin rights
    {
        bool cantChange = false;
        
        //Get RootDomainNamingContext as searchContainer
        var r1 = (SearchResponse)ldapConnection.SendRequest(new SearchRequest("", "(objectClass=*)", SearchScope.Base));
        var searchContainer = response.Entries[0].Attributes["rootdomainnamingcontext"].GetValues(typeof(string))[0]
                                .ToString();
        
        //Set Filter to get specified user
        var filter = $"(&(objectClass=user)(!(objectClass=computer))(distinguishedName={userDistinguishedName}))";
        
        //Get the ntSecurityDescriptor attribute of the user
        var searchRequest = new SearchRequest(searchContainer, filter, SearchScope.Subtree, new[] { "ntSecurityDescriptor" });
        var searchOptions = new SearchOptionsControl(SearchOption.DomainScope);
        searchRequest.Controls.Add(searchOptions);
        var searchResponse = (SearchResponse)ldapConnection.SendRequest(searchRequest);
        var result = searchResponse.Entries.OfType<SearchResultEntry>()
            .SingleOrDefault();
            
        if (result != null)
        {
            //Parse as RawSecurityDescriptor
            RawSecurityDescriptor sd =
                new RawSecurityDescriptor((byte[]) result.Attributes["ntSecurityDescriptor"][0], 0);
            var oACL = sd.DiscretionaryAcl;

            bool everyoneCantChange = false;
            bool selfCantChange = false;

            //Loop through the Access Control Entries that are of ObjectAce type
            foreach (var ace in oACL.OfType<ObjectAce>())
            {
                if (ace?.ObjectAceType.ToString().Equals("AB721A53-1E2F-11D0-9819-00AA0040529B",
                    StringComparison.OrdinalIgnoreCase) == true) //Match on change password ACE (https://docs.microsoft.com/en-us/windows/win32/adsi/modifying-user-cannot-change-password-ldap-provider)
                {
                    if (ace.SecurityIdentifier.Value.Equals("S-1-1-0", StringComparison.OrdinalIgnoreCase) &&
                        ace.AceType == AceType.AccessDeniedObject) //Match on Everyone SecurityIdentifier
                    {
                        everyoneCantChange = true;
                    }

                    if (ace.SecurityIdentifier.Value.Equals("S-1-5-10", StringComparison.OrdinalIgnoreCase) &&
                        ace.AceType == AceType.AccessDeniedObject) //Match on Self SecurityIdentifier
                    {
                        selfCantChange = true;
                    }
                }
            }


            if (everyoneCantChange || selfCantChange)
            {
                cantChange = true;
            }
        }

        return cantChange;
    }
}

Here is my solution for set:

public bool SetUserCannotChangePassword(string userDistinguishedName, bool userCannotChangePassword)
{
    using (var ldapConnection = CreateLdapConnection()) //Assuming you've connected using Admin rights
    {
        bool success = true;
        try
        {
            //Get RootDomainNamingContext as searchContainer
            var r1 = (SearchResponse)ldapConnection.SendRequest(new SearchRequest("", "(objectClass=*)", SearchScope.Base));
            var searchContainer = response.Entries[0].Attributes["rootdomainnamingcontext"].GetValues(typeof(string))[0]
                                    .ToString();
            
            //Set Filter to get specified user
            var filter = $"(&(objectClass=user)(!(objectClass=computer))(distinguishedName={userDistinguishedName}))";
            
            //Get the ntSecurityDescriptor attribute of the user
            var searchRequest = new SearchRequest(searchContainer, filter, SearchScope.Subtree, new[] { "ntSecurityDescriptor", "distinguishedName" });
            var searchOptions = new SearchOptionsControl(SearchOption.DomainScope);
            searchRequest.Controls.Add(searchOptions);
            var searchResponse = (SearchResponse)ldapConnection.SendRequest(searchRequest);
            var result = searchResponse.Entries.OfType<SearchResultEntry>()
                .SingleOrDefault();

            if (result != null)
            {
                try
                {
                    RawSecurityDescriptor sd =
                        new RawSecurityDescriptor((byte[]) result.Attributes["ntSecurityDescriptor"][0], 0);
                    var dn = result.Attributes["distinguishedName"][0];
                    var oACL = sd.DiscretionaryAcl;

                    int? everyoneCantChangeIndex = null;
                    ObjectAce everyoneAce = null;
                    int? selfCantChangeIndex = null;
                    ObjectAce selfAce = null;

                    for (var i = 0; i < oACL.Count; i++)
                    {
                        var oAce = oACL[i] as ObjectAce;
                        if (oAce?.ObjectAceType.ToString().Equals("AB721A53-1E2F-11D0-9819-00AA0040529B",
                            StringComparison.OrdinalIgnoreCase) == true)
                        {
                            if (oAce.SecurityIdentifier.Value.Equals("S-1-1-0",
                                StringComparison.OrdinalIgnoreCase))
                            {
                                everyoneCantChangeIndex = i;
                                everyoneAce = oAce;
                            }

                            if (oAce.SecurityIdentifier.Value.Equals("S-1-5-10",
                                    StringComparison.OrdinalIgnoreCase) &&
                                oAce.AceType == AceType.AccessDeniedObject)
                            {
                                selfCantChangeIndex = i;
                                selfAce = oAce;
                            }
                        }
                    }

                    if (everyoneCantChangeIndex.HasValue)
                    {
                        oACL.RemoveAce(everyoneCantChangeIndex.Value);
                    }

                    if (selfCantChangeIndex.HasValue)
                    {
                        if (everyoneCantChangeIndex.HasValue &&
                            everyoneCantChangeIndex.Value < selfCantChangeIndex.Value)
                        {
                            selfCantChangeIndex--; //Adjust index to ensure removing correct ACE
                        }

                        oACL.RemoveAce(selfCantChangeIndex.Value);
                    }

                    if (userCannotChangePassword)
                    {
                        oACL.InsertAce(everyoneCantChangeIndex ?? oACL.Count,
                            new ObjectAce(AceFlags.None, AceQualifier.AccessDenied,
                                everyoneAce?.AccessMask ?? 256,
                                everyoneAce?.SecurityIdentifier ??
                                new SecurityIdentifier(WellKnownSidType.WorldSid, null),
                                ObjectAceFlags.ObjectAceTypePresent,
                                everyoneAce?.ObjectAceType ??
                                new Guid("{AB721A53-1E2F-11D0-9819-00AA0040529B}"),
                                everyoneAce?.InheritedObjectAceType ?? Guid.Empty,
                                everyoneAce?.IsCallback ?? false, everyoneAce?.GetOpaque()));
                                
                        oACL.InsertAce(selfCantChangeIndex ?? oACL.Count,
                            new ObjectAce(AceFlags.None, AceQualifier.AccessDenied, selfAce?.AccessMask ?? 256,
                                selfAce?.SecurityIdentifier ??
                                new SecurityIdentifier(WellKnownSidType.SelfSid, null),
                                ObjectAceFlags.ObjectAceTypePresent,
                                selfAce?.ObjectAceType ?? new Guid("{AB721A53-1E2F-11D0-9819-00AA0040529B}"),
                                selfAce?.InheritedObjectAceType ?? Guid.Empty, selfAce?.IsCallback ?? false,
                                selfAce?.GetOpaque()));
                    }
                    else
                    {
                        oACL.InsertAce(everyoneCantChangeIndex ?? oACL.Count,
                            new ObjectAce(AceFlags.None, AceQualifier.AccessAllowed,
                                everyoneAce?.AccessMask ?? 256,
                                everyoneAce?.SecurityIdentifier ??
                                new SecurityIdentifier(WellKnownSidType.WorldSid, null),
                                ObjectAceFlags.ObjectAceTypePresent,
                                everyoneAce?.ObjectAceType ??
                                new Guid("{AB721A53-1E2F-11D0-9819-00AA0040529B}"),
                                everyoneAce?.InheritedObjectAceType ?? Guid.Empty,
                                everyoneAce?.IsCallback ?? false, everyoneAce?.GetOpaque()));
                    }

                    var modification = new DirectoryAttributeModification
                    {
                        Operation = DirectoryAttributeOperation.Replace,
                        Name = "ntSecurityDescriptor"
                    };

                    sd.DiscretionaryAcl = OrderRawAcl(oACL);

                    var ba = new byte[sd.BinaryLength];
                    sd.GetBinaryForm(ba, 0);
                    modification.Add(ba);

                    var modifyRequest = new ModifyRequest(dn.ToString(), modification);

                    var modifyResponse = ldapConnection.SendRequest(modifyRequest);
                    if (modifyResponse.ResultCode != ResultCode.Success)
                    {
                        success = false;
                    }
                }
                catch (Exception ex)
                {
                    success = false;
                }
            }
        }
        catch (Exception ex)
        {
            success = false;
        }

        return success;
    }
}

private RawAcl OrderRawAcl(RawAcl oAcl)
{
    // Thanks to this post for this awesome method (https://stackoverflow.com/questions/8126827/how-do-you-programmatically-fix-a-non-canonical-acl)
    
    // A canonical ACL must have ACES sorted according to the following order:
    //   1. Access-denied on the object
    //   2. Access-denied on a child or property
    //   3. Access-allowed on the object
    //   4. Access-allowed on a child or property
    //   5. All inherited ACEs 
    List<GenericAce> implicitDenyDacl = new List<GenericAce>();
    List<GenericAce> implicitDenyObjectDacl = new List<GenericAce>();
    List<GenericAce> inheritedDacl = new List<GenericAce>();
    List<GenericAce> implicitAllowDacl = new List<GenericAce>();
    List<GenericAce> implicitAllowObjectDacl = new List<GenericAce>();
    foreach (var ace in oAcl)
    {
        if ((ace.AceFlags & AceFlags.Inherited) == AceFlags.Inherited)
        {
            inheritedDacl.Add(ace);
        }
        else
        {
            switch (ace.AceType)
            {
                case AceType.AccessAllowed:
                    implicitAllowDacl.Add(ace);
                    break;

                case AceType.AccessDenied:
                    implicitDenyDacl.Add(ace);
                    break;

                case AceType.AccessAllowedObject:
                    implicitAllowObjectDacl.Add(ace);
                    break;

                case AceType.AccessDeniedObject:
                    implicitDenyObjectDacl.Add(ace);
                    break;
            }
        }
    }

    Int32 aceIndex = 0;
    RawAcl newDacl = new RawAcl(oAcl.Revision, oAcl.Count);
    implicitDenyDacl.ForEach(x => newDacl.InsertAce(aceIndex++, x));
    implicitDenyObjectDacl.ForEach(x => newDacl.InsertAce(aceIndex++, x));
    implicitAllowDacl.ForEach(x => newDacl.InsertAce(aceIndex++, x));
    implicitAllowObjectDacl.ForEach(x => newDacl.InsertAce(aceIndex++, x));
    inheritedDacl.ForEach(x => newDacl.InsertAce(aceIndex++, x));

    if (aceIndex != oAcl.Count)
    {
        throw new Exception("Reordering Access Control List unsuccessful. The number of items in the reordered list does not match the number of items submitted list.");
    }
    return newDacl;
}

This basically follows the steps detailed in this documentation: https://docs.microsoft.com/en-us/windows/win32/adsi/modifying-user-cannot-change-password-ldap-provider

Hopefully someone finds this helpful.



来源:https://stackoverflow.com/questions/29812872/find-users-who-cannot-change-their-password

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!