Enable MapUserProvider storing username with the letter case significance
Closes #10245 Closes #11602
This commit is contained in:
parent
fb33cbc2bd
commit
869ccc82b2
30 changed files with 375 additions and 57 deletions
|
@ -68,6 +68,8 @@ public class IckleQueryMapModelCriteriaBuilder<E extends AbstractHotRodEntity, M
|
|||
|
||||
INFINISPAN_NAME_OVERRIDES.put(RoleModel.SearchableFields.IS_CLIENT_ROLE, "clientRole");
|
||||
|
||||
INFINISPAN_NAME_OVERRIDES.put(UserModel.SearchableFields.USERNAME_CASE_INSENSITIVE, "usernameLowercase");
|
||||
INFINISPAN_NAME_OVERRIDES.put(UserModel.SearchableFields.USERNAME, "username");
|
||||
INFINISPAN_NAME_OVERRIDES.put(UserModel.SearchableFields.SERVICE_ACCOUNT_CLIENT, "serviceAccountClientLink");
|
||||
INFINISPAN_NAME_OVERRIDES.put(UserModel.SearchableFields.CONSENT_FOR_CLIENT, "userConsents.clientId");
|
||||
INFINISPAN_NAME_OVERRIDES.put(UserModel.SearchableFields.CONSENT_WITH_CLIENT_SCOPE, "userConsents.grantedClientScopesIds");
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.keycloak.models.UserModel;
|
|||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.map.storage.CriterionNotSupportedException;
|
||||
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.storage.SearchableModelField;
|
||||
import org.keycloak.storage.StorageId;
|
||||
import org.keycloak.util.EnumWithStableIndex;
|
||||
|
@ -62,6 +63,7 @@ public class IckleQueryWhereClauses {
|
|||
WHERE_CLAUSE_PRODUCER_OVERRIDES.put(UserModel.SearchableFields.ATTRIBUTE, IckleQueryWhereClauses::whereClauseForAttributes);
|
||||
WHERE_CLAUSE_PRODUCER_OVERRIDES.put(UserModel.SearchableFields.IDP_AND_USER, IckleQueryWhereClauses::whereClauseForUserIdpAlias);
|
||||
WHERE_CLAUSE_PRODUCER_OVERRIDES.put(UserModel.SearchableFields.CONSENT_CLIENT_FEDERATION_LINK, IckleQueryWhereClauses::whereClauseForConsentClientFederationLink);
|
||||
WHERE_CLAUSE_PRODUCER_OVERRIDES.put(UserModel.SearchableFields.USERNAME_CASE_INSENSITIVE, IckleQueryWhereClauses::whereClauseForUsernameCaseInsensitive);
|
||||
WHERE_CLAUSE_PRODUCER_OVERRIDES.put(UserSessionModel.SearchableFields.CORRESPONDING_SESSION_ID, IckleQueryWhereClauses::whereClauseForCorrespondingSessionId);
|
||||
WHERE_CLAUSE_PRODUCER_OVERRIDES.put(Policy.SearchableFields.CONFIG, IckleQueryWhereClauses::whereClauseForPolicyConfig);
|
||||
WHERE_CLAUSE_PRODUCER_OVERRIDES.put(Event.SearchableFields.EVENT_TYPE, IckleQueryWhereClauses::whereClauseForEnumWithStableIndex);
|
||||
|
@ -226,4 +228,14 @@ public class IckleQueryWhereClauses {
|
|||
|
||||
return produceWhereClause(modelFieldName, op, values, parameters);
|
||||
}
|
||||
|
||||
private static String whereClauseForUsernameCaseInsensitive(String modelFieldName, ModelCriteriaBuilder.Operator op, Object[] values, Map<String, Object> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<Function<ObjectNode, ObjectNode>> 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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,8 +54,31 @@ public class JpaUserModelCriteriaBuilder extends JpaModelCriteriaBuilder<JpaUser
|
|||
public JpaUserModelCriteriaBuilder compare(SearchableModelField<? super UserModel> 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<JpaUser
|
|||
throw new CriterionNotSupportedException(modelField, op);
|
||||
}
|
||||
case ILIKE:
|
||||
if (modelField == UserModel.SearchableFields.USERNAME ||
|
||||
modelField == UserModel.SearchableFields.FIRST_NAME ||
|
||||
if (modelField == UserModel.SearchableFields.FIRST_NAME ||
|
||||
modelField == UserModel.SearchableFields.LAST_NAME ||
|
||||
modelField == UserModel.SearchableFields.EMAIL) {
|
||||
|
||||
validateValue(value, modelField, op, String.class);
|
||||
return new JpaUserModelCriteriaBuilder((cb, query, root) ->
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,4 +19,5 @@ limitations under the License.
|
|||
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||
<include file="META-INF/users/jpa-users-changelog-1.xml"/>
|
||||
<include file="META-INF/users/jpa-users-changelog-2.xml"/>
|
||||
</databaseChangeLog>
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2022 Red Hat, Inc. and/or its affiliates
|
||||
and other contributors as indicated by the @author tags.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd
|
||||
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
|
||||
|
||||
<changeSet author="keycloak" id="users-10245">
|
||||
|
||||
<ext:addGeneratedColumn tableName="kc_user">
|
||||
<ext:column name="usernamelowercase" type="VARCHAR(255)" jsonColumn="metadata" jsonProperty="usernameLowerCase"/>
|
||||
</ext:addGeneratedColumn>
|
||||
<createIndex tableName="kc_user" indexName="user_username_lower_case_realmid">
|
||||
<column name="usernamelowercase"/>
|
||||
<column name="realmid"/>
|
||||
</createIndex>
|
||||
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
|
@ -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<Object, MapUserEntity, UserModel> usernameCaseInsensitive(MapModelCriteriaBuilder<Object, MapUserEntity, UserModel> 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<Object> valueComparator = CriteriaOperator.predicateFor(op, values);
|
||||
Function<MapUserEntity, ?> getter = ue -> valueComparator.test(KeycloakModelUtils.toLowerCaseSafe(ue.getUsername()));
|
||||
return mcb.fieldCompare(Boolean.TRUE::equals, getter);
|
||||
}
|
||||
|
||||
private static MapModelCriteriaBuilder<Object, MapUserEntity, UserModel> getUserConsentClientFederationLink(MapModelCriteriaBuilder<Object, MapUserEntity, UserModel> 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();
|
||||
|
|
|
@ -54,7 +54,6 @@ public abstract class MapUserAdapter extends AbstractUserModel<MapUserEntity> {
|
|||
|
||||
@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;
|
||||
|
||||
|
|
|
@ -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<MapUserEntity, UserModel> 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<UserModel> 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<UserModel> 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<MapUserEntity> 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<MapUserEntity> s = tx.read(withCriteria(mcb).orderBy(SearchableFields.USERNAME, ASCENDING))) {
|
||||
List<MapUserEntity> 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<MapUserEntity> usersWithEmail = tx.read(withCriteria(mcb))
|
||||
.filter(userEntity -> Objects.equals(userEntity.getEmail(), email))
|
||||
.collect(Collectors.toList());
|
||||
List<MapUserEntity> 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<UserModel> 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<UserModel> addSearchToModelCriteria(String value,
|
||||
private DefaultModelCriteria<UserModel> addSearchToModelCriteria(RealmModel realm, String value,
|
||||
DefaultModelCriteria<UserModel> 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));
|
||||
|
|
|
@ -40,6 +40,9 @@ import static org.keycloak.models.map.common.AbstractMapProviderFactory.MapProvi
|
|||
*/
|
||||
public class MapUserProviderFactory extends AbstractMapProviderFactory<MapUserProvider, MapUserEntity, UserModel> implements UserProviderFactory<MapUserProvider>, 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);
|
||||
}
|
||||
|
|
|
@ -271,10 +271,6 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
|||
values = (List<String>) value;
|
||||
}
|
||||
|
||||
if (key.equals(UserModel.USERNAME)) {
|
||||
values = Collections.singletonList(values.get(0).toLowerCase());
|
||||
}
|
||||
|
||||
newAttributes.put(key, Collections.unmodifiableList(values));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,6 @@ public interface UserModel extends RoleMapperModel {
|
|||
public static class SearchableFields {
|
||||
public static final SearchableModelField<UserModel> ID = new SearchableModelField<>("id", String.class);
|
||||
public static final SearchableModelField<UserModel> REALM_ID = new SearchableModelField<>("realmId", String.class);
|
||||
public static final SearchableModelField<UserModel> USERNAME = new SearchableModelField<>("username", String.class);
|
||||
public static final SearchableModelField<UserModel> FIRST_NAME = new SearchableModelField<>("firstName", String.class);
|
||||
public static final SearchableModelField<UserModel> LAST_NAME = new SearchableModelField<>("lastName", String.class);
|
||||
public static final SearchableModelField<UserModel> EMAIL = new SearchableModelField<>("email", String.class);
|
||||
|
@ -61,6 +60,15 @@ public interface UserModel extends RoleMapperModel {
|
|||
public static final SearchableModelField<UserModel> EMAIL_VERIFIED = new SearchableModelField<>("emailVerified", Boolean.class);
|
||||
public static final SearchableModelField<UserModel> FEDERATION_LINK = new SearchableModelField<>("federationLink", String.class);
|
||||
|
||||
/**
|
||||
* Search for user's username in case sensitive mode.
|
||||
*/
|
||||
public static final SearchableModelField<UserModel> USERNAME = new SearchableModelField<>("username", String.class);
|
||||
/**
|
||||
* Search for user's username in case insensitive mode.
|
||||
*/
|
||||
public static final SearchableModelField<UserModel> 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).
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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<UserRepresentation> 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());
|
||||
}
|
||||
|
|
|
@ -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<String, String> 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<String, String> 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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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<String> 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<KeycloakSession, String>) 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<KeycloakSession, String>) 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);
|
||||
|
|
Loading…
Reference in a new issue