Do not lower-case username and email if users are not imported from LDAP

Closes #43621

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2025-10-23 08:02:33 -03:00 committed by GitHub
parent 2b785425fa
commit 6527b139dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 49 additions and 11 deletions

View File

@ -100,6 +100,12 @@ The `Use Truststore SPI` configuration property is deprecated. It should normal
If you set the *Import Users* option, the LDAP Provider handles importing LDAP users into the {project_name} local database. The first time a user logs in or is returned as part of a user query (e.g. using the search field in the admin console), the LDAP provider imports the LDAP user into the {project_name} database. During authentication, the LDAP password is validated.
By default, {project_name} does not support the username and email attributes with case-sensitive values when storing users to the local database. The value for these attributes will be stored in lower-case in the local database.
However, if the *Import Users* option is disabled, {project_name} will not lower-case the username and email attributes when querying users from LDAP.
This behavior allows you to use case-sensitive usernames and emails when *Import Users* is disabled. Note that this behavior applies only to username and email attributes. Other attributes remain case-sensitive.
It is recommended to not use case-sensitive usernames and emails when using LDAP with {project_name}, as some features in {project_name} may not work correctly with case-sensitive usernames and emails.
If you want to sync all LDAP users into the {project_name} database, configure and enable the *Sync Settings* on the LDAP provider configuration page.
Two types of synchronization exist:

View File

@ -17,6 +17,8 @@
package org.keycloak.storage.ldap;
import static org.keycloak.storage.UserStorageProviderModel.IMPORT_ENABLED;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.models.LDAPConstants;
import org.keycloak.storage.UserStorageProvider;
@ -284,6 +286,10 @@ public class LDAPConfig {
return LDAPConstants.VENDOR_NOVELL_EDIRECTORY.equalsIgnoreCase(getVendor());
}
public boolean isImportEnabled() {
return Boolean.parseBoolean(config.getFirstOrDefault(IMPORT_ENABLED, Boolean.TRUE.toString())) ;
}
@Override
public int hashCode() {
return config.hashCode() * 13 + binaryAttributeNames.hashCode();

View File

@ -186,7 +186,11 @@ public class LDAPUtils {
config.getUsernameLdapAttribute() + ", user DN: " + ldapUser.getDn() + ", attributes from LDAP: " + ldapUser.getAttributes());
}
return Optional.of(ldapUsername).map(String::toLowerCase).orElse(null);
if (config.isImportEnabled()) {
return Optional.of(ldapUsername).map(String::toLowerCase).orElse(null);
}
return ldapUsername;
}
public static void checkUuid(LDAPObject ldapUser, LDAPConfig config) {

View File

@ -295,7 +295,7 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
public String getUsername() {
if (UserModel.USERNAME.equals(userModelAttrName)) {
return ofNullable(ldapUser.getAttributeAsString(ldapAttrName))
.map(String::toLowerCase)
.map(this::toLowerCaseIfImportEnabled)
.orElse(null);
}
return super.getUsername();
@ -305,7 +305,7 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
public String getEmail() {
if (UserModel.EMAIL.equals(userModelAttrName)) {
return ofNullable(ldapUser.getAttributeAsString(ldapAttrName))
.map(String::toLowerCase)
.map(this::toLowerCaseIfImportEnabled)
.orElse(null);
}
return super.getEmail();
@ -347,6 +347,12 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
return true;
}
private String toLowerCaseIfImportEnabled(String value) {
if (getLdapProvider().getModel().isImportEnabled()) {
return value.toLowerCase();
}
return value;
}
};
} else if (isBinaryAttribute) {

View File

@ -529,7 +529,7 @@ searchClientByName=Search client by name
loginTimeout=Login timeout
attributeName=Attribute [Name]
updateError=Could not update the provider {{error}}
importUsersHelp=If true, LDAP users will be imported into the Keycloak database and synced by the configured sync policies.
importUsersHelp=If true, LDAP users will be imported into the local database and synced by the configured sync policies. If import is enabled, the username and email attributes will be stored in the local database using case-insensitivity values, in lower-case. If disabled, those attributes will be treated as case-sensitive and the values will have the same format from their corresponding LDAP entries.
emptyClientProfilesInstructions=There are no profiles, select 'Create client profile' to create a new client profile
policyProvider.js=Define conditions for your permissions using JavaScript. It is one of the rule-based policy types supported by Keycloak, and provides flexibility to write any policy based on the Evaluation API.
idpType.social=Social login

View File

@ -51,12 +51,7 @@ public class UserStorageProviderModel extends CacheableStorageProviderModel {
public boolean isImportEnabled() {
if (importEnabled == null) {
String val = getConfig().getFirst(IMPORT_ENABLED);
if (val == null) {
importEnabled = true;
} else {
importEnabled = Boolean.valueOf(val);
}
importEnabled = Boolean.parseBoolean(getConfig().getFirstOrDefault(IMPORT_ENABLED, Boolean.TRUE.toString()));
}
return importEnabled;
}

View File

@ -458,9 +458,30 @@ public class LDAPProvidersIntegrationTest extends AbstractLDAPTest {
}
@Test
public void testUsernameAndEmailInLowerCaseFromLDAP() {
public void testUsernameAndEmailCaseSensitiveIfImportDisabled() {
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
UserStorageProviderModel ldapModel = ctx.getLdapProvider().getModel();
ldapModel.setImportEnabled(false);
ctx.getRealm().updateComponent(ldapModel);
LDAPObject ldapObject = LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), ctx.getRealm(), "JBrown8", "John", "Brown8", "JBrown8@Email.org", null, "1234");
LDAPTestUtils.updateLDAPPassword(ctx.getLdapProvider(), ldapObject, "Password1");
UserModel model = session.users().searchForUserStream(ctx.getRealm(), Map.of(UserModel.USERNAME, "JBrown8")).findAny().orElse(null);
Assert.assertNotNull(model);
assertEquals("JBrown8", model.getUsername());
assertEquals("JBrown8@Email.org", model.getEmail());
ldapModel.setImportEnabled(true);
ctx.getRealm().updateComponent(ldapModel);
});
}
@Test
public void testUsernameAndEmailCaseInSensitiveIfImportEnabled() {
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
UserStorageProviderModel ldapModel = ctx.getLdapProvider().getModel();
ldapModel.setImportEnabled(true);
ctx.getRealm().updateComponent(ldapModel);
LDAPObject ldapObject = LDAPTestUtils.addLDAPUser(ctx.getLdapProvider(), ctx.getRealm(), "JBrown9", "John", "Brown9", "JBrown9@Email.org", null, "1234");
LDAPTestUtils.updateLDAPPassword(ctx.getLdapProvider(), ldapObject, "Password1");
UserModel model = session.users().searchForUserStream(ctx.getRealm(), Map.of(UserModel.USERNAME, "JBrown9")).findAny().orElse(null);