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://learn.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://learn.microsoft.com/en-us/windows/win32/adsi/modifying-user-cannot-change-password-ldap-provider
Hopefully someone finds this helpful.