问题
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