diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilder.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilder.java index 5295a71762..9ee7880832 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilder.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilder.java @@ -68,6 +68,8 @@ public class IckleQueryMapModelCriteriaBuilder parameters) { + for (int i = 0; i < values.length; i++) { + if (values[i] instanceof String) { + values[i] = KeycloakModelUtils.toLowerCaseSafe((String) values[i]); + } + } + + return produceWhereClause(modelFieldName, op == ModelCriteriaBuilder.Operator.ILIKE ? ModelCriteriaBuilder.Operator.LIKE : op, values, parameters); + } } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/Constants.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/Constants.java index 20cbd3b4df..2e9984af37 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/Constants.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/Constants.java @@ -35,7 +35,7 @@ public interface Constants { public static final Integer CURRENT_SCHEMA_VERSION_ROOT_AUTH_SESSION = 1; public static final Integer CURRENT_SCHEMA_VERSION_SINGLE_USE_OBJECT = 1; public static final Integer CURRENT_SCHEMA_VERSION_USER_LOGIN_FAILURE = 1; - public static final Integer CURRENT_SCHEMA_VERSION_USER = 1; + public static final Integer CURRENT_SCHEMA_VERSION_USER = 2; public static final Integer CURRENT_SCHEMA_VERSION_USER_CONSENT = 1; public static final Integer CURRENT_SCHEMA_VERSION_USER_FEDERATED_IDENTITY = 1; public static final Integer CURRENT_SCHEMA_VERSION_USER_SESSION = 1; diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaPredicateFunction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaPredicateFunction.java index a9aaccda59..f938d8a02d 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaPredicateFunction.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaPredicateFunction.java @@ -17,8 +17,6 @@ package org.keycloak.models.map.storage.jpa; -import org.keycloak.models.map.storage.jpa.JpaSubqueryProvider; - import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/migration/JpaUserMigration.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/migration/JpaUserMigration.java index 7de5356b26..2f58e16f1f 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/migration/JpaUserMigration.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/migration/JpaUserMigration.java @@ -16,10 +16,12 @@ */ package org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.Arrays; import java.util.List; import java.util.function.Function; +import org.keycloak.models.utils.KeycloakModelUtils; /** * Migration functions for users. @@ -29,6 +31,13 @@ import java.util.function.Function; public class JpaUserMigration { public static final List> MIGRATORS = Arrays.asList( - o -> o // no migration yet + o -> o, + JpaUserMigration::migrateTreeFrom1To2 ); + + // adds lower-case variant of username into json + private static ObjectNode migrateTreeFrom1To2(ObjectNode node) { + JsonNode usernameNode = node.path("fUsername"); + return node.put("usernameLowerCase", KeycloakModelUtils.toLowerCaseSafe(usernameNode.asText())); + } } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/JpaUserModelCriteriaBuilder.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/JpaUserModelCriteriaBuilder.java index b90ec56d36..d33a9068fb 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/JpaUserModelCriteriaBuilder.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/JpaUserModelCriteriaBuilder.java @@ -54,8 +54,31 @@ public class JpaUserModelCriteriaBuilder extends JpaModelCriteriaBuilder modelField, Operator op, Object... value) { switch(op) { case EQ: - if (modelField == UserModel.SearchableFields.REALM_ID || - modelField == UserModel.SearchableFields.USERNAME || + if (modelField == UserModel.SearchableFields.USERNAME_CASE_INSENSITIVE) { + + validateValue(value, modelField, op, String.class); + + return new JpaUserModelCriteriaBuilder((cb, query, root) -> + cb.or( + cb.and( + cb.equal(root.get("usernameLowerCase"), value[0].toString().toLowerCase()), + cb.ge(root.get("entityVersion"), 2) + ), + cb.and( + cb.equal(root.get("username"), value[0].toString().toLowerCase()), + cb.le(root.get("entityVersion"), 1) + ) + ) + ); + + } else if (modelField == UserModel.SearchableFields.USERNAME) { + validateValue(value, modelField, op, String.class); + + return new JpaUserModelCriteriaBuilder((cb, query, root) -> + cb.equal(root.get("username"), value[0]) + ); + + } else if (modelField == UserModel.SearchableFields.REALM_ID || modelField == UserModel.SearchableFields.EMAIL || modelField == UserModel.SearchableFields.FEDERATION_LINK) { @@ -152,14 +175,43 @@ public class JpaUserModelCriteriaBuilder extends JpaModelCriteriaBuilder cb.like(cb.lower(root.get(modelField.getName())), value[0].toString().toLowerCase())); + + } else if (modelField == UserModel.SearchableFields.USERNAME_CASE_INSENSITIVE) { + + validateValue(value, modelField, op, String.class); + + return new JpaUserModelCriteriaBuilder((cb, query, root) -> + cb.or( + cb.and( + cb.like(root.get("usernameLowerCase"), value[0].toString().toLowerCase()), + cb.ge(root.get("entityVersion"), 2) + ), + cb.and( + cb.like(root.get("username"), value[0].toString().toLowerCase()), + cb.le(root.get("entityVersion"), 1) + ) + ) + ); + + } else { + throw new CriterionNotSupportedException(modelField, op); + } + case LIKE: + if (modelField == UserModel.SearchableFields.USERNAME) { + + validateValue(value, modelField, op, String.class); + + return new JpaUserModelCriteriaBuilder((cb, query, root) -> + cb.like(root.get("username"), value[0].toString()) + ); + } else { throw new CriterionNotSupportedException(modelField, op); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserEntity.java index 59d425121e..ca8292e56c 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserEntity.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserEntity.java @@ -96,6 +96,10 @@ public class JpaUserEntity extends MapUserEntity.AbstractUserEntity implements J @Basic(fetch = FetchType.LAZY) private String username; + @Column(insertable = false, updatable = false) + @Basic(fetch = FetchType.LAZY) + private String usernameLowerCase; + @Column(insertable = false, updatable = false) @Basic(fetch = FetchType.LAZY) private String firstName; @@ -237,6 +241,7 @@ public class JpaUserEntity extends MapUserEntity.AbstractUserEntity implements J @Override public void setUsername(String username) { this.metadata.setUsername(username); + this.metadata.setUsernameLowerCase(username); } @Override diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserMetadata.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserMetadata.java index 5ac3aa81fe..01d962dca8 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserMetadata.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserMetadata.java @@ -20,6 +20,7 @@ import java.io.Serializable; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.user.MapUserEntityImpl; +import org.keycloak.models.utils.KeycloakModelUtils; /** * Class that contains all the user metadata that is written as JSON into the database. @@ -37,6 +38,7 @@ public class JpaUserMetadata extends MapUserEntityImpl implements Serializable { } private Integer entityVersion; + private String usernameLowerCase; public Integer getEntityVersion() { return entityVersion; @@ -45,4 +47,8 @@ public class JpaUserMetadata extends MapUserEntityImpl implements Serializable { public void setEntityVersion(Integer entityVersion) { this.entityVersion = entityVersion; } + + public void setUsernameLowerCase(String username) { + this.usernameLowerCase = KeycloakModelUtils.toLowerCaseSafe(username); + } } diff --git a/model/map-jpa/src/main/resources/META-INF/jpa-users-changelog.xml b/model/map-jpa/src/main/resources/META-INF/jpa-users-changelog.xml index 6ecbc50eba..10a975cf2a 100644 --- a/model/map-jpa/src/main/resources/META-INF/jpa-users-changelog.xml +++ b/model/map-jpa/src/main/resources/META-INF/jpa-users-changelog.xml @@ -19,4 +19,5 @@ limitations under the License. + diff --git a/model/map-jpa/src/main/resources/META-INF/users/jpa-users-changelog-2.xml b/model/map-jpa/src/main/resources/META-INF/users/jpa-users-changelog-2.xml new file mode 100644 index 0000000000..294be5bde6 --- /dev/null +++ b/model/map-jpa/src/main/resources/META-INF/users/jpa-users-changelog-2.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java index ee288f2e14..a80f10c54e 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java @@ -24,7 +24,6 @@ import org.keycloak.authorization.model.Scope; import org.keycloak.events.Event; import org.keycloak.events.admin.AdminEvent; import org.keycloak.models.ActionTokenValueModel; -import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientScopeModel; import org.keycloak.models.GroupModel; @@ -51,6 +50,7 @@ import org.keycloak.models.map.role.MapRoleEntity; import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity; import org.keycloak.models.map.storage.QueryParameters; import org.keycloak.models.map.user.MapUserConsentEntity; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.storage.SearchableModelField; import java.util.Comparator; @@ -59,7 +59,6 @@ import java.util.Map; import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder.UpdatePredicatesFunc; import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; import org.keycloak.models.map.user.MapUserEntity; -import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity; import org.keycloak.models.map.userSession.MapUserSessionEntity; import org.keycloak.sessions.RootAuthenticationSessionModel; import org.keycloak.storage.StorageId; @@ -134,6 +133,7 @@ public class MapFieldPredicates { put(USER_PREDICATES, UserModel.SearchableFields.REALM_ID, MapUserEntity::getRealmId); put(USER_PREDICATES, UserModel.SearchableFields.USERNAME, MapUserEntity::getUsername); + put(USER_PREDICATES, UserModel.SearchableFields.USERNAME_CASE_INSENSITIVE, MapFieldPredicates::usernameCaseInsensitive); put(USER_PREDICATES, UserModel.SearchableFields.FIRST_NAME, MapUserEntity::getFirstName); put(USER_PREDICATES, UserModel.SearchableFields.LAST_NAME, MapUserEntity::getLastName); put(USER_PREDICATES, UserModel.SearchableFields.EMAIL, MapUserEntity::getEmail); @@ -306,6 +306,18 @@ public class MapFieldPredicates { return mcb.fieldCompare(Boolean.TRUE::equals, getter); } + private static MapModelCriteriaBuilder usernameCaseInsensitive(MapModelCriteriaBuilder mcb, Operator op, Object[] values) { + for (int i = 0; i < values.length; i++) { + if (values[i] instanceof String) { + values[i] = KeycloakModelUtils.toLowerCaseSafe((String) values[i]); + } + } + + Predicate valueComparator = CriteriaOperator.predicateFor(op, values); + Function getter = ue -> valueComparator.test(KeycloakModelUtils.toLowerCaseSafe(ue.getUsername())); + return mcb.fieldCompare(Boolean.TRUE::equals, getter); +} + private static MapModelCriteriaBuilder getUserConsentClientFederationLink(MapModelCriteriaBuilder mcb, Operator op, Object[] values) { String providerId = ensureEqSingleValue(UserModel.SearchableFields.CONSENT_CLIENT_FEDERATION_LINK, "provider_id", op, values); String providerIdS = new StorageId((String) providerId, "").getId(); diff --git a/model/map/src/main/java/org/keycloak/models/map/user/MapUserAdapter.java b/model/map/src/main/java/org/keycloak/models/map/user/MapUserAdapter.java index ae797199c5..1e1e7e58da 100644 --- a/model/map/src/main/java/org/keycloak/models/map/user/MapUserAdapter.java +++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserAdapter.java @@ -54,7 +54,6 @@ public abstract class MapUserAdapter extends AbstractUserModel { @Override public void setUsername(String username) { - username = KeycloakModelUtils.toLowerCaseSafe(username); // Do not continue if current username of entity is the requested username if (username != null && username.equals(entity.getUsername())) return; diff --git a/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java b/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java index 0f5d35d4e1..50f4cafc6d 100644 --- a/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java @@ -80,6 +80,8 @@ import static org.keycloak.models.map.common.AbstractMapProviderFactory.MapProvi import static org.keycloak.models.map.storage.QueryParameters.Order.ASCENDING; import static org.keycloak.models.map.storage.QueryParameters.withCriteria; import static org.keycloak.models.map.storage.criteria.DefaultModelCriteria.criteria; +import static org.keycloak.models.map.user.MapUserProviderFactory.REALM_ATTR_USERNAME_CASE_SENSITIVE; +import static org.keycloak.models.map.user.MapUserProviderFactory.REALM_ATTR_USERNAME_CASE_SENSITIVE_DEFAULT; public class MapUserProvider implements UserProvider.Streams { @@ -93,6 +95,10 @@ public class MapUserProvider implements UserProvider.Streams { session.getTransactionManager().enlist(tx); } + private Boolean getUsernameCaseSensitiveAttribute(RealmModel realm) { + return realm.getAttribute(REALM_ATTR_USERNAME_CASE_SENSITIVE, REALM_ATTR_USERNAME_CASE_SENSITIVE_DEFAULT); + } + private Function entityToAdapterFunc(RealmModel realm) { // Clone entity before returning back, to avoid giving away a reference to the live object to the caller return origEntity -> new MapUserAdapter(session, realm, origEntity) { @@ -330,12 +336,14 @@ public class MapUserProvider implements UserProvider.Streams { LOG.tracef("addUser(%s, %s, %s, %s, %s)%s", realm, id, username, addDefaultRoles, addDefaultRequiredActions, getShortStackTrace()); DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) - .compare(SearchableFields.USERNAME, Operator.EQ, username); - + .compare(getUsernameCaseSensitiveAttribute(realm) ? + SearchableFields.USERNAME : + SearchableFields.USERNAME_CASE_INSENSITIVE, Operator.EQ, username); + if (tx.getCount(withCriteria(mcb)) > 0) { throw new ModelDuplicateException("User with username '" + username + "' in realm " + realm.getName() + " already exists" ); } - + if (id != null && tx.read(id) != null) { throw new ModelDuplicateException("User exists: " + id); } @@ -344,7 +352,7 @@ public class MapUserProvider implements UserProvider.Streams { entity.setId(id); entity.setRealmId(realm.getId()); entity.setEmailConstraint(KeycloakModelUtils.generateId()); - entity.setUsername(username.toLowerCase()); + entity.setUsername(username); entity.setCreatedTimestamp(Time.currentTimeMillis()); entity = tx.create(entity); @@ -488,11 +496,19 @@ public class MapUserProvider implements UserProvider.Streams { LOG.tracef("getUserByUsername(%s, %s)%s", realm, username, getShortStackTrace()); DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) - .compare(SearchableFields.USERNAME, Operator.ILIKE, username); + .compare(getUsernameCaseSensitiveAttribute(realm) ? + SearchableFields.USERNAME : + SearchableFields.USERNAME_CASE_INSENSITIVE, Operator.EQ, username); - try (Stream s = tx.read(withCriteria(mcb))) { - return s.findFirst() - .map(entityToAdapterFunc(realm)).orElse(null); + // there is orderBy used to always return the same user in case multiple users are returned from the store + try (Stream s = tx.read(withCriteria(mcb).orderBy(SearchableFields.USERNAME, ASCENDING))) { + List users = s.collect(Collectors.toList()); + if (users.isEmpty()) return null; + if (users.size() != 1) { + LOG.warnf("There are colliding usernames for users with usernames and ids: %s", + users.stream().collect(Collectors.toMap(MapUserEntity::getUsername, MapUserEntity::getId))); + } + return entityToAdapterFunc(realm).apply(users.get(0)); } } @@ -503,11 +519,9 @@ public class MapUserProvider implements UserProvider.Streams { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.EMAIL, Operator.EQ, email); - List usersWithEmail = tx.read(withCriteria(mcb)) - .filter(userEntity -> Objects.equals(userEntity.getEmail(), email)) - .collect(Collectors.toList()); + List usersWithEmail = tx.read(withCriteria(mcb)).collect(Collectors.toList()); + if (usersWithEmail.isEmpty()) return null; - if (usersWithEmail.size() > 1) { // Realm settings have been changed from allowing duplicate emails to not allowing them // but duplicates haven't been removed. @@ -515,7 +529,7 @@ public class MapUserProvider implements UserProvider.Streams { } MapUserEntity userEntity = usersWithEmail.get(0); - + if (!realm.isDuplicateEmailsAllowed()) { if (userEntity.getEmail() != null && !userEntity.getEmail().equals(userEntity.getEmailConstraint())) { // Realm settings have been changed from allowing duplicate emails to not allowing them. @@ -523,7 +537,7 @@ public class MapUserProvider implements UserProvider.Streams { userEntity.setEmailConstraint(userEntity.getEmail()); } } - + return entityToAdapterFunc(realm).apply(userEntity); } @@ -573,16 +587,18 @@ public class MapUserProvider implements UserProvider.Streams { DefaultModelCriteria searchCriteria = null; for (String stringToSearch : value.split("\\s+")) { if (searchCriteria == null) { - searchCriteria = addSearchToModelCriteria(stringToSearch, mcb); + searchCriteria = addSearchToModelCriteria(realm, stringToSearch, mcb); } else { - searchCriteria = mcb.and(searchCriteria, addSearchToModelCriteria(stringToSearch, mcb)); + searchCriteria = mcb.and(searchCriteria, addSearchToModelCriteria(realm, stringToSearch, mcb)); } } criteria = mcb.and(criteria, searchCriteria); break; case USERNAME: - criteria = criteria.compare(SearchableFields.USERNAME, Operator.ILIKE, searchedString); + criteria = getUsernameCaseSensitiveAttribute(realm) ? + criteria.compare(SearchableFields.USERNAME, Operator.LIKE, searchedString) : + criteria.compare(SearchableFields.USERNAME_CASE_INSENSITIVE, Operator.ILIKE, searchedString); break; case FIRST_NAME: criteria = criteria.compare(SearchableFields.FIRST_NAME, Operator.ILIKE, searchedString); @@ -682,7 +698,7 @@ public class MapUserProvider implements UserProvider.Streams { @Override public UserModel addUser(RealmModel realm, String username) { - return addUser(realm, null, username.toLowerCase(), true, true); + return addUser(realm, null, username, true, true); } @Override @@ -747,7 +763,7 @@ public class MapUserProvider implements UserProvider.Streams { return r; } - private DefaultModelCriteria addSearchToModelCriteria(String value, + private DefaultModelCriteria addSearchToModelCriteria(RealmModel realm, String value, DefaultModelCriteria mcb) { if (value.length() >= 2 && value.charAt(0) == '"' && value.charAt(value.length() - 1) == '"') { @@ -767,7 +783,9 @@ public class MapUserProvider implements UserProvider.Streams { } return mcb.or( - mcb.compare(SearchableFields.USERNAME, Operator.ILIKE, value), + getUsernameCaseSensitiveAttribute(realm) ? + mcb.compare(SearchableFields.USERNAME, Operator.LIKE, value) : + mcb.compare(SearchableFields.USERNAME_CASE_INSENSITIVE, Operator.ILIKE, value), mcb.compare(SearchableFields.EMAIL, Operator.ILIKE, value), mcb.compare(SearchableFields.FIRST_NAME, Operator.ILIKE, value), mcb.compare(SearchableFields.LAST_NAME, Operator.ILIKE, value)); diff --git a/model/map/src/main/java/org/keycloak/models/map/user/MapUserProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/user/MapUserProviderFactory.java index 1fac8b044c..0c8ddcd06b 100644 --- a/model/map/src/main/java/org/keycloak/models/map/user/MapUserProviderFactory.java +++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserProviderFactory.java @@ -40,6 +40,9 @@ import static org.keycloak.models.map.common.AbstractMapProviderFactory.MapProvi */ public class MapUserProviderFactory extends AbstractMapProviderFactory implements UserProviderFactory, InvalidationHandler { + public static final String REALM_ATTR_USERNAME_CASE_SENSITIVE = "keycloak.username-search.case-sensitive"; + public static final Boolean REALM_ATTR_USERNAME_CASE_SENSITIVE_DEFAULT = Boolean.FALSE; + public MapUserProviderFactory() { super(UserModel.class, MapUserProvider.class); } diff --git a/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultAttributes.java b/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultAttributes.java index e9e6a89264..93eec2242b 100644 --- a/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultAttributes.java +++ b/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultAttributes.java @@ -271,10 +271,6 @@ public class DefaultAttributes extends HashMap> implements values = (List) value; } - if (key.equals(UserModel.USERNAME)) { - values = Collections.singletonList(values.get(0).toLowerCase()); - } - newAttributes.put(key, Collections.unmodifiableList(values)); } } diff --git a/server-spi/src/main/java/org/keycloak/models/UserModel.java b/server-spi/src/main/java/org/keycloak/models/UserModel.java index 0669ccfdc2..93846de0ce 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserModel.java +++ b/server-spi/src/main/java/org/keycloak/models/UserModel.java @@ -53,7 +53,6 @@ public interface UserModel extends RoleMapperModel { public static class SearchableFields { public static final SearchableModelField ID = new SearchableModelField<>("id", String.class); public static final SearchableModelField REALM_ID = new SearchableModelField<>("realmId", String.class); - public static final SearchableModelField USERNAME = new SearchableModelField<>("username", String.class); public static final SearchableModelField FIRST_NAME = new SearchableModelField<>("firstName", String.class); public static final SearchableModelField LAST_NAME = new SearchableModelField<>("lastName", String.class); public static final SearchableModelField EMAIL = new SearchableModelField<>("email", String.class); @@ -61,6 +60,15 @@ public interface UserModel extends RoleMapperModel { public static final SearchableModelField EMAIL_VERIFIED = new SearchableModelField<>("emailVerified", Boolean.class); public static final SearchableModelField FEDERATION_LINK = new SearchableModelField<>("federationLink", String.class); + /** + * Search for user's username in case sensitive mode. + */ + public static final SearchableModelField USERNAME = new SearchableModelField<>("username", String.class); + /** + * Search for user's username in case insensitive mode. + */ + public static final SearchableModelField USERNAME_CASE_INSENSITIVE = new SearchableModelField<>("usernameCaseInsensitive", String.class); + /** * This field can only searched either for users coming from an IDP, then the operand is (idp_alias), * or as user coming from a particular IDP with given username there, then the operand is a pair (idp_alias, idp_user_id). diff --git a/server-spi/src/main/java/org/keycloak/storage/user/UserLookupProvider.java b/server-spi/src/main/java/org/keycloak/storage/user/UserLookupProvider.java index f80787ddaa..2fba45cc65 100644 --- a/server-spi/src/main/java/org/keycloak/storage/user/UserLookupProvider.java +++ b/server-spi/src/main/java/org/keycloak/storage/user/UserLookupProvider.java @@ -52,11 +52,15 @@ public interface UserLookupProvider { UserModel getUserById(String id, RealmModel realm); /** + * Exact search for a user by its username. * Returns a user with the given username belonging to the realm * - * @param username case insensitive username (case-sensitivity is controlled by storage) + * @param username (case-sensitivity is controlled by storage) * @param realm the realm model * @return found user model, or {@code null} if no such user exists + * @throws org.keycloak.models.ModelDuplicateException when searched with case + * insensitive mode and there are more users with username which differs only + * by case */ default UserModel getUserByUsername(RealmModel realm, String username) { return getUserByUsername(username, realm); @@ -75,7 +79,7 @@ public interface UserLookupProvider { /** * Returns a user with the given email belonging to the realm * - * @param email case insensitive email address (case-sensitivity is controlled by storage) + * @param email email address * @param realm the realm model * @return found user model, or {@code null} if no such user exists * diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java index 0fa8c423d7..1784d88204 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java @@ -325,6 +325,8 @@ public class AssertEvents implements TestRule { description.appendText("contains scope in any order"); } }); + } else if (key.equals(Details.USERNAME) && value != null) { + return detail(key, Matchers.equalToIgnoringCase(value)); } else { return detail(key, CoreMatchers.equalTo(value)); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java index 0eab506922..bf590f7c9b 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java @@ -17,6 +17,7 @@ package org.keycloak.testsuite.account; import com.fasterxml.jackson.core.type.TypeReference; +import org.hamcrest.Matchers; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; @@ -393,7 +394,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { user.setUsername("updatedUsername"); user = updateAndGet(user); - assertEquals("updatedusername", user.getUsername()); + assertThat("updatedusername", Matchers.equalToIgnoringCase(user.getUsername())); realmRep.setEditUsernameAllowed(false); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SAMLServletAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SAMLServletAdapterTest.java index 7d6781f32e..361700c5eb 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SAMLServletAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SAMLServletAdapterTest.java @@ -145,6 +145,7 @@ import org.keycloak.testsuite.auth.page.login.Login; import org.keycloak.testsuite.auth.page.login.SAMLIDPInitiatedLogin; import org.keycloak.testsuite.auth.page.login.SAMLPostLoginTenant1; import org.keycloak.testsuite.auth.page.login.SAMLPostLoginTenant2; +import org.keycloak.testsuite.model.StoreProvider; import org.keycloak.testsuite.page.AbstractPage; import org.keycloak.testsuite.saml.AbstractSamlTest; import org.keycloak.testsuite.updaters.ClientAttributeUpdater; @@ -803,7 +804,8 @@ public class SAMLServletAdapterTest extends AbstractSAMLServletAdapterTest { UserRepresentation topGroupUser = createUserRepresentation("topGroupUser", "top@redhat.com", "", "", true); setPasswordFor(topGroupUser, PASSWORD); - assertSuccessfulLogin(salesPostServletPage, topGroupUser, testRealmSAMLPostLoginPage, "principal=topgroupuser"); + String expectedString = StoreProvider.getCurrentProvider().isMapStore() ? "principal=topGroupUser" : "principal=topgroupuser"; + assertSuccessfulLogin(salesPostServletPage, topGroupUser, testRealmSAMLPostLoginPage, expectedString); salesPostServletPage.logout(); checkLoggedOut(salesPostServletPage, testRealmSAMLPostLoginPage); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java index 6f979b8c8e..316f6924ed 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java @@ -35,6 +35,8 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.keycloak.models.Constants.defaultClients; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.admin.client.resource.ClientResource; @@ -455,7 +457,7 @@ public class ClientTest extends AbstractAdminTest { getCleanup().addClientUuid(id); response.close(); UserRepresentation userRep = realm.clients().get(id).getServiceAccountUser(); - assertEquals("service-account-serviceclient", userRep.getUsername()); + MatcherAssert.assertThat("service-account-serviceclient", Matchers.equalToIgnoringCase(userRep.getUsername())); // KEYCLOAK-11197 service accounts are no longer created with a placeholder e-mail. assertNull(userRep.getEmail()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java index 03ac0868ff..9bc1f587fd 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/FineGrainAdminUnitTest.java @@ -16,6 +16,7 @@ */ package org.keycloak.testsuite.admin; +import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; import org.junit.Assert; import org.junit.Test; @@ -628,7 +629,7 @@ public class FineGrainAdminUnitTest extends AbstractKeycloakTest { // Should only return the list of users that belong to "top" group List queryUsers = realmClient.realm(TEST).users().list(); Assert.assertEquals(queryUsers.size(), 1); - Assert.assertEquals("groupmember", queryUsers.get(0).getUsername()); + MatcherAssert.assertThat("groupmember", Matchers.equalToIgnoringCase(queryUsers.get(0).getUsername())); for (UserRepresentation user : queryUsers) { System.out.println(user.getUsername()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UsersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UsersTest.java index 138c689ad4..089d5fadad 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UsersTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UsersTest.java @@ -17,6 +17,7 @@ package org.keycloak.testsuite.admin; +import org.junit.Assume; import org.junit.Before; import org.junit.Test; import org.keycloak.admin.client.Keycloak; @@ -26,6 +27,7 @@ import org.keycloak.common.Profile; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.ManagementPermissionRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.authorization.DecisionStrategy; @@ -33,7 +35,9 @@ import org.keycloak.representations.idm.authorization.PolicyRepresentation; import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation; import org.keycloak.representations.idm.authorization.UserPolicyRepresentation; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; +import org.keycloak.testsuite.updaters.RealmAttributeUpdater; import org.keycloak.testsuite.util.AdminClientUtil; +import org.keycloak.testsuite.util.RealmBuilder; import java.io.IOException; import java.security.KeyManagementException; @@ -42,13 +46,17 @@ import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.not; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.keycloak.models.map.user.MapUserProviderFactory.REALM_ATTR_USERNAME_CASE_SENSITIVE; public class UsersTest extends AbstractAdminTest { @@ -60,6 +68,57 @@ public class UsersTest extends AbstractAdminTest { } } + @Test + public void searchUserDefaultSettings() throws Exception { + createUser(REALM_NAME, "User", "password", "firstName", "lastName", "user@example.com"); + + assertCaseInsensitiveSearch(); + } + + @Test + public void searchUserCaseSensitiveFirst() throws Exception { + Assume.assumeFalse(isJpaRealmProvider()); + Map attributes = new HashMap<>(); + attributes.put(REALM_ATTR_USERNAME_CASE_SENSITIVE, "true"); + try (AutoCloseable c = new RealmAttributeUpdater(adminClient.realm(REALM_NAME)) + .updateWith(r -> r.setAttributes(attributes)) + .update()) { + + createUser(REALM_NAME, "User", "password", "firstName", "lastName", "user@example.com"); + + assertCaseSensitiveSearch(); + + RealmRepresentation realmRep = adminClient.realm(REALM_NAME).toRepresentation(); + RealmBuilder.edit(realmRep) + .attribute(REALM_ATTR_USERNAME_CASE_SENSITIVE, "false"); + realm.update(realmRep); + + assertCaseInsensitiveSearch(); + } + } + + @Test + public void searchUserCaseInSensitiveFirst() throws Exception { + Assume.assumeFalse(isJpaRealmProvider()); + Map attributes = new HashMap<>(); + attributes.put(REALM_ATTR_USERNAME_CASE_SENSITIVE, "false"); + try (AutoCloseable c = new RealmAttributeUpdater(adminClient.realm(REALM_NAME)) + .updateWith(r -> r.setAttributes(attributes)) + .update()) { + + createUser(REALM_NAME, "User", "password", "firstName", "lastName", "user@example.com"); + + assertCaseInsensitiveSearch(); + + RealmRepresentation realmRep = adminClient.realm(REALM_NAME).toRepresentation(); + RealmBuilder.edit(realmRep) + .attribute(REALM_ATTR_USERNAME_CASE_SENSITIVE, "true"); + realm.update(realmRep); + + assertCaseSensitiveSearch(); + } + } + /** * https://issues.redhat.com/browse/KEYCLOAK-15146 */ @@ -426,4 +485,32 @@ public class UsersTest extends AbstractAdminTest { return grp; } + + private void assertCaseInsensitiveSearch() { + // not-exact case-insensitive search + assertThat(realm.users().search("user"), hasSize(1)); + assertThat(realm.users().search("User"), hasSize(1)); + assertThat(realm.users().search("USER"), hasSize(1)); + assertThat(realm.users().search("Use"), hasSize(1)); + + // exact case-insensitive search + assertThat(realm.users().search("user", true), hasSize(1)); + assertThat(realm.users().search("User", true), hasSize(1)); + assertThat(realm.users().search("USER", true), hasSize(1)); + assertThat(realm.users().search("Use", true), hasSize(0)); + } + + private void assertCaseSensitiveSearch() { + // not-exact case-sensitive search + assertThat(realm.users().search("user"), hasSize(0)); + assertThat(realm.users().search("User"), hasSize(1)); + assertThat(realm.users().search("USER"), hasSize(0)); + assertThat(realm.users().search("Use"), hasSize(1)); + + // exact case-sensitive search + assertThat(realm.users().search("user", true), hasSize(0)); + assertThat(realm.users().search("User", true), hasSize(1)); + assertThat(realm.users().search("USER", true), hasSize(0)); + assertThat(realm.users().search("Use", true), hasSize(0)); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AccountLinkTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AccountLinkTest.java index 98175a16be..91f8194604 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AccountLinkTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AccountLinkTest.java @@ -207,7 +207,7 @@ public class AccountLinkTest extends AbstractKeycloakTest { String memProviderId = ApiUtil.getCreatedId(resp); // Create federated user - String username = "fedUser1"; + String username = "fed-user1"; UserRepresentation userRepresentation = new UserRepresentation(); userRepresentation.setUsername(username); userRepresentation.setEmail("feduser1@mail.com"); 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 7785eb1a0a..3049369151 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 @@ -371,7 +371,7 @@ public class LDAPProvidersIntegrationTest extends AbstractLDAPTest { // KEYCLOAK-12340 @Test public void ldapPasswordChangeWithAdminEndpointAndRequiredAction() throws Exception { - String username = "adminEndpointReqAct"; + String username = "admin-endpoint-req-act"; String email = username + "@email.cz"; // Register new LDAP user with password, logout user @@ -505,13 +505,13 @@ public class LDAPProvidersIntegrationTest extends AbstractLDAPTest { loginPage.clickRegister(); registerPage.assertCurrent(); - registerPage.register("firstName", "lastName", "email2@check.cz", "registerUserSuccess2", "Password1", "Password1"); + registerPage.register("firstName", "lastName", "email2@check.cz", "register-user-success2", "Password1", "Password1"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); - UserRepresentation user = ApiUtil.findUserByUsername(testRealm(),"registerUserSuccess2"); + UserRepresentation user = ApiUtil.findUserByUsername(testRealm(),"register-user-success2"); Assert.assertNotNull(user); assertFederatedUserLink(user); - Assert.assertEquals("registerusersuccess2", user.getUsername()); + Assert.assertEquals("register-user-success2", user.getUsername()); Assert.assertEquals("firstName", user.getFirstName()); Assert.assertEquals("lastName", user.getLastName()); Assert.assertTrue(user.isEnabled()); @@ -812,17 +812,17 @@ public class LDAPProvidersIntegrationTest extends AbstractLDAPTest { @Test public void testRemoveFederatedUser() { - UserRepresentation user = ApiUtil.findUserByUsername(testRealm(), "registerusersuccess2"); + UserRepresentation user = ApiUtil.findUserByUsername(testRealm(), "register-user-success2"); // Case when this test was executed "alone" (User "registerusersuccess2" is registered inside registerUserLdapSuccess) if (user == null) { registerUserLdapSuccess(); - user = ApiUtil.findUserByUsername(testRealm(), "registerusersuccess2"); + user = ApiUtil.findUserByUsername(testRealm(), "register-user-success2"); } assertFederatedUserLink(user); testRealm().users().get(user.getId()).remove(); - user = ApiUtil.findUserByUsername(testRealm(), "registerusersuccess2"); + user = ApiUtil.findUserByUsername(testRealm(), "register-user-success2"); Assert.assertNull(user); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java index c784ea2e91..11e201c9ce 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java @@ -361,7 +361,7 @@ public class UserStorageTest extends AbstractAuthTest { testRealmAccountPage.navigateTo(); loginPage.clickRegister(); - registerPage.register("firstName", "lastName", "email@mail.com", "verifyEmail", "password", "password"); + registerPage.register("firstName", "lastName", "email@mail.com", "verify-email", "password", "password"); verifyEmailPage.assertCurrent(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java index 63ddfb18aa..4ec8b08d0d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java @@ -16,6 +16,7 @@ */ package org.keycloak.testsuite.forms; +import org.hamcrest.Matchers; import org.jboss.arquillian.graphene.page.Page; import org.junit.Assert; import org.junit.Assume; @@ -696,7 +697,7 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest { assertThat(user, notNullValue()); if (username != null) { - assertThat(username.toLowerCase(), is(user.getUsername())); + assertThat(username, Matchers.equalToIgnoringCase(user.getUsername())); } assertThat(email.toLowerCase(), is(user.getEmail())); assertThat(firstName, is(user.getFirstName())); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterWithUserProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterWithUserProfileTest.java index e697b1b708..db3db111fe 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterWithUserProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterWithUserProfileTest.java @@ -31,6 +31,7 @@ import java.util.Collections; import java.util.List; import org.apache.commons.lang3.StringUtils; +import org.hamcrest.Matchers; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -613,7 +614,7 @@ public class RegisterWithUserProfileTest extends RegisterTest { // test that timestamp is current with 10s tollerance Assert.assertTrue((System.currentTimeMillis() - user.getCreatedTimestamp()) < 10000); // test user info is set from form - assertEquals(username.toLowerCase(), user.getUsername()); + assertThat(username, Matchers.equalToIgnoringCase(user.getUsername())); assertEquals(email.toLowerCase(), user.getEmail()); assertEquals(firstName, user.getFirstName()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/AbstractX509AuthenticationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/AbstractX509AuthenticationTest.java index 7cbfa7093f..8bde86521b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/AbstractX509AuthenticationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/AbstractX509AuthenticationTest.java @@ -293,7 +293,7 @@ public abstract class AbstractX509AuthenticationTest extends AbstractTestRealmKe UserRepresentation user = UserBuilder.create() .id(KeycloakModelUtils.generateId()) - .username("Keycloak") + .username("keycloak") .email("localhost@localhost") .enabled(true) .password("password") diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/UserModelTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/UserModelTest.java index 1e38b204e6..8cc51b839e 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/UserModelTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/UserModelTest.java @@ -20,11 +20,13 @@ import org.keycloak.component.ComponentModel; import org.keycloak.models.Constants; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.ModelException; +import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; import org.keycloak.models.RealmProvider; import org.keycloak.models.UserModel; import org.keycloak.models.UserProvider; +import org.keycloak.models.map.realm.MapRealmProviderFactory; +import org.keycloak.models.map.user.MapUserProviderFactory; import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProviderFactory; import org.keycloak.storage.UserStorageProviderModel; @@ -36,21 +38,27 @@ import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.hamcrest.Matchers; import org.junit.Test; -import javax.naming.NamingException; - import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.junit.Assume.assumeThat; +import org.keycloak.models.jpa.JpaUserProvider; +import static org.keycloak.models.map.user.MapUserProviderFactory.REALM_ATTR_USERNAME_CASE_SENSITIVE; /** * @@ -66,6 +74,8 @@ public class UserModelTest extends KeycloakModelTest { private static final int DELETED_USER_COUNT = LAST_DELETED_USER_INDEX - FIRST_DELETED_USER_INDEX; private String realmId; + private String realm1Id; + private String realm2Id; private final List groupIds = new ArrayList<>(NUM_GROUPS); private String userFederationId; @@ -83,6 +93,8 @@ public class UserModelTest extends KeycloakModelTest { @Override public void cleanEnvironment(KeycloakSession s) { s.realms().removeRealm(realmId); + if (realm1Id != null) s.realms().removeRealm(realm1Id); + if (realm2Id != null) s.realms().removeRealm(realm2Id); } @Override @@ -115,6 +127,53 @@ public class UserModelTest extends KeycloakModelTest { return null; } + @Test + @RequireProvider(value = UserProvider.class, only = {MapUserProviderFactory.PROVIDER_ID}) + @RequireProvider(value = RealmProvider.class, only = {MapRealmProviderFactory.PROVIDER_ID}) + public void testCaseSensitivityGetUserByUsername() { + + realm1Id = inComittedTransaction((Function) session -> { + RealmModel realm = session.realms().createRealm("realm1"); + realm.setDefaultRole(session.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName())); + realm.setAttribute(REALM_ATTR_USERNAME_CASE_SENSITIVE, true); + return realm.getId(); + }); + + withRealm(realm1Id, (session, realm) -> { + UserModel user1 = session.users().addUser(realm, "user"); + UserModel user2 = session.users().addUser(realm, "USER"); + + assertThat(user1, not(nullValue())); + assertThat(user2, not(nullValue())); + + assertThat(user1.getUsername(), equalTo("user")); + assertThat(user2.getUsername(), equalTo("USER")); + + return null; + }); + + realm2Id = inComittedTransaction((Function) session -> { + RealmModel realm = session.realms().createRealm("realm2"); + realm.setDefaultRole(session.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName())); + realm.setAttribute(REALM_ATTR_USERNAME_CASE_SENSITIVE, false); + return realm.getId(); + }); + + withRealm(realm2Id, (session, realm) -> { + UserModel user1 = session.users().addUser(realm, "user"); + assertThat(user1, not(nullValue())); + + try { + session.users().addUser(realm, "USER"); + } catch (ModelDuplicateException e) { + return null; // expected + } + + fail("ModelDuplicateException expected"); + return null; + }); + } + @Test public void testAddRemoveUser() { inRolledBackTransaction(1, this::addRemoveUser);