diff --git a/docs/documentation/server_admin/topics/user-federation/ldap.adoc b/docs/documentation/server_admin/topics/user-federation/ldap.adoc index 7efc10d0001..fb12415f9ea 100644 --- a/docs/documentation/server_admin/topics/user-federation/ldap.adoc +++ b/docs/documentation/server_admin/topics/user-federation/ldap.adoc @@ -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: diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPConfig.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPConfig.java index 949d17fbd3a..cb237feb55e 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPConfig.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPConfig.java @@ -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(); diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPUtils.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPUtils.java index 54ede535e9d..3bfb2ac3f8c 100755 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPUtils.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPUtils.java @@ -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) { diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapper.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapper.java index 58d74af60a0..1161d7d8c21 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapper.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapper.java @@ -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) { diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index 26a1b357de9..0656bb51844 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -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 diff --git a/model/storage/src/main/java/org/keycloak/storage/UserStorageProviderModel.java b/model/storage/src/main/java/org/keycloak/storage/UserStorageProviderModel.java index fa32e64cb96..a6dcd844e22 100755 --- a/model/storage/src/main/java/org/keycloak/storage/UserStorageProviderModel.java +++ b/model/storage/src/main/java/org/keycloak/storage/UserStorageProviderModel.java @@ -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; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPProvidersIntegrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPProvidersIntegrationTest.java index 06a8126f9ea..27373071fd4 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPProvidersIntegrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPProvidersIntegrationTest.java @@ -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);