Password modification time attribute as an operational and read-only attribute

Closes #40270

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2025-06-10 11:28:55 -03:00 committed by GitHub
parent a8a455486d
commit 9412e339a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 47 additions and 14 deletions

View File

@ -1228,20 +1228,16 @@ public class LDAPStorageProvider implements UserStorageProvider,
}
private long getPasswordChangedTime(LDAPObject ldapObject) {
String value = ldapObject.getAttributeAsString(getAttributeName());
String attributeName = getLdapIdentityStore().getPasswordModificationTimeAttributeName();
String value = ldapObject.getAttributeAsString(attributeName);
if (StringUtil.isBlank(value)) {
return -1L;
}
if (LDAPConstants.PWD_LAST_SET.equals(getAttributeName())) {
if (LDAPConstants.PWD_LAST_SET.equals(attributeName)) {
return (Long.parseLong(value) / 10000L) - 11644473600000L;
}
return LDAPUtils.generalizedTimeToDate(value).getTime();
}
private String getAttributeName() {
LDAPConfig config = getLdapIdentityStore().getConfig();
return config.isActiveDirectory() ? LDAPConstants.PWD_LAST_SET : LDAPConstants.PWD_CHANGED_TIME;
}
}

View File

@ -113,6 +113,12 @@ public class LDAPUtils {
.collect(Collectors.toSet());
mandatoryAttrs.add(ldapConfig.getRdnLdapAttribute());
String passwordModifiedTimeAttributeName = ldapStore.getPasswordModificationTimeAttributeName();
String passwordModifiedTime = user.getFirstAttribute(passwordModifiedTimeAttributeName);
if (passwordModifiedTime != null) {
ldapUser.setSingleAttribute(passwordModifiedTimeAttributeName, passwordModifiedTime);
}
ldapUser.executeOnMandatoryAttributesComplete(mandatoryAttrs, ldapObject -> {
LDAPUtils.computeAndSetDn(ldapConfig, ldapObject);
ldapStore.add(ldapObject);

View File

@ -524,6 +524,13 @@ public class LDAPIdentityStore implements IdentityStore {
.map(String::toLowerCase)
.collect(Collectors.toSet());
if (!isCreate) {
// for updates, assume the PWD_CHANGED_TIME attribute is an operational attribute and read-only
// otherwise, updates will fail when trying to modify the attribute
// vendors like AD, support the same type of attribute differently and using a mapper
ldapObject.addReadOnlyAttributeName(LDAPConstants.PWD_CHANGED_TIME);
}
for (Map.Entry<String, Set<String>> attrEntry : ldapObject.getAttributes().entrySet()) {
String attrName = attrEntry.getKey();
Set<String> attrValue = attrEntry.getValue();
@ -602,4 +609,8 @@ public class LDAPIdentityStore implements IdentityStore {
return attr;
}
public String getPasswordModificationTimeAttributeName() {
return getConfig().isActiveDirectory() ? LDAPConstants.PWD_LAST_SET : LDAPConstants.PWD_CHANGED_TIME;
}
}

View File

@ -65,6 +65,9 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapp
public static final Set<String> PASSWORD_UPDATE_MSAD_ERROR_CODES = Set.of("52D");
private final Function<LDAPObject, UserAccountControl> GET_USER_ACCOUNT_CONTROL = ldapUser -> {
if (ldapUser == null) {
return UserAccountControl.empty();
}
String userAccountControl = ldapUser.getAttributeAsString(LDAPConstants.USER_ACCOUNT_CONTROL);
return UserAccountControl.of(userAccountControl);
};

View File

@ -49,9 +49,13 @@ public class UserAccountControl {
private static final UserAccountControl EMPTY = new UserAccountControl(0);
public static UserAccountControl empty() {
return EMPTY;
}
public static UserAccountControl of(String userAccountControl) {
if (userAccountControl == null) {
return EMPTY;
return empty();
}
return new UserAccountControl(Long.parseLong(userAccountControl));
}

View File

@ -138,7 +138,7 @@ public class LDAPTestUtils {
} else if (otherAttrs.containsKey(name)) {
return otherAttrs.getFirst(name);
}
return super.getFirstAttribute(name);
return null;
}
@Override

View File

@ -307,4 +307,8 @@ public class LDAPRule extends ExternalResource {
STARTTLS
}
}
public boolean isEmbeddedServer() {
return ldapTestConfiguration.isStartEmbeddedLdapServer();
}
}

View File

@ -34,6 +34,7 @@ import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.keycloak.admin.client.resource.UserProfileResource;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.federation.kerberos.KerberosFederationProvider;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
@ -90,6 +91,7 @@ public class LDAPAccountRestApiTest extends AbstractLDAPTest {
@Override
protected void afterImportTestRealm() {
boolean isEmbeddedServer = ldapRule.isEmbeddedServer();
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
RealmModel appRealm = ctx.getRealm();
@ -97,10 +99,17 @@ public class LDAPAccountRestApiTest extends AbstractLDAPTest {
// Delete all LDAP users and add some new for testing
LDAPTestUtils.removeAllLDAPUsers(ctx.getLdapProvider(), appRealm);
LDAPObject john = LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
LDAPTestUtils.updateLDAPPassword(ctx.getLdapProvider(), john, "Password1");
john.setSingleAttribute(LDAPConstants.PWD_CHANGED_TIME, "22000101000000Z");
ctx.getLdapProvider().getLdapIdentityStore().update(john);
if (isEmbeddedServer) {
MultivaluedHashMap<String, String> otherAttrs = new MultivaluedHashMap<>();
otherAttrs.putSingle(LDAPConstants.PWD_CHANGED_TIME, "22000101000000Z");
LDAPObject john = LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "johnkeycloak", "John", "Doe", "john@email.org", otherAttrs);
LDAPTestUtils.updateLDAPPassword(ctx.getLdapProvider(), john, "Password1");
} else {
LDAPObject john = LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
LDAPTestUtils.updateLDAPPassword(ctx.getLdapProvider(), john, "Password1");
}
});
}

View File

@ -246,7 +246,7 @@ public class LDAPBinaryAttributesTest extends AbstractLDAPTest {
} else if (UserModel.USERNAME.equals(name)) {
return username;
}
return super.getFirstAttribute(name);
return null;
}
@Override