diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0176a11cf0..8c5bac025d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -160,7 +160,7 @@ jobs: run: | declare -A PARAMS TESTGROUP PARAMS["quarkus"]="-Pauth-server-quarkus" - PARAMS["undertow-map"]="-Pauth-server-undertow -Dkeycloak.client.provider=map -Dkeycloak.group.provider=map -Dkeycloak.role.provider=map -Dkeycloak.authSession.provider=map" + PARAMS["undertow-map"]="-Pauth-server-undertow -Dkeycloak.client.provider=map -Dkeycloak.group.provider=map -Dkeycloak.role.provider=map -Dkeycloak.authSession.provider=map -Dkeycloak.user.provider=map" PARAMS["wildfly"]="-Pauth-server-wildfly" TESTGROUP["group1"]="-Dtest=!**.crossdc.**,!**.cluster.**,%regex[org.keycloak.testsuite.(a[abc]|ad[a-l]|[^a-q]).*]" # Tests alphabetically before admin tests and those after "r" TESTGROUP["group2"]="-Dtest=!**.crossdc.**,!**.cluster.**,%regex[org.keycloak.testsuite.(ad[^a-l]|a[^a-d]|b).*]" # Admin tests and those starting with "b" diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java index 7a00eae871..524499e423 100755 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java @@ -353,7 +353,7 @@ public class LDAPStorageProvider implements UserStorageProvider, } @Override - public Stream searchForUserStream(String search, RealmModel realm, int firstResult, int maxResults) { + public Stream searchForUserStream(String search, RealmModel realm, Integer firstResult, Integer maxResults) { Map attributes = new HashMap(); attributes.put(UserModel.SEARCH,search); return searchForUserStream(attributes, realm, firstResult, maxResults); @@ -365,7 +365,7 @@ public class LDAPStorageProvider implements UserStorageProvider, } @Override - public Stream searchForUserStream(Map params, RealmModel realm, int firstResult, int maxResults) { + public Stream searchForUserStream(Map params, RealmModel realm, Integer firstResult, Integer maxResults) { String search = params.get(UserModel.SEARCH); if(search!=null) { int spaceIndex = search.lastIndexOf(' '); @@ -401,7 +401,7 @@ public class LDAPStorageProvider implements UserStorageProvider, } @Override - public Stream getGroupMembersStream(RealmModel realm, GroupModel group, int firstResult, int maxResults) { + public Stream getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults) { return realm.getComponentsStream(model.getId(), LDAPStorageMapper.class.getName()) .sorted(ldapMappersComparator.sortAsc()) .map(mapperModel -> @@ -417,7 +417,7 @@ public class LDAPStorageProvider implements UserStorageProvider, } @Override - public Stream getRoleMembersStream(RealmModel realm, RoleModel role, int firstResult, int maxResults) { + public Stream getRoleMembersStream(RealmModel realm, RoleModel role, Integer firstResult, Integer maxResults) { return realm.getComponentsStream(model.getId(), LDAPStorageMapper.class.getName()) .sorted(ldapMappersComparator.sortAsc()) .map(mapperModel -> mapperManager.getMapper(mapperModel).getRoleMembers(realm, role, firstResult, maxResults)) diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java index 41b6c42ce7..b4692a024b 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java @@ -454,7 +454,7 @@ public class UserCacheSession implements UserCache.Streams { } @Override - public Stream getGroupMembersStream(RealmModel realm, GroupModel group, int firstResult, int maxResults) { + public Stream getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults) { return getDelegate().getGroupMembersStream(realm, group, firstResult, maxResults); } @@ -464,7 +464,7 @@ public class UserCacheSession implements UserCache.Streams { } @Override - public Stream getRoleMembersStream(RealmModel realm, RoleModel role, int firstResult, int maxResults) { + public Stream getRoleMembersStream(RealmModel realm, RoleModel role, Integer firstResult, Integer maxResults) { return getDelegate().getRoleMembersStream(realm, role, firstResult, maxResults); } @@ -472,7 +472,7 @@ public class UserCacheSession implements UserCache.Streams { public Stream getRoleMembersStream(RealmModel realm, RoleModel role) { return getDelegate().getRoleMembersStream(realm, role); } - + @Override public UserModel getServiceAccount(ClientModel client) { // Just an attempt to find the user from cache by default serviceAccount username @@ -576,7 +576,7 @@ public class UserCacheSession implements UserCache.Streams { } @Override - public Stream getUsersStream(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) { + public Stream getUsersStream(RealmModel realm, Integer firstResult, Integer maxResults, boolean includeServiceAccounts) { return getDelegate().getUsersStream(realm, firstResult, maxResults, includeServiceAccounts); } @@ -596,7 +596,7 @@ public class UserCacheSession implements UserCache.Streams { } @Override - public Stream searchForUserStream(String search, RealmModel realm, int firstResult, int maxResults) { + public Stream searchForUserStream(String search, RealmModel realm, Integer firstResult, Integer maxResults) { return getDelegate().searchForUserStream(search, realm, firstResult, maxResults); } @@ -606,7 +606,7 @@ public class UserCacheSession implements UserCache.Streams { } @Override - public Stream searchForUserStream(Map attributes, RealmModel realm, int firstResult, int maxResults) { + public Stream searchForUserStream(Map attributes, RealmModel realm, Integer firstResult, Integer maxResults) { return getDelegate().searchForUserStream(attributes, realm, firstResult, maxResults); } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index b46d7d63c8..04ead6e978 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -98,6 +98,18 @@ public class JpaUserProvider implements UserProvider.Streams, UserCredentialStor credentialStore = new JpaUserCredentialStore(session, em); } + private static TypedQuery paginateQuery(TypedQuery query, Integer first, Integer max) { + if (first != null && first > 0) { + query = query.setFirstResult(first); + } + + if (max != null && max >= 0) { + query = query.setMaxResults(max); + } + + return query; + } + @Override public UserModel addUser(RealmModel realm, String id, String username, boolean addDefaultRoles, boolean addDefaultRequiredActions) { if (id == null) { @@ -121,13 +133,11 @@ public class JpaUserProvider implements UserProvider.Streams, UserCredentialStor } if (addDefaultRequiredActions) { - Optional requiredAction = realm.getRequiredActionProvidersStream() - .filter(RequiredActionProviderModel::isEnabled) - .filter(RequiredActionProviderModel::isDefaultAction) - .map(RequiredActionProviderModel::getAlias) - .findFirst(); - if (requiredAction.isPresent()) - userModel.addRequiredAction(requiredAction.get()); + realm.getRequiredActionProvidersStream() + .filter(RequiredActionProviderModel::isEnabled) + .filter(RequiredActionProviderModel::isDefaultAction) + .map(RequiredActionProviderModel::getAlias) + .forEach(userModel::addRequiredAction); } return userModel; @@ -140,7 +150,7 @@ public class JpaUserProvider implements UserProvider.Streams, UserCredentialStor @Override public boolean removeUser(RealmModel realm, UserModel user) { - UserEntity userEntity = em.find(UserEntity.class, user.getId()); + UserEntity userEntity = em.find(UserEntity.class, user.getId(), LockModeType.PESSIMISTIC_WRITE); if (userEntity == null) return false; removeUser(userEntity); return true; @@ -150,18 +160,10 @@ public class JpaUserProvider implements UserProvider.Streams, UserCredentialStor String id = user.getId(); em.createNamedQuery("deleteUserRoleMappingsByUser").setParameter("user", user).executeUpdate(); em.createNamedQuery("deleteUserGroupMembershipsByUser").setParameter("user", user).executeUpdate(); - em.createNamedQuery("deleteFederatedIdentityByUser").setParameter("user", user).executeUpdate(); em.createNamedQuery("deleteUserConsentClientScopesByUser").setParameter("user", user).executeUpdate(); em.createNamedQuery("deleteUserConsentsByUser").setParameter("user", user).executeUpdate(); - em.flush(); - // not sure why i have to do a clear() here. I was getting some messed up errors that Hibernate couldn't - // un-delete the UserEntity. - em.clear(); - user = em.find(UserEntity.class, id, LockModeType.PESSIMISTIC_WRITE); - if (user != null) { - em.remove(user); - } + em.remove(user); em.flush(); } @@ -749,44 +751,29 @@ public class JpaUserProvider implements UserProvider.Streams, UserCredentialStor } @Override - public Stream getUsersStream(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) { + public Stream getUsersStream(RealmModel realm, Integer firstResult, Integer maxResults, boolean includeServiceAccounts) { String queryName = includeServiceAccounts ? "getAllUsersByRealm" : "getAllUsersByRealmExcludeServiceAccount" ; TypedQuery query = em.createNamedQuery(queryName, UserEntity.class); query.setParameter("realmId", realm.getId()); - if (firstResult != -1) { - query.setFirstResult(firstResult); - } - if (maxResults != -1) { - query.setMaxResults(maxResults); - } - return closing(query.getResultStream().map(entity -> new UserAdapter(session, realm, em, entity))); + + return closing(paginateQuery(query, firstResult, maxResults).getResultStream().map(entity -> new UserAdapter(session, realm, em, entity))); } @Override - public Stream getGroupMembersStream(RealmModel realm, GroupModel group, int firstResult, int maxResults) { + public Stream getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults) { TypedQuery query = em.createNamedQuery("groupMembership", UserEntity.class); query.setParameter("groupId", group.getId()); - if (firstResult != -1) { - query.setFirstResult(firstResult); - } - if (maxResults != -1) { - query.setMaxResults(maxResults); - } - return closing(query.getResultStream().map(user -> new UserAdapter(session, realm, em, user))); + + return closing(paginateQuery(query, firstResult, maxResults).getResultStream().map(user -> new UserAdapter(session, realm, em, user))); } @Override - public Stream getRoleMembersStream(RealmModel realm, RoleModel role, int firstResult, int maxResults) { + public Stream getRoleMembersStream(RealmModel realm, RoleModel role, Integer firstResult, Integer maxResults) { TypedQuery query = em.createNamedQuery("usersInRole", UserEntity.class); query.setParameter("roleId", role.getId()); - if (firstResult != -1) { - query.setFirstResult(firstResult); - } - if (maxResults != -1) { - query.setMaxResults(maxResults); - } - return closing(query.getResultStream().map(user -> new UserAdapter(session, realm, em, user))); + + return closing(paginateQuery(query, firstResult, maxResults).getResultStream().map(user -> new UserAdapter(session, realm, em, user))); } @Override @@ -795,7 +782,7 @@ public class JpaUserProvider implements UserProvider.Streams, UserCredentialStor } @Override - public Stream searchForUserStream(String search, RealmModel realm, int firstResult, int maxResults) { + public Stream searchForUserStream(String search, RealmModel realm, Integer firstResult, Integer maxResults) { Map attributes = new HashMap<>(); attributes.put(UserModel.SEARCH, search); session.setAttribute(UserModel.INCLUDE_SERVICE_ACCOUNT, false); @@ -808,7 +795,7 @@ public class JpaUserProvider implements UserProvider.Streams, UserCredentialStor } @Override - public Stream searchForUserStream(Map attributes, RealmModel realm, int firstResult, int maxResults) { + public Stream searchForUserStream(Map attributes, RealmModel realm, Integer firstResult, Integer maxResults) { CriteriaBuilder builder = em.getCriteriaBuilder(); CriteriaQuery queryBuilder = builder.createQuery(UserEntity.class); Root root = queryBuilder.from(UserEntity.class); @@ -915,16 +902,9 @@ public class JpaUserProvider implements UserProvider.Streams, UserCredentialStor TypedQuery query = em.createQuery(queryBuilder); - if (firstResult != -1) { - query.setFirstResult(firstResult); - } - - if (maxResults != -1) { - query.setMaxResults(maxResults); - } - UserProvider users = session.users(); - return closing(query.getResultStream().map(userEntity -> users.getUserById(userEntity.getId(), realm))); + return closing(paginateQuery(query, firstResult, maxResults).getResultStream()) + .map(userEntity -> users.getUserById(userEntity.getId(), realm)); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java index 7d9089c2b9..a3dad14b58 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java @@ -263,12 +263,6 @@ public class UserAdapter implements UserModel.Streams, JpaModel { return user.getRequiredActions().stream().map(action -> action.getAction()).distinct(); } - @Override - public void addRequiredAction(RequiredAction action) { - String actionName = action.name(); - addRequiredAction(actionName); - } - @Override public void addRequiredAction(String actionName) { for (UserRequiredActionEntity attr : user.getRequiredActions()) { @@ -283,12 +277,6 @@ public class UserAdapter implements UserModel.Streams, JpaModel { user.getRequiredActions().add(attr); } - @Override - public void removeRequiredAction(RequiredAction action) { - String actionName = action.name(); - removeRequiredAction(actionName); - } - @Override public void removeRequiredAction(String actionName) { Iterator it = user.getRequiredActions().iterator(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java index f41b46b29d..fece208f45 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java @@ -110,7 +110,7 @@ public class UserEntity { @BatchSize(size = 20) protected Collection credentials; - @OneToMany(mappedBy="user") + @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="user") @Fetch(FetchMode.SELECT) @BatchSize(size = 20) protected Collection federatedIdentities; diff --git a/model/map/pom.xml b/model/map/pom.xml index 12e27fce5c..a349a93d52 100644 --- a/model/map/pom.xml +++ b/model/map/pom.xml @@ -37,6 +37,11 @@ junit test + + org.hamcrest + hamcrest-all + test + \ No newline at end of file diff --git a/model/map/src/main/java/org/keycloak/models/map/common/Serialization.java b/model/map/src/main/java/org/keycloak/models/map/common/Serialization.java index ccb1b8000d..e0a1358d9b 100644 --- a/model/map/src/main/java/org/keycloak/models/map/common/Serialization.java +++ b/model/map/src/main/java/org/keycloak/models/map/common/Serialization.java @@ -51,6 +51,7 @@ public class Serialization { } try { // Naive solution but will do. + @SuppressWarnings("unchecked") final T res = MAPPER.readValue(MAPPER.writeValueAsBytes(orig), (Class) orig.getClass()); return res; } catch (IOException ex) { diff --git a/model/map/src/main/java/org/keycloak/models/map/user/AbstractUserEntity.java b/model/map/src/main/java/org/keycloak/models/map/user/AbstractUserEntity.java new file mode 100644 index 0000000000..8a353e9aa1 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/user/AbstractUserEntity.java @@ -0,0 +1,371 @@ +/* + * Copyright 2020 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. + */ + +package org.keycloak.models.map.user; + +import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.models.utils.KeycloakModelUtils; + +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * + * @author mhajas + */ +public abstract class AbstractUserEntity implements AbstractEntity { + + private final K id; + private final String realmId; + + private String username; + private String firstName; + private Long createdTimestamp; + private String lastName; + private String email; + private boolean enabled; + private boolean emailVerified; + // This is necessary to be able to dynamically switch unique email constraints on and off in the realm settings + private String emailConstraint = KeycloakModelUtils.generateId(); + private Map> attributes = new HashMap<>(); + private Set requiredActions = new HashSet<>(); + private final Map credentials = new HashMap<>(); + private final List credentialsOrder = new LinkedList<>(); + private final Map federatedIdentities = new HashMap<>(); + private final Map userConsents = new HashMap<>(); + private Set groupsMembership = new HashSet<>(); + private Set rolesMembership = new HashSet<>(); + private String federationLink; + private String serviceAccountClientLink; + private int notBefore; + + static Comparator> COMPARE_BY_USERNAME = Comparator.comparing(AbstractUserEntity::getUsername); + + /** + * Flag signalizing that any of the setters has been meaningfully used. + */ + protected boolean updated; + + protected AbstractUserEntity() { + this.id = null; + this.realmId = null; + } + + public AbstractUserEntity(K id, String realmId) { + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(realmId, "realmId"); + + this.id = id; + this.realmId = realmId; + } + + @Override + public K getId() { + return this.id; + } + + @Override + public boolean isUpdated() { + return this.updated + || userConsents.values().stream().anyMatch(UserConsentEntity::isUpdated) + || credentials.values().stream().anyMatch(UserCredentialEntity::isUpdated) + || federatedIdentities.values().stream().anyMatch(UserFederatedIdentityEntity::isUpdated); + } + + public String getRealmId() { + return realmId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.updated |= !Objects.equals(this.username, username); + this.username = username; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.updated |= !Objects.equals(this.firstName, firstName); + this.firstName = firstName; + } + + public Long getCreatedTimestamp() { + return createdTimestamp; + } + + public void setCreatedTimestamp(Long createdTimestamp) { + this.updated |= !Objects.equals(this.createdTimestamp, createdTimestamp); + this.createdTimestamp = createdTimestamp; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.updated |= !Objects.equals(this.lastName, lastName); + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email, boolean duplicateEmailsAllowed) { + this.updated |= !Objects.equals(this.email, email); + this.email = email; + this.emailConstraint = email == null || duplicateEmailsAllowed ? KeycloakModelUtils.generateId() : email; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.updated |= !Objects.equals(this.enabled, enabled); + this.enabled = enabled; + } + + public boolean isEmailVerified() { + return emailVerified; + } + + public void setEmailVerified(boolean emailVerified) { + this.updated |= !Objects.equals(this.emailVerified, emailVerified); + this.emailVerified = emailVerified; + } + + public String getEmailConstraint() { + return emailConstraint; + } + + public void setEmailConstraint(String emailConstraint) { + this.updated |= !Objects.equals(this.emailConstraint, emailConstraint); + this.emailConstraint = emailConstraint; + } + + public Map> getAttributes() { + return attributes; + } + + public List getAttribute(String name) { + return attributes.getOrDefault(name, Collections.emptyList()); + } + + public void setAttributes(Map> attributes) { + this.updated |= !Objects.equals(this.attributes, attributes); + this.attributes = attributes; + } + + public void setAttribute(String name, List value) { + this.updated |= !Objects.equals(this.attributes.put(name, value), value); + } + + public void removeAttribute(String name) { + this.updated |= this.attributes.remove(name) != null; + } + + public Set getRequiredActions() { + return requiredActions; + } + + public void setRequiredActions(Set requiredActions) { + this.updated |= !Objects.equals(this.requiredActions, requiredActions); + this.requiredActions = requiredActions; + } + + public void addRequiredAction(String requiredAction) { + this.updated |= this.requiredActions.add(requiredAction); + } + + public void removeRequiredAction(String requiredAction) { + this.updated |= this.requiredActions.remove(requiredAction); + } + + public void updateCredential(UserCredentialEntity credentialEntity) { + this.updated |= credentials.replace(credentialEntity.getId(), credentialEntity) != null; + } + + public void addCredential(UserCredentialEntity credentialEntity) { + if (credentials.containsKey(credentialEntity.getId())) { + throw new ModelDuplicateException("A CredentialModel with given id already exists"); + } + + this.updated = true; + credentials.put(credentialEntity.getId(), credentialEntity); + credentialsOrder.add(credentialEntity.getId()); + } + + public boolean removeCredential(String credentialId) { + if (!credentials.containsKey(credentialId)) { + return false; + } + + this.updated = true; + this.credentials.remove(credentialId); + this.credentialsOrder.remove(credentialId); + + return true; + } + + public UserCredentialEntity getCredential(String id) { + return credentials.get(id); + } + + public Stream getCredentials() { + return credentialsOrder.stream() + .map(credentials::get); + } + + public int getCredentialIndex(String credentialId) { + return credentialsOrder.indexOf(credentialId); + } + + public void moveCredential(int currentPosition, int newPosition) { + this.updated |= currentPosition != newPosition; + credentialsOrder.add(newPosition, credentialsOrder.remove(currentPosition)); + } + + public Stream getFederatedIdentities() { + return federatedIdentities.values().stream(); + } + + public void setFederatedIdentities(Collection federatedIdentities) { + this.updated = true; + this.federatedIdentities.clear(); + this.federatedIdentities.putAll(federatedIdentities.stream() + .collect(Collectors.toMap(UserFederatedIdentityEntity::getIdentityProvider, Function.identity()))); + } + + public void addFederatedIdentity(UserFederatedIdentityEntity federatedIdentity) { + String idpId = federatedIdentity.getIdentityProvider(); + this.updated |= !Objects.equals(this.federatedIdentities.put(idpId, federatedIdentity), federatedIdentity); + } + + public UserFederatedIdentityEntity getFederatedIdentity(String federatedIdentity) { + return this.federatedIdentities.get(federatedIdentity); + } + + public boolean removeFederatedIdentity(String providerId) { + boolean removed = federatedIdentities.remove(providerId) != null; + this.updated |= removed; + return removed; + } + + public void updateFederatedIdentity(UserFederatedIdentityEntity federatedIdentityModel) { + this.updated |= federatedIdentities.replace(federatedIdentityModel.getIdentityProvider(), federatedIdentityModel) != null; + } + + public Stream getUserConsents() { + return userConsents.values().stream(); + } + + public UserConsentEntity getUserConsent(String clientId) { + return this.userConsents.get(clientId); + } + + + public void addUserConsent(UserConsentEntity userConsentEntity) { + String clientId = userConsentEntity.getClientId(); + this.updated |= !Objects.equals(this.userConsents.put(clientId, userConsentEntity), userConsentEntity); + } + + public boolean removeUserConsent(String clientId) { + boolean removed = userConsents.remove(clientId) != null; + this.updated |= removed; + return removed; + } + + public Set getGroupsMembership() { + return groupsMembership; + } + + public void setGroupsMembership(Set groupsMembership) { + this.updated |= Objects.equals(groupsMembership, this.groupsMembership); + this.groupsMembership = groupsMembership; + } + + public void addGroupsMembership(String groupId) { + this.updated |= this.groupsMembership.add(groupId); + } + + public void removeGroupsMembership(String groupId) { + this.updated |= this.groupsMembership.remove(groupId); + } + + public Set getRolesMembership() { + return rolesMembership; + } + + public void setRolesMembership(Set rolesMembership) { + this.updated |= Objects.equals(rolesMembership, this.rolesMembership); + this.rolesMembership = rolesMembership; + } + + public void addRolesMembership(String roleId) { + this.updated |= this.rolesMembership.add(roleId); + } + + public void removeRolesMembership(String roleId) { + this.updated |= this.rolesMembership.remove(roleId); + } + + public String getFederationLink() { + return federationLink; + } + + public void setFederationLink(String federationLink) { + this.updated |= !Objects.equals(this.federationLink, federationLink); + this.federationLink = federationLink; + } + + public String getServiceAccountClientLink() { + return serviceAccountClientLink; + } + + public void setServiceAccountClientLink(String serviceAccountClientLink) { + this.updated |= !Objects.equals(this.serviceAccountClientLink, serviceAccountClientLink); + this.serviceAccountClientLink = serviceAccountClientLink; + } + + public int getNotBefore() { + return notBefore; + } + + public void setNotBefore(int notBefore) { + this.updated |= !Objects.equals(this.notBefore, notBefore); + this.notBefore = notBefore; + } + +} diff --git a/model/map/src/main/java/org/keycloak/models/map/user/AbstractUserModel.java b/model/map/src/main/java/org/keycloak/models/map/user/AbstractUserModel.java new file mode 100644 index 0000000000..ea757d3259 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/user/AbstractUserModel.java @@ -0,0 +1,56 @@ +/* + * Copyright 2020 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. + */ + +package org.keycloak.models.map.user; + +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.map.common.AbstractEntity; + +import java.util.Objects; + +public abstract class AbstractUserModel implements UserModel.Streams { + + protected final KeycloakSession session; + protected final RealmModel realm; + protected final E entity; + + public AbstractUserModel(KeycloakSession session, RealmModel realm, E entity) { + Objects.requireNonNull(entity, "entity"); + Objects.requireNonNull(realm, "realm"); + + this.session = session; + this.realm = realm; + this.entity = entity; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof UserModel)) return false; + + UserModel that = (UserModel) o; + return Objects.equals(that.getId(), getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } +} 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 new file mode 100644 index 0000000000..48dab2a92c --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserAdapter.java @@ -0,0 +1,307 @@ +/* + * Copyright 2020 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. + */ + +package org.keycloak.models.map.user; + +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.models.ClientModel; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.RoleUtils; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + + +public abstract class MapUserAdapter extends AbstractUserModel { + public MapUserAdapter(KeycloakSession session, RealmModel realm, MapUserEntity entity) { + super(session, realm, entity); + } + + @Override + public String getId() { + return entity.getId().toString(); + } + + @Override + public String getUsername() { + return entity.getUsername(); + } + + @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; + + if (checkUsernameUniqueness(realm, username)) { + throw new ModelDuplicateException("A user with username " + username + " already exists"); + } + + entity.setUsername(username); + } + + @Override + public Long getCreatedTimestamp() { + return entity.getCreatedTimestamp(); + } + + @Override + public void setCreatedTimestamp(Long timestamp) { + entity.setCreatedTimestamp(timestamp); + } + + @Override + public boolean isEnabled() { + return entity.isEnabled(); + } + + @Override + public void setEnabled(boolean enabled) { + entity.setEnabled(enabled); + } + + private Optional getSpecialAttributeValue(String name) { + if (UserModel.FIRST_NAME.equals(name)) { + return Optional.ofNullable(entity.getFirstName()); + } else if (UserModel.LAST_NAME.equals(name)) { + return Optional.ofNullable(entity.getLastName()); + } else if (UserModel.EMAIL.equals(name)) { + return Optional.ofNullable(entity.getEmail()); + } else if (UserModel.USERNAME.equals(name)) { + return Optional.ofNullable(entity.getUsername()); + } + + return Optional.empty(); + } + + private boolean setSpecialAttributeValue(String name, String value) { + if (UserModel.FIRST_NAME.equals(name)) { + entity.setFirstName(value); + return true; + } else if (UserModel.LAST_NAME.equals(name)) { + entity.setLastName(value); + return true; + } else if (UserModel.EMAIL.equals(name)) { + setEmail(value); + return true; + } else if (UserModel.USERNAME.equals(name)) { + setUsername(value); + return true; + } + + return false; + } + + @Override + public void setSingleAttribute(String name, String value) { + if (setSpecialAttributeValue(name, value)) return; + if (value == null) { + entity.removeAttribute(name); + return; + } + entity.setAttribute(name, Collections.singletonList(value)); + } + + @Override + public void setAttribute(String name, List values) { + String valueToSet = (values != null && values.size() > 0) ? values.get(0) : null; + if (setSpecialAttributeValue(name, valueToSet)) return; + + entity.removeAttribute(name); + if (valueToSet == null) { + return; + } + + entity.setAttribute(name, values); + } + + @Override + public void removeAttribute(String name) { + entity.removeAttribute(name); + } + + @Override + public String getFirstAttribute(String name) { + return getSpecialAttributeValue(name) + .orElseGet(() -> entity.getAttribute(name).stream().findFirst() + .orElse(null)); + } + + @Override + public Stream getAttributeStream(String name) { + return getSpecialAttributeValue(name).map(Collections::singletonList) + .orElseGet(() -> entity.getAttribute(name)).stream(); + } + + @Override + public Map> getAttributes() { + MultivaluedHashMap result = new MultivaluedHashMap<>(entity.getAttributes()); + result.add(UserModel.FIRST_NAME, entity.getFirstName()); + result.add(UserModel.LAST_NAME, entity.getLastName()); + result.add(UserModel.EMAIL, entity.getEmail()); + result.add(UserModel.USERNAME, entity.getUsername()); + + return result; + } + + @Override + public Stream getRequiredActionsStream() { + return entity.getRequiredActions().stream(); + } + + @Override + public void addRequiredAction(String action) { + entity.addRequiredAction(action); + } + + @Override + public void removeRequiredAction(String action) { + entity.removeRequiredAction(action); + } + + @Override + public String getFirstName() { + return entity.getFirstName(); + } + + @Override + public void setFirstName(String firstName) { + entity.setFirstName(firstName); + } + + @Override + public String getLastName() { + return entity.getLastName(); + } + + @Override + public void setLastName(String lastName) { + entity.setLastName(lastName); + } + + @Override + public String getEmail() { + return entity.getEmail(); + } + + @Override + public void setEmail(String email) { + email = KeycloakModelUtils.toLowerCaseSafe(email); + if (email != null && email.equals(entity.getEmail())) return; + boolean duplicatesAllowed = realm.isDuplicateEmailsAllowed(); + + if (!duplicatesAllowed && email != null && checkEmailUniqueness(realm, email)) { + throw new ModelDuplicateException("A user with email " + email + " already exists"); + } + + entity.setEmail(email, duplicatesAllowed); + } + + public abstract boolean checkEmailUniqueness(RealmModel realm, String email); + public abstract boolean checkUsernameUniqueness(RealmModel realm, String username); + + @Override + public boolean isEmailVerified() { + return entity.isEmailVerified(); + } + + @Override + public void setEmailVerified(boolean verified) { + entity.setEmailVerified(verified); + } + + @Override + public Stream getGroupsStream() { + return session.groups().getGroupsStream(realm, entity.getGroupsMembership().stream()); + } + + @Override + public void joinGroup(GroupModel group) { + entity.addGroupsMembership(group.getId()); + } + + @Override + public void leaveGroup(GroupModel group) { + entity.removeGroupsMembership(group.getId()); + } + + @Override + public boolean isMemberOf(GroupModel group) { + return entity.getGroupsMembership().contains(group.getId()); + } + + @Override + public String getFederationLink() { + return entity.getFederationLink(); + } + + @Override + public void setFederationLink(String link) { + entity.setFederationLink(link); + } + + @Override + public String getServiceAccountClientLink() { + return entity.getServiceAccountClientLink(); + } + + @Override + public void setServiceAccountClientLink(String clientInternalId) { + entity.setServiceAccountClientLink(clientInternalId); + } + + + @Override + public Stream getRealmRoleMappingsStream() { + return getRoleMappingsStream().filter(RoleUtils::isRealmRole); + } + + @Override + public Stream getClientRoleMappingsStream(ClientModel app) { + return getRoleMappingsStream().filter(r -> RoleUtils.isClientRole(r, app)); + } + + @Override + public boolean hasRole(RoleModel role) { + return entity.getRolesMembership().contains(role.getId()); + } + + @Override + public void grantRole(RoleModel role) { + entity.addRolesMembership(role.getId()); + } + + @Override + public Stream getRoleMappingsStream() { + return entity.getRolesMembership().stream().map(realm::getRoleById); + } + + @Override + public void deleteRoleMapping(RoleModel role) { + entity.removeRolesMembership(role.getId()); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/user/MapUserEntity.java b/model/map/src/main/java/org/keycloak/models/map/user/MapUserEntity.java new file mode 100644 index 0000000000..5e3cec58db --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserEntity.java @@ -0,0 +1,34 @@ +/* + * Copyright 2020 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. + */ + +package org.keycloak.models.map.user; + +import java.util.Comparator; +import java.util.UUID; + +public class MapUserEntity extends AbstractUserEntity { + + public static final Comparator COMPARE_BY_USERNAME = Comparator.comparing(MapUserEntity::getUsername, String.CASE_INSENSITIVE_ORDER); + + protected MapUserEntity() { + super(); + } + + public MapUserEntity(UUID id, String realmId) { + super(id, realmId); + } +} 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 new file mode 100644 index 0000000000..628623f264 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java @@ -0,0 +1,891 @@ +/* + * Copyright 2020 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. + */ + +package org.keycloak.models.map.user; + +import org.apache.commons.lang.StringUtils; +import org.jboss.logging.Logger; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.store.ResourceStore; +import org.keycloak.common.util.Time; +import org.keycloak.component.ComponentModel; +import org.keycloak.credential.CredentialModel; +import org.keycloak.credential.UserCredentialStore; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientScopeModel; +import org.keycloak.models.FederatedIdentityModel; +import org.keycloak.models.GroupModel; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.ModelException; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RequiredActionProviderModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserConsentModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserProvider; +import org.keycloak.models.map.common.Serialization; +import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.utils.DefaultRoles; +import org.keycloak.storage.StorageId; +import org.keycloak.storage.UserStorageProvider; +import org.keycloak.storage.client.ClientStorageProvider; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.keycloak.common.util.StackUtil.getShortStackTrace; +import static org.keycloak.models.UserModel.EMAIL; +import static org.keycloak.models.UserModel.EMAIL_VERIFIED; +import static org.keycloak.models.UserModel.FIRST_NAME; +import static org.keycloak.models.UserModel.LAST_NAME; +import static org.keycloak.models.UserModel.USERNAME; + +public class MapUserProvider implements UserProvider.Streams, UserCredentialStore.Streams { + + private static final Logger LOG = Logger.getLogger(MapUserProvider.class); + private static final Predicate ALWAYS_FALSE = c -> { return false; }; + private final KeycloakSession session; + final MapKeycloakTransaction tx; + private final MapStorage userStore; + + public MapUserProvider(KeycloakSession session, MapStorage store) { + this.session = session; + this.userStore = store; + this.tx = new MapKeycloakTransaction<>(userStore); + session.getTransactionManager().enlist(tx); + } + + private MapUserEntity registerEntityForChanges(MapUserEntity origEntity) { + MapUserEntity res = tx.get(origEntity.getId(), id -> Serialization.from(origEntity)); + tx.putIfChanged(origEntity.getId(), res, MapUserEntity::isUpdated); + return res; + } + + 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, registerEntityForChanges(origEntity)) { + + @Override + public boolean checkEmailUniqueness(RealmModel realm, String email) { + return getUserByEmail(email, realm) != null; + } + + @Override + public boolean checkUsernameUniqueness(RealmModel realm, String username) { + return getUserByUsername(username, realm) != null; + } + }; + } + + private Predicate entityRealmFilter(RealmModel realm) { + if (realm == null || realm.getId() == null) { + return MapUserProvider.ALWAYS_FALSE; + } + String realmId = realm.getId(); + return entity -> Objects.equals(realmId, entity.getRealmId()); + } + + private ModelException userDoesntExistException() { + return new ModelException("Specified user doesn't exist."); + } + + private Optional getEntityById(RealmModel realm, String id) { + try { + return getEntityById(realm, UUID.fromString(id)); + } catch (IllegalArgumentException ex) { + return Optional.empty(); + } + } + + private MapUserEntity getRegisteredEntityByIdOrThrow(RealmModel realm, String id) { + return getEntityById(realm, id) + .map(this::registerEntityForChanges) + .orElseThrow(this::userDoesntExistException); + } + + private Optional getEntityById(RealmModel realm, UUID id) { + MapUserEntity mapUserEntity = tx.get(id, userStore::get); + if (mapUserEntity != null && entityRealmFilter(realm).test(mapUserEntity)) { + return Optional.of(mapUserEntity); + } + + return Optional.empty(); + } + + private Optional getRegisteredEntityById(RealmModel realm, String id) { + return getEntityById(realm, id).map(this::registerEntityForChanges); + } + + private Stream getNotRemovedUpdatedUsersStream() { + Stream updatedAndNotRemovedUsersStream = userStore.entrySet().stream() + .map(tx::getUpdated) // If the group has been removed, tx.get will return null, otherwise it will return me.getValue() + .filter(Objects::nonNull); + return Stream.concat(tx.createdValuesStream(), updatedAndNotRemovedUsersStream); + } + + private Stream getUnsortedUserEntitiesStream(RealmModel realm) { + return getNotRemovedUpdatedUsersStream() + .filter(entityRealmFilter(realm)); + } + + private Stream paginatedStream(Stream originalStream, Integer first, Integer max) { + if (first != null && first > 0) { + originalStream = originalStream.skip(first); + } + + if (max != null && max >= 0) { + originalStream = originalStream.limit(max); + } + + return originalStream; + } + + @Override + public void addFederatedIdentity(RealmModel realm, UserModel user, FederatedIdentityModel socialLink) { + if (user == null || user.getId() == null) { + return; + } + LOG.tracef("addFederatedIdentity(%s, %s, %s)%s", realm, user.getId(), socialLink.getIdentityProvider(), getShortStackTrace()); + + getRegisteredEntityById(realm, user.getId()) + .ifPresent(userEntity -> + userEntity.addFederatedIdentity(UserFederatedIdentityEntity.fromModel(socialLink))); + } + + @Override + public boolean removeFederatedIdentity(RealmModel realm, UserModel user, String socialProvider) { + LOG.tracef("removeFederatedIdentity(%s, %s, %s)%s", realm, user.getId(), socialProvider, getShortStackTrace()); + return getRegisteredEntityById(realm, user.getId()) + .map(entity -> entity.removeFederatedIdentity(socialProvider)) + .orElse(false); + } + + @Override + public void preRemove(RealmModel realm, IdentityProviderModel provider) { + String socialProvider = provider.getAlias(); + LOG.tracef("preRemove[RealmModel realm, IdentityProviderModel provider](%s, %s)%s", realm, socialProvider, getShortStackTrace()); + getUnsortedUserEntitiesStream(realm) + .map(this::registerEntityForChanges) + .forEach(userEntity -> userEntity.removeFederatedIdentity(socialProvider)); + } + + @Override + public void updateFederatedIdentity(RealmModel realm, UserModel federatedUser, FederatedIdentityModel federatedIdentityModel) { + LOG.tracef("updateFederatedIdentity(%s, %s, %s)%s", realm, federatedUser.getId(), federatedIdentityModel.getIdentityProvider(), getShortStackTrace()); + getRegisteredEntityById(realm, federatedUser.getId()) + .ifPresent(entity -> entity.updateFederatedIdentity(UserFederatedIdentityEntity.fromModel(federatedIdentityModel))); + } + + @Override + public Stream getFederatedIdentitiesStream(UserModel user, RealmModel realm) { + LOG.tracef("getFederatedIdentitiesStream(%s, %s)%s", realm, user.getId(), getShortStackTrace()); + return getEntityById(realm, user.getId()) + .map(AbstractUserEntity::getFederatedIdentities).orElseGet(Stream::empty) + .map(UserFederatedIdentityEntity::toModel); + } + + @Override + public FederatedIdentityModel getFederatedIdentity(UserModel user, String socialProvider, RealmModel realm) { + LOG.tracef("getFederatedIdentity(%s, %s, %s)%s", realm, user.getId(), socialProvider, getShortStackTrace()); + return getEntityById(realm, user.getId()) + .map(userEntity -> userEntity.getFederatedIdentity(socialProvider)) + .map(UserFederatedIdentityEntity::toModel) + .orElse(null); + } + + @Override + public UserModel getUserByFederatedIdentity(FederatedIdentityModel socialLink, RealmModel realm) { + LOG.tracef("getUserByFederatedIdentity(%s, %s)%s", realm, socialLink, getShortStackTrace()); + return getUnsortedUserEntitiesStream(realm) + .filter(userEntity -> Objects.nonNull(userEntity.getFederatedIdentity(socialLink.getIdentityProvider()))) + .filter(userEntity -> Objects.equals(userEntity.getFederatedIdentity(socialLink.getIdentityProvider()).getUserId(), socialLink.getUserId())) + .collect(Collectors.collectingAndThen( + Collectors.toList(), + list -> { + if (list.size() == 0) { + return null; + } else if (list.size() != 1) { + throw new IllegalStateException("More results found for identityProvider=" + socialLink.getIdentityProvider() + + ", userId=" + socialLink.getUserId() + ", results=" + list); + } + + return entityToAdapterFunc(realm).apply(list.get(0)); + })); + } + + @Override + public void addConsent(RealmModel realm, String userId, UserConsentModel consent) { + LOG.tracef("addConsent(%s, %s, %s)%s", realm, userId, consent, getShortStackTrace()); + + UserConsentEntity consentEntity = UserConsentEntity.fromModel(consent); + getRegisteredEntityById(realm, userId).ifPresent(userEntity -> userEntity.addUserConsent(consentEntity)); + } + + @Override + public UserConsentModel getConsentByClient(RealmModel realm, String userId, String clientInternalId) { + LOG.tracef("getConsentByClient(%s, %s, %s)%s", realm, userId, clientInternalId, getShortStackTrace()); + return getEntityById(realm, userId) + .map(userEntity -> userEntity.getUserConsent(clientInternalId)) + .map(consent -> UserConsentEntity.toModel(realm, consent)) + .orElse(null); + } + + @Override + public Stream getConsentsStream(RealmModel realm, String userId) { + LOG.tracef("getConsentByClientStream(%s, %s)%s", realm, userId, getShortStackTrace()); + return getEntityById(realm, userId) + .map(AbstractUserEntity::getUserConsents) + .orElse(Stream.empty()) + .map(consent -> UserConsentEntity.toModel(realm, consent)); + } + + @Override + public void updateConsent(RealmModel realm, String userId, UserConsentModel consent) { + LOG.tracef("updateConsent(%s, %s, %s)%s", realm, userId, consent, getShortStackTrace()); + + MapUserEntity user = getRegisteredEntityByIdOrThrow(realm, userId); + UserConsentEntity userConsentEntity = user.getUserConsent(consent.getClient().getId()); + if (userConsentEntity == null) { + throw new ModelException("Consent not found for client [" + consent.getClient().getId() + "] and user [" + userId + "]"); + } + + userConsentEntity.setGrantedClientScopesIds( + consent.getGrantedClientScopes().stream() + .map(ClientScopeModel::getId) + .collect(Collectors.toSet()) + ); + + userConsentEntity.setLastUpdatedDate(Time.currentTimeMillis()); + } + + @Override + public boolean revokeConsentForClient(RealmModel realm, String userId, String clientInternalId) { + LOG.tracef("revokeConsentForClient(%s, %s, %s)%s", realm, userId, clientInternalId, getShortStackTrace()); + return getRegisteredEntityById(realm, userId) + .map(userEntity -> userEntity.removeUserConsent(clientInternalId)) + .orElse(false); + } + + @Override + public void setNotBeforeForUser(RealmModel realm, UserModel user, int notBefore) { + LOG.tracef("setNotBeforeForUser(%s, %s, %d)%s", realm, user.getId(), notBefore, getShortStackTrace()); + getRegisteredEntityById(realm, user.getId()).ifPresent(userEntity -> userEntity.setNotBefore(notBefore)); + } + + @Override + public int getNotBeforeOfUser(RealmModel realm, UserModel user) { + LOG.tracef("getNotBeforeOfUser(%s, %s)%s", realm, user.getId(), getShortStackTrace()); + return getEntityById(realm, user.getId()) + .map(AbstractUserEntity::getNotBefore) + .orElse(0); + } + + @Override + public UserModel getServiceAccount(ClientModel client) { + LOG.tracef("getServiceAccount(%s)%s", client.getId(), getShortStackTrace()); + return getUnsortedUserEntitiesStream(client.getRealm()) + .filter(userEntity -> Objects.equals(userEntity.getServiceAccountClientLink(), client.getId())) + .collect(Collectors.collectingAndThen( + Collectors.toList(), + list -> { + if (list.size() == 0) { + return null; + } else if (list.size() != 1) { + throw new IllegalStateException("More service account linked users found for client=" + client.getClientId() + + ", results=" + list); + } + + return entityToAdapterFunc(client.getRealm()).apply(list.get(0)); + } + )); + } + + @Override + public UserModel addUser(RealmModel realm, String id, String username, boolean addDefaultRoles, boolean addDefaultRequiredActions) { + LOG.tracef("addUser(%s, %s, %s, %s, %s)%s", realm, id, username, addDefaultRoles, addDefaultRequiredActions, getShortStackTrace()); + if (getUnsortedUserEntitiesStream(realm) + .anyMatch(userEntity -> Objects.equals(userEntity.getUsername(), username))) { + throw new ModelDuplicateException("User with username '" + username + "' in realm " + realm.getName() + " already exists" ); + } + + final UUID entityId = id == null ? UUID.randomUUID() : UUID.fromString(id); + + if (tx.get(entityId, userStore::get) != null) { + throw new ModelDuplicateException("User exists: " + entityId); + } + + MapUserEntity entity = new MapUserEntity(entityId, realm.getId()); + entity.setUsername(username.toLowerCase()); + entity.setCreatedTimestamp(Time.currentTimeMillis()); + + tx.putIfAbsent(entityId, entity); + final UserModel userModel = entityToAdapterFunc(realm).apply(entity); + + if (addDefaultRoles) { + DefaultRoles.addDefaultRoles(realm, userModel); + + // No need to check if user has group as it's new user + realm.getDefaultGroupsStream().forEach(userModel::joinGroup); + } + + if (addDefaultRequiredActions){ + realm.getRequiredActionProvidersStream() + .filter(RequiredActionProviderModel::isEnabled) + .filter(RequiredActionProviderModel::isDefaultAction) + .map(RequiredActionProviderModel::getAlias) + .forEach(userModel::addRequiredAction); + } + + return userModel; + } + + @Override + public void preRemove(RealmModel realm) { + LOG.tracef("preRemove[RealmModel](%s)%s", realm, getShortStackTrace()); + getUnsortedUserEntitiesStream(realm) + .map(MapUserEntity::getId) + .forEach(tx::remove); + } + + @Override + public void removeImportedUsers(RealmModel realm, String storageProviderId) { + LOG.tracef("removeImportedUsers(%s, %s)%s", realm, storageProviderId, getShortStackTrace()); + getUnsortedUserEntitiesStream(realm) + .filter(userEntity -> Objects.equals(userEntity.getFederationLink(), storageProviderId)) + .map(MapUserEntity::getId) + .forEach(tx::remove); + } + + @Override + public void unlinkUsers(RealmModel realm, String storageProviderId) { + LOG.tracef("unlinkUsers(%s, %s)%s", realm, storageProviderId, getShortStackTrace()); + getUnsortedUserEntitiesStream(realm) + .filter(userEntity -> Objects.equals(userEntity.getFederationLink(), storageProviderId)) + .map(this::registerEntityForChanges) + .forEach(userEntity -> userEntity.setFederationLink(null)); + } + + @Override + public void preRemove(RealmModel realm, RoleModel role) { + String roleId = role.getId(); + LOG.tracef("preRemove[RoleModel](%s, %s)%s", realm, roleId, getShortStackTrace()); + getUnsortedUserEntitiesStream(realm) + .filter(userEntity -> userEntity.getRolesMembership().contains(roleId)) + .map(this::registerEntityForChanges) + .forEach(userEntity -> userEntity.removeRolesMembership(roleId)); + } + + @Override + public void preRemove(RealmModel realm, GroupModel group) { + String groupId = group.getId(); + LOG.tracef("preRemove[GroupModel](%s, %s)%s", realm, groupId, getShortStackTrace()); + getUnsortedUserEntitiesStream(realm) + .filter(userEntity -> userEntity.getGroupsMembership().contains(groupId)) + .map(this::registerEntityForChanges) + .forEach(userEntity -> userEntity.removeGroupsMembership(groupId)); + } + + @Override + public void preRemove(RealmModel realm, ClientModel client) { + String clientId = client.getId(); + LOG.tracef("preRemove[ClientModel](%s, %s)%s", realm, clientId, getShortStackTrace()); + getUnsortedUserEntitiesStream(realm) + .filter(userEntity -> Objects.nonNull(userEntity.getUserConsent(clientId))) + .map(this::registerEntityForChanges) + .forEach(userEntity -> userEntity.removeUserConsent(clientId)); + } + + @Override + public void preRemove(ProtocolMapperModel protocolMapper) { + // No-op + } + + @Override + public void preRemove(ClientScopeModel clientScope) { + String clientScopeId = clientScope.getId(); + LOG.tracef("preRemove[ClientScopeModel](%s)%s", clientScopeId, getShortStackTrace()); + + getUnsortedUserEntitiesStream(clientScope.getRealm()) + .map(this::registerEntityForChanges) + .flatMap(AbstractUserEntity::getUserConsents) + .forEach(consent -> consent.removeGrantedClientScopesIds(clientScopeId)); + } + + @Override + public void preRemove(RealmModel realm, ComponentModel component) { + String componentId = component.getId(); + LOG.tracef("preRemove[ComponentModel](%s, %s)%s", realm, componentId, getShortStackTrace()); + if (component.getProviderType().equals(UserStorageProvider.class.getName())) { + removeImportedUsers(realm, componentId); + } + if (component.getProviderType().equals(ClientStorageProvider.class.getName())) { + getUnsortedUserEntitiesStream(realm) + .forEach(removeConsentsForExternalClient(componentId)); + } + } + + private Consumer removeConsentsForExternalClient(String componentId) { + return userEntity -> { + List consentModels = userEntity.getUserConsents() + .filter(consent -> + Objects.equals(new StorageId(consent.getClientId()).getProviderId(), componentId)) + .collect(Collectors.toList()); + + if (consentModels.size() > 0) { + userEntity = registerEntityForChanges(userEntity); + for (UserConsentEntity consentEntity : consentModels) { + userEntity.removeUserConsent(consentEntity.getClientId()); + } + } + }; + } + + @Override + public void grantToAllUsers(RealmModel realm, RoleModel role) { + String roleId = role.getId(); + LOG.tracef("grantToAllUsers(%s, %s)%s", realm, roleId, getShortStackTrace()); + getUnsortedUserEntitiesStream(realm) + .map(this::registerEntityForChanges) + .forEach(entity -> entity.addRolesMembership(roleId)); + } + + @Override + public UserModel getUserById(String id, RealmModel realm) { + LOG.tracef("getUserById(%s, %s)%s", realm, id, getShortStackTrace()); + return getEntityById(realm, id).map(entityToAdapterFunc(realm)).orElse(null); + } + + @Override + public UserModel getUserByUsername(String username, RealmModel realm) { + if (username == null) return null; + final String usernameLowercase = username.toLowerCase(); + + LOG.tracef("getUserByUsername(%s, %s)%s", realm, username, getShortStackTrace()); + return getUnsortedUserEntitiesStream(realm) + .filter(userEntity -> Objects.equals(userEntity.getUsername(), usernameLowercase)) + .findFirst() + .map(entityToAdapterFunc(realm)).orElse(null); + } + + @Override + public UserModel getUserByEmail(String email, RealmModel realm) { + LOG.tracef("getUserByEmail(%s, %s)%s", realm, email, getShortStackTrace()); + List usersWithEmail = getUnsortedUserEntitiesStream(realm) + .filter(userEntity -> Objects.equals(userEntity.getEmail(), email)) + .collect(Collectors.toList()); + if (usersWithEmail.size() == 0) 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. + throw new ModelDuplicateException("Multiple users with email '" + email + "' exist in Keycloak."); + } + + MapUserEntity userEntity = registerEntityForChanges(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. + // We need to update the email constraint to reflect this change in the user entities. + userEntity.setEmailConstraint(userEntity.getEmail()); + } + } + + return new MapUserAdapter(session, realm, userEntity) { + @Override + public boolean checkEmailUniqueness(RealmModel realm, String email) { + return getUserByEmail(email, realm) != null; + } + @Override + public boolean checkUsernameUniqueness(RealmModel realm, String username) { + return getUserByUsername(username, realm) != null; + } + }; + } + + @Override + public int getUsersCount(RealmModel realm) { + LOG.tracef("getUsersCount(%s)%s", realm, getShortStackTrace()); + return getUsersCount(realm, false); + } + + @Override + public int getUsersCount(RealmModel realm, boolean includeServiceAccount) { + LOG.tracef("getUsersCount(%s, %s)%s", realm, includeServiceAccount, getShortStackTrace()); + Stream unsortedUserEntitiesStream = getUnsortedUserEntitiesStream(realm); + + if (!includeServiceAccount) { + unsortedUserEntitiesStream = unsortedUserEntitiesStream + .filter(userEntity -> Objects.isNull(userEntity.getServiceAccountClientLink())); + } + + return (int) unsortedUserEntitiesStream.count(); + } + + @Override + public Stream getUsersStream(RealmModel realm, Integer firstResult, Integer maxResults, boolean includeServiceAccounts) { + LOG.tracef("getUsersStream(%s, %d, %d, %s)%s", realm, firstResult, maxResults, includeServiceAccounts, getShortStackTrace()); + Stream usersStream = getUnsortedUserEntitiesStream(realm); + if (!includeServiceAccounts) { + usersStream = usersStream.filter(userEntity -> Objects.isNull(userEntity.getServiceAccountClientLink())); + } + + return paginatedStream(usersStream.sorted(MapUserEntity.COMPARE_BY_USERNAME), firstResult, maxResults) + .map(entityToAdapterFunc(realm)); + } + + @Override + public Stream getUsersStream(RealmModel realm) { + LOG.tracef("getUsersStream(%s)%s", realm, getShortStackTrace()); + return getUsersStream(realm, null, null, false); + } + + @Override + public Stream getUsersStream(RealmModel realm, boolean includeServiceAccounts) { + LOG.tracef("getUsersStream(%s)%s", realm, getShortStackTrace()); + return getUsersStream(realm, null, null, includeServiceAccounts); + } + + @Override + public Stream getUsersStream(RealmModel realm, int firstResult, int maxResults) { + LOG.tracef("getUsersStream(%s, %d, %d)%s", realm, firstResult, maxResults, getShortStackTrace()); + return getUsersStream(realm, firstResult, maxResults, false); + } + + @Override + public Stream searchForUserStream(String search, RealmModel realm) { + LOG.tracef("searchForUserStream(%s, %s)%s", realm, search, getShortStackTrace()); + return searchForUserStream(search, realm, null, null); + } + + @Override + public Stream searchForUserStream(String search, RealmModel realm, Integer firstResult, Integer maxResults) { + LOG.tracef("searchForUserStream(%s, %s, %d, %d)%s", realm, search, firstResult, maxResults, getShortStackTrace()); + Map attributes = new HashMap<>(); + attributes.put(UserModel.SEARCH, search); + session.setAttribute(UserModel.INCLUDE_SERVICE_ACCOUNT, false); + return searchForUserStream(attributes, realm, firstResult, maxResults); + } + + @Override + public Stream searchForUserStream(Map params, RealmModel realm) { + LOG.tracef("searchForUserStream(%s, %s)%s", realm, params, getShortStackTrace()); + return searchForUserStream(params, realm, null, null); + } + + @Override + public Stream searchForUserStream(Map attributes, RealmModel realm, Integer firstResult, Integer maxResults) { + LOG.tracef("searchForUserStream(%s, %s, %d, %d)%s", realm, attributes, firstResult, maxResults, getShortStackTrace()); + /* Find all predicates based on attributes map */ + List> predicatesList = new ArrayList<>(); + + if (!session.getAttributeOrDefault(UserModel.INCLUDE_SERVICE_ACCOUNT, true)) { + predicatesList.add(userEntity -> Objects.isNull(userEntity.getServiceAccountClientLink())); + } + + final boolean exactSearch = Boolean.parseBoolean(attributes.getOrDefault(UserModel.EXACT, Boolean.FALSE.toString())); + + for (Map.Entry entry : attributes.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (value == null) { + continue; + } + + final String searchedString = value.toLowerCase(); + Function, Predicate> containsOrExactPredicate = + func -> { + return userEntity -> testContainsOrExact(func.apply(userEntity), searchedString, exactSearch); + }; + + switch (key) { + case UserModel.SEARCH: + List> orPredicates = new ArrayList<>(); + orPredicates.add(userEntity -> StringUtils.containsIgnoreCase(userEntity.getUsername(), searchedString)); + orPredicates.add(userEntity -> StringUtils.containsIgnoreCase(userEntity.getEmail(), searchedString)); + orPredicates.add(userEntity -> StringUtils.containsIgnoreCase(concatFirstNameLastName(userEntity), searchedString)); + + predicatesList.add(orPredicates.stream().reduce(Predicate::or).orElse(t -> false)); + break; + + case USERNAME: + predicatesList.add(containsOrExactPredicate.apply(MapUserEntity::getUsername)); + break; + case FIRST_NAME: + predicatesList.add(containsOrExactPredicate.apply(MapUserEntity::getFirstName)); + break; + case LAST_NAME: + predicatesList.add(containsOrExactPredicate.apply(MapUserEntity::getLastName)); + break; + case EMAIL: + predicatesList.add(containsOrExactPredicate.apply(MapUserEntity::getEmail)); + break; + case EMAIL_VERIFIED: { + boolean booleanValue = Boolean.parseBoolean(searchedString); + predicatesList.add(userEntity -> Objects.equals(userEntity.isEmailVerified(), booleanValue)); + break; + } + case UserModel.ENABLED: { + boolean booleanValue = Boolean.parseBoolean(searchedString); + predicatesList.add(userEntity -> Objects.equals(userEntity.isEnabled(), booleanValue)); + break; + } + case UserModel.IDP_ALIAS: { + predicatesList.add(mapUserEntity -> Objects.nonNull(mapUserEntity.getFederatedIdentity(value))); + break; + } + case UserModel.IDP_USER_ID: { + predicatesList.add(mapUserEntity -> mapUserEntity.getFederatedIdentities() + .anyMatch(idp -> Objects.equals(idp.getUserId(), value))); + break; + } + } + } + + @SuppressWarnings("unchecked") + Set userGroups = (Set) session.getAttribute(UserModel.GROUPS); + if (userGroups != null && userGroups.size() > 0) { + final ResourceStore resourceStore = session.getProvider(AuthorizationProvider.class).getStoreFactory() + .getResourceStore(); + final Predicate resourceByGroupIdExists = id -> resourceStore + .findByResourceServer(Collections.singletonMap("name", new String[] { "group.resource." + id }), + null, 0, 1).size() == 1; + + predicatesList.add(userEntity -> { + return userEntity.getGroupsMembership() + .stream() + .filter(userGroups::contains) + .anyMatch(resourceByGroupIdExists); + }); + } + + // Prepare resulting predicate + Predicate resultingPredicate = predicatesList.stream() + .reduce(Predicate::and) // Combine all predicates with and + .orElse(t -> true); // If there is no predicate in predicatesList, return all users + + Stream usersStream = getUnsortedUserEntitiesStream(realm) // Get stream of all users in the realm + .filter(resultingPredicate) // Apply all predicates to userStream + .sorted(AbstractUserEntity.COMPARE_BY_USERNAME); // Sort before paginating + + return paginatedStream(usersStream, firstResult, maxResults) // paginate if necessary + .map(entityToAdapterFunc(realm)) + .filter(Objects::nonNull); + } + + private String concatFirstNameLastName(MapUserEntity entity) { + StringBuilder stringBuilder = new StringBuilder(); + if (entity.getFirstName() != null) { + stringBuilder.append(entity.getFirstName()); + } + + stringBuilder.append(" "); + + if (entity.getLastName() != null) { + stringBuilder.append(entity.getLastName()); + } + + return stringBuilder.toString(); + } + + private boolean testContainsOrExact(String testedString, String searchedString, boolean exactMatch) { + if (exactMatch) { + return StringUtils.equalsIgnoreCase(testedString, searchedString); + } else { + return StringUtils.containsIgnoreCase(testedString, searchedString); + } + } + + @Override + public Stream getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults) { + LOG.tracef("getGroupMembersStream(%s, %s, %d, %d)%s", realm, group.getId(), firstResult, maxResults, getShortStackTrace()); + return paginatedStream(getUnsortedUserEntitiesStream(realm) + .filter(userEntity -> userEntity.getGroupsMembership().contains(group.getId())) + .sorted(MapUserEntity.COMPARE_BY_USERNAME), firstResult, maxResults) + .map(entityToAdapterFunc(realm)); + } + + @Override + public Stream getGroupMembersStream(RealmModel realm, GroupModel group) { + LOG.tracef("getGroupMembersStream(%s, %s)%s", realm, group.getId(), getShortStackTrace()); + return getGroupMembersStream(realm, group, null, null); + } + + @Override + public Stream searchForUserByUserAttributeStream(String attrName, String attrValue, RealmModel realm) { + LOG.tracef("searchForUserByUserAttributeStream(%s, %s, %s)%s", realm, attrName, attrValue, getShortStackTrace()); + return getUnsortedUserEntitiesStream(realm) + .filter(userEntity -> userEntity.getAttribute(attrName).contains(attrValue)) + .map(entityToAdapterFunc(realm)) + .sorted(UserModel.COMPARE_BY_USERNAME); + } + + @Override + public UserModel addUser(RealmModel realm, String username) { + return addUser(realm, null, username.toLowerCase(), true, true); + } + + @Override + public boolean removeUser(RealmModel realm, UserModel user) { + String userId = user.getId(); + Optional userById = getEntityById(realm, userId); + if (userById.isPresent()) { + tx.remove(UUID.fromString(userId)); + return true; + } + + return false; + } + + @Override + public Stream getRoleMembersStream(RealmModel realm, RoleModel role, Integer firstResult, Integer maxResults) { + LOG.tracef("getRoleMembersStream(%s, %s, %d, %d)%s", realm, role, firstResult, maxResults, getShortStackTrace()); + return paginatedStream(getUnsortedUserEntitiesStream(realm) + .filter(entity -> entity.getRolesMembership().contains(role.getId())) + .sorted(MapUserEntity.COMPARE_BY_USERNAME), firstResult, maxResults) + .map(entityToAdapterFunc(realm)); + } + + @Override + public void updateCredential(RealmModel realm, UserModel user, CredentialModel cred) { + getRegisteredEntityById(realm, user.getId()) + .ifPresent(updateCredential(cred)); + } + + private Consumer updateCredential(CredentialModel credentialModel) { + return user -> { + UserCredentialEntity credentialEntity = user.getCredential(credentialModel.getId()); + if (credentialEntity == null) return; + + credentialEntity.setCreatedDate(credentialModel.getCreatedDate()); + credentialEntity.setUserLabel(credentialModel.getUserLabel()); + credentialEntity.setType(credentialModel.getType()); + credentialEntity.setSecretData(credentialModel.getSecretData()); + credentialEntity.setCredentialData(credentialModel.getCredentialData()); + }; + } + + @Override + public CredentialModel createCredential(RealmModel realm, UserModel user, CredentialModel cred) { + LOG.tracef("createCredential(%s, %s, %s)%s", realm, user.getId(), cred.getId(), getShortStackTrace()); + UserCredentialEntity credentialEntity = UserCredentialEntity.fromModel(cred); + + getRegisteredEntityByIdOrThrow(realm, user.getId()) + .addCredential(credentialEntity); + + return UserCredentialEntity.toModel(credentialEntity); + } + + @Override + public boolean removeStoredCredential(RealmModel realm, UserModel user, String id) { + LOG.tracef("removeStoredCredential(%s, %s, %s)%s", realm, user.getId(), id, getShortStackTrace()); + return getRegisteredEntityById(realm, user.getId()) + .map(mapUserEntity -> mapUserEntity.removeCredential(id)) + .orElse(false); + } + + @Override + public CredentialModel getStoredCredentialById(RealmModel realm, UserModel user, String id) { + LOG.tracef("getStoredCredentialById(%s, %s, %s)%s", realm, user.getId(), id, getShortStackTrace()); + return getEntityById(realm, user.getId()) + .map(mapUserEntity -> mapUserEntity.getCredential(id)) + .map(UserCredentialEntity::toModel) + .orElse(null); + } + + @Override + public Stream getStoredCredentialsStream(RealmModel realm, UserModel user) { + LOG.tracef("getStoredCredentialsStream(%s, %s)%s", realm, user.getId(), getShortStackTrace()); + return getEntityById(realm, user.getId()) + .map(AbstractUserEntity::getCredentials) + .orElseGet(Stream::empty) + .map(UserCredentialEntity::toModel); + } + + @Override + public Stream getStoredCredentialsByTypeStream(RealmModel realm, UserModel user, String type) { + LOG.tracef("getStoredCredentialsByTypeStream(%s, %s, %s)%s", realm, user.getId(), type, getShortStackTrace()); + return getStoredCredentialsStream(realm, user) + .filter(credential -> Objects.equals(type, credential.getType())); + } + + @Override + public CredentialModel getStoredCredentialByNameAndType(RealmModel realm, UserModel user, String name, String type) { + LOG.tracef("getStoredCredentialByNameAndType(%s, %s, %s, %s)%s", realm, user.getId(), name, type, getShortStackTrace()); + return getStoredCredentialsByType(realm, user, type).stream() + .filter(credential -> Objects.equals(name, credential.getUserLabel())) + .findFirst().orElse(null); + } + + @Override + public boolean moveCredentialTo(RealmModel realm, UserModel user, String id, String newPreviousCredentialId) { + LOG.tracef("moveCredentialTo(%s, %s, %s, %s)%s", realm, user.getId(), id, newPreviousCredentialId, getShortStackTrace()); + String userId = user.getId(); + MapUserEntity userEntity = getRegisteredEntityById(realm, userId).orElse(null); + if (userEntity == null) { + LOG.warnf("User with id: [%s] not found", userId); + return false; + } + + // Find index of credential which should be before id in the list + int newPreviousCredentialIdIndex = -1; // If newPreviousCredentialId == null we need to put id credential to index 0 + if (newPreviousCredentialId != null) { + newPreviousCredentialIdIndex = userEntity.getCredentialIndex(newPreviousCredentialId); + if (newPreviousCredentialIdIndex == -1) { // If not null previous credential not found, print warning and return false + LOG.warnf("Credential with id: [%s] for user: [%s] not found", newPreviousCredentialId, userId); + return false; + } + } + + // Find current index of credential (id) which will be moved + int currentPositionOfId = userEntity.getCredentialIndex(id); + if (currentPositionOfId == -1) { + LOG.warnf("Credential with id: [%s] for user: [%s] not found", id, userId); + return false; + } + + // If id is before newPreviousCredentialId in priority list, it will be moved to position -1 + if (currentPositionOfId < newPreviousCredentialIdIndex) { + newPreviousCredentialIdIndex -= 1; + } + + // Move credential to desired index + userEntity.moveCredential(currentPositionOfId, newPreviousCredentialIdIndex + 1); + return true; + } + + @Override + public void close() { + + } +} 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 new file mode 100644 index 0000000000..930533064c --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserProviderFactory.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020 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. + */ + +package org.keycloak.models.map.user; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.UserProvider; +import org.keycloak.models.UserProviderFactory; +import org.keycloak.models.map.common.AbstractMapProviderFactory; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.map.storage.MapStorageProvider; + +import java.util.UUID; + +/** + * + * @author mhajas + */ +public class MapUserProviderFactory extends AbstractMapProviderFactory implements UserProviderFactory { + + private MapStorage store; + + @Override + public void postInit(KeycloakSessionFactory factory) { + MapStorageProvider sp = (MapStorageProvider) factory.getProviderFactory(MapStorageProvider.class); + this.store = sp.getStorage("users", UUID.class, MapUserEntity.class); + } + + + @Override + public UserProvider create(KeycloakSession session) { + return new MapUserProvider(session, store); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/user/UserConsentEntity.java b/model/map/src/main/java/org/keycloak/models/map/user/UserConsentEntity.java new file mode 100644 index 0000000000..08be0930b5 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/user/UserConsentEntity.java @@ -0,0 +1,128 @@ +/* + * Copyright 2020 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. + */ + +package org.keycloak.models.map.user; + +import org.keycloak.common.util.Time; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientScopeModel; +import org.keycloak.models.ModelException; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserConsentModel; +import org.keycloak.models.utils.KeycloakModelUtils; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + + +public class UserConsentEntity { + + private String clientId; + private final Set grantedClientScopesIds = new HashSet<>(); + private Long createdDate; + private Long lastUpdatedDate; + private boolean updated; + + private UserConsentEntity() {} + + public static UserConsentEntity fromModel(UserConsentModel model) { + long currentTime = Time.currentTimeMillis(); + + UserConsentEntity consentEntity = new UserConsentEntity(); + consentEntity.setClientId(model.getClient().getId()); + consentEntity.setCreatedDate(currentTime); + consentEntity.setLastUpdatedDate(currentTime); + + model.getGrantedClientScopes() + .stream() + .map(ClientScopeModel::getId) + .forEach(consentEntity::addGrantedClientScopeId); + + return consentEntity; + } + + public static UserConsentModel toModel(RealmModel realm, UserConsentEntity entity) { + if (entity == null) { + return null; + } + + ClientModel client = realm.getClientById(entity.getClientId()); + if (client == null) { + throw new ModelException("Client with id " + entity.getClientId() + " is not available"); + } + UserConsentModel model = new UserConsentModel(client); + model.setCreatedDate(entity.getCreatedDate()); + model.setLastUpdatedDate(entity.getLastUpdatedDate()); + + entity.getGrantedClientScopesIds().stream() + .map(scopeId -> KeycloakModelUtils.findClientScopeById(realm, client, scopeId)) + .filter(Objects::nonNull) + .forEach(model::addGrantedClientScope); + + return model; + } + + public boolean isUpdated() { + return updated; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.updated = !Objects.equals(this.clientId, clientId); + this.clientId = clientId; + } + + public Set getGrantedClientScopesIds() { + return grantedClientScopesIds; + } + + public void addGrantedClientScopeId(String scope) { + this.updated |= grantedClientScopesIds.add(scope); + } + + public void setGrantedClientScopesIds(Set scopesIds) { + this.updated |= !Objects.equals(grantedClientScopesIds, scopesIds); + this.grantedClientScopesIds.clear(); + this.grantedClientScopesIds.addAll(scopesIds); + } + + public void removeGrantedClientScopesIds(String scopesId) { + this.updated |= this.grantedClientScopesIds.remove(scopesId); + } + + public Long getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(Long createdDate) { + this.updated |= !Objects.equals(this.createdDate, createdDate); + this.createdDate = createdDate; + } + + public Long getLastUpdatedDate() { + return lastUpdatedDate; + } + + public void setLastUpdatedDate(Long lastUpdatedDate) { + this.updated |= !Objects.equals(this.lastUpdatedDate, lastUpdatedDate); + this.lastUpdatedDate = lastUpdatedDate; + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/user/UserCredentialEntity.java b/model/map/src/main/java/org/keycloak/models/map/user/UserCredentialEntity.java new file mode 100644 index 0000000000..c296cc1497 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/user/UserCredentialEntity.java @@ -0,0 +1,118 @@ +/* + * Copyright 2020 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. + */ + +package org.keycloak.models.map.user; + +import org.keycloak.credential.CredentialModel; +import org.keycloak.models.utils.KeycloakModelUtils; + +import java.util.Objects; + +public class UserCredentialEntity { + + private String id; + private String type; + private String userLabel; + private Long createdDate; + private String secretData; + private String credentialData; + private boolean updated; + + UserCredentialEntity() {} + + public static UserCredentialEntity fromModel(CredentialModel model) { + UserCredentialEntity credentialEntity = new UserCredentialEntity(); + String id = model.getId() == null ? KeycloakModelUtils.generateId() : model.getId(); + credentialEntity.setId(id); + credentialEntity.setCreatedDate(model.getCreatedDate()); + credentialEntity.setUserLabel(model.getUserLabel()); + credentialEntity.setType(model.getType()); + credentialEntity.setSecretData(model.getSecretData()); + credentialEntity.setCredentialData(model.getCredentialData()); + + return credentialEntity; + } + + public static CredentialModel toModel(UserCredentialEntity entity) { + CredentialModel model = new CredentialModel(); + model.setId(entity.getId()); + model.setType(entity.getType()); + model.setCreatedDate(entity.getCreatedDate()); + model.setUserLabel(entity.getUserLabel()); + model.setSecretData(entity.getSecretData()); + model.setCredentialData(entity.getCredentialData()); + return model; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.updated |= !Objects.equals(this.id, id); + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.updated |= !Objects.equals(this.type, type); + this.type = type; + } + + public String getUserLabel() { + return userLabel; + } + + public void setUserLabel(String userLabel) { + this.updated |= !Objects.equals(this.userLabel, userLabel); + this.userLabel = userLabel; + } + + public Long getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(Long createdDate) { + this.updated |= !Objects.equals(this.createdDate, createdDate); + this.createdDate = createdDate; + } + + public String getSecretData() { + return secretData; + } + + public void setSecretData(String secretData) { + this.updated |= !Objects.equals(this.secretData, secretData); + this.secretData = secretData; + } + + public String getCredentialData() { + return credentialData; + } + + public void setCredentialData(String credentialData) { + this.updated |= !Objects.equals(this.credentialData, credentialData); + this.credentialData = credentialData; + } + + public boolean isUpdated() { + return updated; + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/user/UserFederatedIdentityEntity.java b/model/map/src/main/java/org/keycloak/models/map/user/UserFederatedIdentityEntity.java new file mode 100644 index 0000000000..d3356fd519 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/user/UserFederatedIdentityEntity.java @@ -0,0 +1,88 @@ +/* + * Copyright 2020 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. + */ + +package org.keycloak.models.map.user; + +import org.keycloak.models.FederatedIdentityModel; + +import java.util.Objects; + +public class UserFederatedIdentityEntity { + private String token; + private String userId; + private String identityProvider; + private String userName; + private boolean updated; + + private UserFederatedIdentityEntity() {} + + public static UserFederatedIdentityEntity fromModel(FederatedIdentityModel model) { + if (model == null) return null; + UserFederatedIdentityEntity entity = new UserFederatedIdentityEntity(); + entity.setIdentityProvider(model.getIdentityProvider()); + entity.setUserId(model.getUserId()); + entity.setUserName(model.getUserName().toLowerCase()); + entity.setToken(model.getToken()); + + return entity; + } + + public static FederatedIdentityModel toModel(UserFederatedIdentityEntity entity) { + if (entity == null) return null; + return new FederatedIdentityModel(entity.getIdentityProvider(), entity.getUserId(), entity.getUserName(), entity.getToken()); + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.updated |= !Objects.equals(this.token, token); + this.token = token; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.updated |= !Objects.equals(this.userId, userId); + this.userId = userId; + } + + public String getIdentityProvider() { + return identityProvider; + } + + public void setIdentityProvider(String identityProvider) { + this.updated |= !Objects.equals(this.identityProvider, identityProvider); + this.identityProvider = identityProvider; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.updated |= !Objects.equals(this.userName, userName); + this.userName = userName; + } + + public boolean isUpdated() { + return updated; + } +} diff --git a/model/map/src/main/resources/META-INF/services/org.keycloak.models.UserProviderFactory b/model/map/src/main/resources/META-INF/services/org.keycloak.models.UserProviderFactory new file mode 100644 index 0000000000..87133090ef --- /dev/null +++ b/model/map/src/main/resources/META-INF/services/org.keycloak.models.UserProviderFactory @@ -0,0 +1,18 @@ +# +# Copyright 2020 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. +# + +org.keycloak.models.map.user.MapUserProviderFactory diff --git a/model/map/src/main/test/java/org/keycloak/models/map/user/AbstractUserEntityCredentialsOrderTest.java b/model/map/src/main/test/java/org/keycloak/models/map/user/AbstractUserEntityCredentialsOrderTest.java new file mode 100644 index 0000000000..4c82afbdd9 --- /dev/null +++ b/model/map/src/main/test/java/org/keycloak/models/map/user/AbstractUserEntityCredentialsOrderTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2020 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. + */ + +package org.keycloak.models.map.user; + +import org.junit.Before; +import org.junit.Test; +import org.hamcrest.Matchers; +import static org.junit.Assert.assertThat; +import org.keycloak.credential.CredentialModel; + +import java.util.List; +import java.util.stream.Collectors; + +public class AbstractUserEntityCredentialsOrderTest { + + private AbstractUserEntity user; + + @Before + public void init() { + user = new AbstractUserEntity(1, "realmId") {}; + + for (int i = 1; i <= 5; i++) { + UserCredentialEntity credentialModel = new UserCredentialEntity(); + credentialModel.setId(Integer.toString(i)); + + user.addCredential(credentialModel); + } + } + + private void assertOrder(Integer... ids) { + List currentList = user.getCredentials().map(entity -> Integer.valueOf(entity.getId())).collect(Collectors.toList()); + assertThat(currentList, Matchers.contains(ids)); + } + + @Test + public void testCorrectOrder() { + assertOrder(1, 2, 3, 4, 5); + } + + @Test + public void testMoveToZero() { + user.moveCredential(2, 0); + assertOrder(3, 1, 2, 4, 5); + } + + @Test + public void testMoveBack() { + user.moveCredential(3, 1); + assertOrder(1, 4, 2, 3, 5); + } + + @Test + public void testMoveForward() { + user.moveCredential(1, 3); + assertOrder(1, 3, 4, 2, 5); + } + + @Test + public void testSamePosition() { + user.moveCredential(1, 1); + assertOrder(1, 2, 3, 4, 5); + } + + @Test + public void testSamePositionZero() { + user.moveCredential(0, 0); + assertOrder(1, 2, 3, 4, 5); + } + +} \ No newline at end of file diff --git a/server-spi-private/src/main/java/org/keycloak/events/EventListenerProvider.java b/server-spi-private/src/main/java/org/keycloak/events/EventListenerProvider.java index a46b294dcc..239950d1de 100644 --- a/server-spi-private/src/main/java/org/keycloak/events/EventListenerProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/events/EventListenerProvider.java @@ -18,15 +18,54 @@ package org.keycloak.events; import org.keycloak.events.admin.AdminEvent; +import org.keycloak.models.KeycloakTransaction; import org.keycloak.provider.Provider; /** * @author Stian Thorgersen + * + * This interface provides a way to listen to events that happen during the keycloak run. + *

+ * There are two types of events: + *

    + *
  • Event - User events (fired when users do some action, like log in, register etc.)
  • + *
  • Admin event - An administrator did some action like client created/updated etc.
  • + *
+ * + * + * Implementors can leverage the fact that the {@code onEvent} and {@code onAdminEvent} are run within a running + * transaction. Hence, if the event processing uses JPA, it can insert event details into a table, and the whole + * transaction including the event is either committed or rolled back. However if transaction processing is not + * an option, e.g. in the case of log files, it is recommended to hook onto transaction after the commit is complete + * via the {@link org.keycloak.models.KeycloakTransactionManager#enlistAfterCompletion(KeycloakTransaction)} method, so + * that the events are stacked in memory and only written to the file after the original transaction completes + * successfully. + * */ public interface EventListenerProvider extends Provider { + /** + * + * Called when a user event occurs e.g. log in, register. + *

+ * Note this method should not do any action that cannot be rolled back, see {@link EventListenerProvider} javadoc + * for more details. + * + * @param event to be triggered + */ void onEvent(Event event); + /** + * + * Called when an admin event occurs e.g. a client was updated/deleted. + *

+ * Note this method should not do any action that cannot be rolled back, see {@link EventListenerProvider} javadoc + * for more details. + * + * @param event to be triggered + * @param includeRepresentation when false, event listener should NOT include representation field in the resulting + * action + */ void onEvent(AdminEvent event, boolean includeRepresentation); } diff --git a/server-spi-private/src/main/java/org/keycloak/events/EventListenerTransaction.java b/server-spi-private/src/main/java/org/keycloak/events/EventListenerTransaction.java new file mode 100644 index 0000000000..ff07bcdef6 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/events/EventListenerTransaction.java @@ -0,0 +1,79 @@ +/* + * Copyright 2020 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. + */ + +package org.keycloak.events; + +import org.keycloak.events.admin.AdminEvent; +import org.keycloak.models.AbstractKeycloakTransaction; + +import java.util.LinkedList; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +public class EventListenerTransaction extends AbstractKeycloakTransaction { + + private static class AdminEventEntry { + private final AdminEvent event; + private final boolean includeRepresentation; + + public AdminEventEntry(AdminEvent event, boolean includeRepresentation) { + this.event = event; + this.includeRepresentation = includeRepresentation; + } + } + + private final List adminEventsToSend = new LinkedList<>(); + private final List eventsToSend = new LinkedList<>(); + private final BiConsumer adminEventConsumer; + private final Consumer eventConsumer; + + public EventListenerTransaction(BiConsumer adminEventConsumer, Consumer eventConsumer) { + this.adminEventConsumer = adminEventConsumer; + this.eventConsumer = eventConsumer; + } + + public void addAdminEvent(AdminEvent adminEvent, boolean includeRepresentation) { + adminEventsToSend.add(new AdminEventEntry(adminEvent, includeRepresentation)); + } + + public void addEvent(Event event) { + eventsToSend.add(event); + } + + @Override + protected void commitImpl() { + adminEventsToSend.forEach(this::consumeAdminEventEntry); + if (eventConsumer != null) { + eventsToSend.forEach(eventConsumer); + } + } + + private void consumeAdminEventEntry(AdminEventEntry entry) { + if (adminEventConsumer != null) { + adminEventConsumer.accept(entry.event, entry.includeRepresentation); + } + } + + @Override + protected void rollbackImpl() { + adminEventsToSend.clear(); + eventsToSend.clear(); + } + + +} diff --git a/server-spi-private/src/main/java/org/keycloak/events/admin/AdminEvent.java b/server-spi-private/src/main/java/org/keycloak/events/admin/AdminEvent.java index 7eac904e02..234bc839d5 100644 --- a/server-spi-private/src/main/java/org/keycloak/events/admin/AdminEvent.java +++ b/server-spi-private/src/main/java/org/keycloak/events/admin/AdminEvent.java @@ -41,6 +41,20 @@ public class AdminEvent { private String error; + public AdminEvent() {} + public AdminEvent(AdminEvent toCopy) { + this.time = toCopy.getTime(); + this.realmId = toCopy.getRealmId(); + this.authDetails = new AuthDetails(toCopy.getAuthDetails()); + this.resourceType = toCopy.getResourceTypeAsString(); + this.operationType = toCopy.getOperationType(); + this.resourcePath = toCopy.getResourcePath(); + this.representation = toCopy.getRepresentation(); + this.error = toCopy.getError(); + } + + + /** * Returns the time of the event * diff --git a/server-spi-private/src/main/java/org/keycloak/events/admin/AuthDetails.java b/server-spi-private/src/main/java/org/keycloak/events/admin/AuthDetails.java index 45e1d7c8e8..4dc7782961 100644 --- a/server-spi-private/src/main/java/org/keycloak/events/admin/AuthDetails.java +++ b/server-spi-private/src/main/java/org/keycloak/events/admin/AuthDetails.java @@ -29,6 +29,14 @@ public class AuthDetails { private String userId; private String ipAddress; + + public AuthDetails() {} + public AuthDetails(AuthDetails toCopy) { + this.realmId = toCopy.getRealmId(); + this.clientId = toCopy.getClientId(); + this.userId = toCopy.getUserId(); + this.ipAddress = toCopy.getIpAddress(); + } public String getRealmId() { return realmId; 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 35f1b15f57..5069ac47a0 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserModel.java +++ b/server-spi/src/main/java/org/keycloak/models/UserModel.java @@ -19,6 +19,7 @@ package org.keycloak.models; import org.keycloak.provider.ProviderEvent; +import java.util.Comparator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -45,6 +46,8 @@ public interface UserModel extends RoleMapperModel { String SEARCH = "keycloak.session.realm.users.query.search"; String EXACT = "keycloak.session.realm.users.query.exact"; + Comparator COMPARE_BY_USERNAME = Comparator.comparing(UserModel::getUsername, String.CASE_INSENSITIVE_ORDER); + interface UserRemovedEvent extends ProviderEvent { RealmModel getRealm(); UserModel getUser(); @@ -56,7 +59,13 @@ public interface UserModel extends RoleMapperModel { // No default method here to allow Abstract subclasses where the username is provided in a different manner String getUsername(); - // No default method here to allow Abstract subclasses where the username is provided in a different manner + /** + * Sets username for this user. + * + * No default method here to allow Abstract subclasses where the username is provided in a different manner + * + * @param username username string + */ void setUsername(String username); /** @@ -129,9 +138,17 @@ public interface UserModel extends RoleMapperModel { void removeRequiredAction(String action); - void addRequiredAction(RequiredAction action); + default void addRequiredAction(RequiredAction action) { + if (action == null) return; + String actionName = action.name(); + addRequiredAction(actionName); + } - void removeRequiredAction(RequiredAction action); + default void removeRequiredAction(RequiredAction action) { + if (action == null) return; + String actionName = action.name(); + removeRequiredAction(actionName); + } String getFirstName(); @@ -143,6 +160,11 @@ public interface UserModel extends RoleMapperModel { String getEmail(); + /** + * Sets email for this user. + * + * @param email the email + */ void setEmail(String email); boolean isEmailVerified(); diff --git a/server-spi/src/main/java/org/keycloak/models/UserProvider.java b/server-spi/src/main/java/org/keycloak/models/UserProvider.java index 0e3685e713..249f55ab24 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/UserProvider.java @@ -87,12 +87,25 @@ public interface UserProvider extends Provider, return value != null ? value.stream() : Stream.empty(); } + /** + * + * @param realm + * @param userId + * @param consent + * @throws ModelException when consent doesn't exist for the userId + */ void updateConsent(RealmModel realm, String userId, UserConsentModel consent); boolean revokeConsentForClient(RealmModel realm, String userId, String clientInternalId); void setNotBeforeForUser(RealmModel realm, UserModel user, int notBefore); int getNotBeforeOfUser(RealmModel realm, UserModel user); + /** + * + * @param client + * @throws IllegalArgumentException when there are more service accounts associated with the given clientId + * @return + */ UserModel getServiceAccount(ClientModel client); /** @@ -114,7 +127,7 @@ public interface UserProvider extends Provider, } /** - * @deprecated Use {@link #getUsersStream(RealmModel, int, int, boolean) getUsersStream} instead. + * @deprecated Use {@link #getUsersStream(RealmModel, Integer, Integer, boolean) getUsersStream} instead. */ @Deprecated List getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts); @@ -128,8 +141,8 @@ public interface UserProvider extends Provider, * @param includeServiceAccounts {@code true} if service accounts should be included in the result; {@code false} otherwise. * @return a non-null {@link Stream} of users associated withe the realm. */ - default Stream getUsersStream(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) { - List value = this.getUsers(realm, firstResult, maxResults, includeServiceAccounts); + default Stream getUsersStream(RealmModel realm, Integer firstResult, Integer maxResults, boolean includeServiceAccounts) { + List value = this.getUsers(realm, firstResult == null ? -1 : firstResult, maxResults == null ? -1 : maxResults, includeServiceAccounts); return value != null ? value.stream() : Stream.empty(); } @@ -204,6 +217,6 @@ public interface UserProvider extends Provider, } @Override - Stream getUsersStream(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts); + Stream getUsersStream(RealmModel realm, Integer firstResult, Integer maxResults, boolean includeServiceAccounts); } } 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 ccbf46e1d1..4f405213d5 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 @@ -31,5 +31,12 @@ public interface UserLookupProvider { UserModel getUserByUsername(String username, RealmModel realm); + /** + * + * @param email + * @param realm + * @throws org.keycloak.models.ModelDuplicateException when there are more users with same email + * @return + */ UserModel getUserByEmail(String email, RealmModel realm); } diff --git a/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java b/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java index f9d550133d..a36424714b 100644 --- a/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java +++ b/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java @@ -214,7 +214,7 @@ public interface UserQueryProvider { * @param firstResult * @param maxResults * @return - * @deprecated Use {@link #searchForUserStream(String, RealmModel, int, int) searchForUserStream} instead. + * @deprecated Use {@link #searchForUserStream(String, RealmModel, Integer, Integer) searchForUserStream} instead. */ @Deprecated List searchForUser(String search, RealmModel realm, int firstResult, int maxResults); @@ -231,8 +231,8 @@ public interface UserQueryProvider { * @param maxResults maximum number of results to return. Ignored if negative. * @return a non-null {@link Stream} of users that match the search criteria. */ - default Stream searchForUserStream(String search, RealmModel realm, int firstResult, int maxResults) { - List value = this.searchForUser(search, realm, firstResult, maxResults); + default Stream searchForUserStream(String search, RealmModel realm, Integer firstResult, Integer maxResults) { + List value = this.searchForUser(search, realm, firstResult == null ? -1 : firstResult, maxResults == null ? -1 : maxResults); return value != null ? value.stream() : Stream.empty(); } @@ -294,7 +294,7 @@ public interface UserQueryProvider { * @param firstResult * @param maxResults * @return - * @deprecated Use {@link #searchForUserStream(Map, RealmModel, int, int) searchForUserStream} instead. + * @deprecated Use {@link #searchForUserStream(Map, RealmModel, Integer, Integer) searchForUserStream} instead. */ @Deprecated List searchForUser(Map params, RealmModel realm, int firstResult, int maxResults); @@ -317,8 +317,8 @@ public interface UserQueryProvider { * @param maxResults maximum number of results to return. Ignored if negative. * @return a non-null {@link Stream} of users that match the search criteria. */ - default Stream searchForUserStream(Map params, RealmModel realm, int firstResult, int maxResults) { - List value = this.searchForUser(params, realm, firstResult, maxResults); + default Stream searchForUserStream(Map params, RealmModel realm, Integer firstResult, Integer maxResults) { + List value = this.searchForUser(params, realm, firstResult == null ? -1 : firstResult, maxResults == null ? -1 : maxResults); return value != null ? value.stream() : Stream.empty(); } @@ -362,7 +362,7 @@ public interface UserQueryProvider { * @param firstResult * @param maxResults * @return - * @deprecated Use {@link #getGroupMembersStream(RealmModel, GroupModel, int, int) getGroupMembersStream} instead. + * @deprecated Use {@link #getGroupMembersStream(RealmModel, GroupModel, Integer, Integer) getGroupMembersStream} instead. */ @Deprecated List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults); @@ -379,8 +379,8 @@ public interface UserQueryProvider { * @param maxResults maximum number of results to return. Ignored if negative. * @return a non-null {@link Stream} of users that belong to the group. */ - default Stream getGroupMembersStream(RealmModel realm, GroupModel group, int firstResult, int maxResults) { - List value = this.getGroupMembers(realm, group, firstResult, maxResults); + default Stream getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults) { + List value = this.getGroupMembers(realm, group, firstResult == null ? -1 : firstResult, maxResults == null ? -1 : maxResults); return value != null ? value.stream() : Stream.empty(); } @@ -405,17 +405,17 @@ public interface UserQueryProvider { * @return a non-null {@link Stream} of users that have the specified role. */ default Stream getRoleMembersStream(RealmModel realm, RoleModel role) { - return Stream.empty(); + return getRoleMembersStream(realm, role, null, null); } /** * Search for users that have a specific role with a specific roleId. * + * @param role * @param firstResult * @param maxResults - * @param role * @return - * @deprecated Use {@link #getRoleMembersStream(RealmModel, RoleModel, int, int) getRoleMembersStream} instead. + * @deprecated Use {@link #getRoleMembersStream(RealmModel, RoleModel, Integer, Integer) getRoleMembersStream} instead. */ @Deprecated default List getRoleMembers(RealmModel realm, RoleModel role, int firstResult, int maxResults) { @@ -431,7 +431,7 @@ public interface UserQueryProvider { * @param maxResults maximum number of results to return. Ignored if negative. * @return a non-null {@link Stream} of users that have the specified role. */ - default Stream getRoleMembersStream(RealmModel realm, RoleModel role, int firstResult, int maxResults) { + default Stream getRoleMembersStream(RealmModel realm, RoleModel role, Integer firstResult, Integer maxResults) { return Stream.empty(); } @@ -506,7 +506,7 @@ public interface UserQueryProvider { } @Override - Stream searchForUserStream(String search, RealmModel realm, int firstResult, int maxResults); + Stream searchForUserStream(String search, RealmModel realm, Integer firstResult, Integer maxResults); @Override default List searchForUser(Map params, RealmModel realm) { @@ -522,7 +522,7 @@ public interface UserQueryProvider { } @Override - Stream searchForUserStream(Map params, RealmModel realm, int firstResult, int maxResults); + Stream searchForUserStream(Map params, RealmModel realm, Integer firstResult, Integer maxResults); @Override default List getGroupMembers(RealmModel realm, GroupModel group) { @@ -538,7 +538,7 @@ public interface UserQueryProvider { } @Override - Stream getGroupMembersStream(RealmModel realm, GroupModel group, int firstResult, int maxResults); + Stream getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults); @Override default List searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) { diff --git a/services/src/main/java/org/keycloak/events/email/EmailEventListenerProvider.java b/services/src/main/java/org/keycloak/events/email/EmailEventListenerProvider.java index 94bd1a3434..9c3c0f1fe5 100755 --- a/services/src/main/java/org/keycloak/events/email/EmailEventListenerProvider.java +++ b/services/src/main/java/org/keycloak/events/email/EmailEventListenerProvider.java @@ -22,6 +22,7 @@ import org.keycloak.email.EmailException; import org.keycloak.email.EmailTemplateProvider; import org.keycloak.events.Event; import org.keycloak.events.EventListenerProvider; +import org.keycloak.events.EventListenerTransaction; import org.keycloak.events.EventType; import org.keycloak.events.admin.AdminEvent; import org.keycloak.models.KeycloakSession; @@ -42,6 +43,7 @@ public class EmailEventListenerProvider implements EventListenerProvider { private RealmProvider model; private EmailTemplateProvider emailTemplateProvider; private Set includedEvents; + private EventListenerTransaction tx = new EventListenerTransaction(null, this::sendEmail); public EmailEventListenerProvider(KeycloakSession session, EmailTemplateProvider emailTemplateProvider, Set includedEvents) { this.session = session; @@ -54,15 +56,19 @@ public class EmailEventListenerProvider implements EventListenerProvider { public void onEvent(Event event) { if (includedEvents.contains(event.getType())) { if (event.getRealmId() != null && event.getUserId() != null) { - RealmModel realm = model.getRealm(event.getRealmId()); - UserModel user = session.users().getUserById(event.getUserId(), realm); - if (user != null && user.getEmail() != null && user.isEmailVerified()) { - try { - emailTemplateProvider.setRealm(realm).setUser(user).sendEvent(event); - } catch (EmailException e) { - log.error("Failed to send type mail", e); - } - } + tx.addEvent(event); + } + } + } + + private void sendEmail(Event event) { + RealmModel realm = model.getRealm(event.getRealmId()); + UserModel user = session.users().getUserById(event.getUserId(), realm); + if (user != null && user.getEmail() != null && user.isEmailVerified()) { + try { + emailTemplateProvider.setRealm(realm).setUser(user).sendEvent(event); + } catch (EmailException e) { + log.error("Failed to send type mail", e); } } } diff --git a/services/src/main/java/org/keycloak/events/log/JBossLoggingEventListenerProvider.java b/services/src/main/java/org/keycloak/events/log/JBossLoggingEventListenerProvider.java index 1a1a2e4e74..f76a8726a0 100755 --- a/services/src/main/java/org/keycloak/events/log/JBossLoggingEventListenerProvider.java +++ b/services/src/main/java/org/keycloak/events/log/JBossLoggingEventListenerProvider.java @@ -21,6 +21,7 @@ import org.keycloak.common.util.StackUtil; import org.jboss.logging.Logger; import org.keycloak.events.Event; import org.keycloak.events.EventListenerProvider; +import org.keycloak.events.EventListenerTransaction; import org.keycloak.events.admin.AdminEvent; import org.keycloak.models.KeycloakContext; import org.keycloak.models.KeycloakSession; @@ -40,16 +41,28 @@ public class JBossLoggingEventListenerProvider implements EventListenerProvider private final Logger logger; private final Logger.Level successLevel; private final Logger.Level errorLevel; + private final EventListenerTransaction tx = new EventListenerTransaction(this::logAdminEvent, this::logEvent); public JBossLoggingEventListenerProvider(KeycloakSession session, Logger logger, Logger.Level successLevel, Logger.Level errorLevel) { this.session = session; this.logger = logger; this.successLevel = successLevel; this.errorLevel = errorLevel; + + this.session.getTransactionManager().enlistAfterCompletion(tx); } @Override public void onEvent(Event event) { + tx.addEvent(event); + } + + @Override + public void onEvent(AdminEvent adminEvent, boolean includeRepresentation) { + tx.addAdminEvent(adminEvent, includeRepresentation); + } + + private void logEvent(Event event) { Logger.Level level = event.getError() != null ? errorLevel : successLevel; if (logger.isEnabled(level)) { @@ -85,15 +98,15 @@ public class JBossLoggingEventListenerProvider implements EventListenerProvider } } } - - AuthenticationSessionModel authSession = session.getContext().getAuthenticationSession(); + + AuthenticationSessionModel authSession = session.getContext().getAuthenticationSession(); if(authSession!=null) { sb.append(", authSessionParentId="); sb.append(authSession.getParentSession().getId()); sb.append(", authSessionTabId="); sb.append(authSession.getTabId()); } - + if(logger.isTraceEnabled()) { setKeycloakContext(sb); @@ -106,8 +119,7 @@ public class JBossLoggingEventListenerProvider implements EventListenerProvider } } - @Override - public void onEvent(AdminEvent adminEvent, boolean includeRepresentation) { + private void logAdminEvent(AdminEvent adminEvent, boolean includeRepresentation) { Logger.Level level = adminEvent.getError() != null ? errorLevel : successLevel; if (logger.isEnabled(level)) { @@ -132,7 +144,7 @@ public class JBossLoggingEventListenerProvider implements EventListenerProvider sb.append(", error="); sb.append(adminEvent.getError()); } - + if(logger.isTraceEnabled()) { setKeycloakContext(sb); } diff --git a/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java b/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java index a811c7eeaa..28845ced21 100644 --- a/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java +++ b/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java @@ -100,7 +100,7 @@ public abstract class AbstractPartialImport implements PartialImport { create(realm, session, resourceRep); } catch (Exception e) { ServicesLogger.LOGGER.overwriteError(e, getName(resourceRep)); - throw new ErrorResponseException(ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR)); + throw e; } String modelId = getModelId(realm, session, resourceRep); @@ -122,7 +122,7 @@ public abstract class AbstractPartialImport implements PartialImport { results.addResult(added(modelId, resourceRep)); } catch (Exception e) { ServicesLogger.LOGGER.creationError(e, getName(resourceRep)); - throw new ErrorResponseException(ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR)); + throw e; } } diff --git a/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java b/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java index f119c7cb34..a63cc04b39 100644 --- a/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java +++ b/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java @@ -19,6 +19,7 @@ package org.keycloak.partialimport; import org.keycloak.events.admin.OperationType; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; import org.keycloak.representations.idm.PartialImportRepresentation; @@ -58,44 +59,40 @@ public class PartialImportManager { } public Response saveResources() { + try { - PartialImportResults results = new PartialImportResults(); + PartialImportResults results = new PartialImportResults(); - for (PartialImport partialImport : partialImports) { - try { + for (PartialImport partialImport : partialImports) { partialImport.prepare(rep, realm, session); - } catch (ErrorResponseException error) { - if (session.getTransactionManager().isActive()) session.getTransactionManager().setRollbackOnly(); - return error.getResponse(); } - } - for (PartialImport partialImport : partialImports) { - try { + for (PartialImport partialImport : partialImports) { partialImport.removeOverwrites(realm, session); results.addAllResults(partialImport.doImport(rep, realm, session)); - } catch (ErrorResponseException error) { - if (session.getTransactionManager().isActive()) session.getTransactionManager().setRollbackOnly(); - return error.getResponse(); } - } - for (PartialImportResult result : results.getResults()) { - switch (result.getAction()) { - case ADDED : fireCreatedEvent(result); break; - case OVERWRITTEN: fireUpdateEvent(result); break; + for (PartialImportResult result : results.getResults()) { + switch (result.getAction()) { + case ADDED : fireCreatedEvent(result); break; + case OVERWRITTEN: fireUpdateEvent(result); break; + } } - } - if (session.getTransactionManager().isActive()) { - try { + if (session.getTransactionManager().isActive()) { session.getTransactionManager().commit(); - } catch (ModelException e) { - return ErrorResponse.exists(e.getLocalizedMessage()); } - } - return Response.ok(results).build(); + return Response.ok(results).build(); + } catch (ModelDuplicateException e) { + return ErrorResponse.exists(e.getLocalizedMessage()); + } catch (ErrorResponseException error) { + if (session.getTransactionManager().isActive()) session.getTransactionManager().setRollbackOnly(); + return error.getResponse(); + } catch (Exception e) { + if (session.getTransactionManager().isActive()) session.getTransactionManager().setRollbackOnly(); + return ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR); + } } private void fireCreatedEvent(PartialImportResult result) { diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminEventBuilder.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminEventBuilder.java index f497c9ba80..e5680ad556 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminEventBuilder.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminEventBuilder.java @@ -72,9 +72,9 @@ public class AdminEventBuilder { } /** - * Refreshes the builder assuming that the realm event information has + * Refreshes the builder assuming that the realm event information has * changed. Thought to be used when the updateRealmEventsConfig has - * modified the events configuration. Now the store and the listeners are + * modified the events configuration. Now the store and the listeners are * updated to have previous and new setup. * @param session The session * @return The same builder @@ -82,7 +82,7 @@ public class AdminEventBuilder { public AdminEventBuilder refreshRealmEventsConfig(KeycloakSession session) { return this.updateStore(session).addListeners(session); } - + private AdminEventBuilder updateStore(KeycloakSession session) { if (realm.isAdminEventsEnabled() && store == null) { this.store = session.getProvider(EventStoreProvider.class); @@ -92,7 +92,7 @@ public class AdminEventBuilder { } return this; } - + private AdminEventBuilder addListeners(KeycloakSession session) { realm.getEventsListenersStream() .filter(((Predicate) listeners::containsKey).negate()) @@ -233,15 +233,15 @@ public class AdminEventBuilder { } private void send() { - boolean includeRepresentation = false; - if(realm.isAdminEventsDetailsEnabled()) { - includeRepresentation = true; - } - adminEvent.setTime(Time.currentTimeMillis()); + boolean includeRepresentation = realm.isAdminEventsDetailsEnabled(); + + // Event needs to be copied because the same builder can be used with another event + AdminEvent eventCopy = new AdminEvent(adminEvent); + eventCopy.setTime(Time.currentTimeMillis()); if (store != null) { try { - store.onEvent(adminEvent, includeRepresentation); + store.onEvent(eventCopy, includeRepresentation); } catch (Throwable t) { ServicesLogger.LOGGER.failedToSaveEvent(t); } @@ -250,7 +250,7 @@ public class AdminEventBuilder { if (listeners != null) { for (EventListenerProvider l : listeners.values()) { try { - l.onEvent(adminEvent, includeRepresentation); + l.onEvent(eventCopy, includeRepresentation); } catch (Throwable t) { ServicesLogger.LOGGER.failedToSendType(t, l); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeResource.java index f1877a5437..09a2b060be 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeResource.java @@ -101,10 +101,11 @@ public class ClientScopeResource { try { RepresentationToModel.updateClientScope(rep, clientScope); + adminEvent.operation(OperationType.UPDATE).resourcePath(session.getContext().getUri()).representation(rep).success(); + if (session.getTransactionManager().isActive()) { session.getTransactionManager().commit(); } - adminEvent.operation(OperationType.UPDATE).resourcePath(session.getContext().getUri()).representation(rep).success(); return Response.noContent().build(); } catch (ModelDuplicateException e) { return ErrorResponse.exists("Client Scope " + rep.getName() + " already exists"); diff --git a/services/src/main/java/org/keycloak/storage/UserStorageManager.java b/services/src/main/java/org/keycloak/storage/UserStorageManager.java index 0ab9d6f44f..27dc97950b 100755 --- a/services/src/main/java/org/keycloak/storage/UserStorageManager.java +++ b/services/src/main/java/org/keycloak/storage/UserStorageManager.java @@ -128,13 +128,25 @@ public class UserStorageManager extends AbstractStorageManager { RealmModel realmModel = session.realms().getRealm(realm.getId()); if (realmModel == null) return; UserModel deletedUser = session.userLocalStorage().getUserById(userId, realmModel); if (deletedUser != null) { - new UserManager(session).removeUser(realmModel, deletedUser, session.userLocalStorage()); - logger.debugf("Removed invalid user '%s'", userName); + try { + new UserManager(session).removeUser(realmModel, deletedUser, session.userLocalStorage()); + logger.debugf("Removed invalid user '%s'", userName); + } catch (ModelException ex) { + // Ignore exception, possible cause may be concurrent deleteInvalidUser calls which means + // ModelException exception may be ignored because users will be removed with next call or is + // already removed + logger.debugf(ex, "ModelException thrown during deleteInvalidUser with username '%s'", userName); + } } }); } @@ -268,7 +280,7 @@ public class UserStorageManager extends AbstractStorageManager getGroupMembersStream(final RealmModel realm, final GroupModel group, int firstResult, int maxResults) { + public Stream getGroupMembersStream(final RealmModel realm, final GroupModel group, Integer firstResult, Integer maxResults) { Stream results = query((provider) -> { if (provider instanceof UserQueryProvider) { return ((UserQueryProvider)provider).getGroupMembersStream(realm, group); @@ -289,7 +301,7 @@ public class UserStorageManager extends AbstractStorageManager getRoleMembersStream(final RealmModel realm, final RoleModel role, int firstResult, int maxResults) { + public Stream getRoleMembersStream(final RealmModel realm, final RoleModel role, Integer firstResult, Integer maxResults) { Stream results = query((provider) -> { if (provider instanceof UserQueryProvider) { return ((UserQueryProvider)provider).getRoleMembersStream(realm, role); @@ -316,7 +328,7 @@ public class UserStorageManager extends AbstractStorageManager getUsersStream(final RealmModel realm, int firstResult, int maxResults, final boolean includeServiceAccounts) { + public Stream getUsersStream(final RealmModel realm, Integer firstResult, Integer maxResults, final boolean includeServiceAccounts) { Stream results = query((provider) -> { if (provider instanceof UserProvider) { // it is local storage return ((UserProvider) provider).getUsersStream(realm, includeServiceAccounts); @@ -375,7 +387,7 @@ public class UserStorageManager extends AbstractStorageManager searchForUserStream(String search, RealmModel realm, int firstResult, int maxResults) { + public Stream searchForUserStream(String search, RealmModel realm, Integer firstResult, Integer maxResults) { Stream results = query((provider) -> { if (provider instanceof UserQueryProvider) { return ((UserQueryProvider)provider).searchForUserStream(search, realm); @@ -391,7 +403,7 @@ public class UserStorageManager extends AbstractStorageManager searchForUserStream(Map attributes, RealmModel realm, int firstResult, int maxResults) { + public Stream searchForUserStream(Map attributes, RealmModel realm, Integer firstResult, Integer maxResults) { Stream results = query((provider) -> { if (provider instanceof UserQueryProvider) { if (attributes.containsKey(UserModel.SEARCH)) { diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/events/EventsListenerProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/events/TestEventsListenerProvider.java similarity index 65% rename from testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/events/EventsListenerProvider.java rename to testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/events/TestEventsListenerProvider.java index b0ea56af8d..c03f5ac7e9 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/events/EventsListenerProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/events/TestEventsListenerProvider.java @@ -19,7 +19,9 @@ package org.keycloak.testsuite.events; import org.keycloak.events.Event; import org.keycloak.events.EventListenerProvider; +import org.keycloak.events.EventListenerTransaction; import org.keycloak.events.admin.AdminEvent; +import org.keycloak.models.KeycloakSession; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -27,20 +29,24 @@ import java.util.concurrent.LinkedBlockingQueue; /** * @author Marko Strukelj */ -public class EventsListenerProvider implements EventListenerProvider { +public class TestEventsListenerProvider implements EventListenerProvider { private static final BlockingQueue events = new LinkedBlockingQueue(); private static final BlockingQueue adminEvents = new LinkedBlockingQueue<>(); + private final EventListenerTransaction tx = new EventListenerTransaction((event, includeRepre) -> adminEvents.add(event), events::add); + + public TestEventsListenerProvider(KeycloakSession session) { + session.getTransactionManager().enlistAfterCompletion(tx); + } @Override public void onEvent(Event event) { - events.add(event); + tx.addEvent(event); } @Override public void onEvent(AdminEvent event, boolean includeRepresentation) { - // Save the copy for case when same AdminEventBuilder is used more times during same transaction to avoid overwriting previously referenced event - adminEvents.add(copy(event)); + tx.addAdminEvent(event, includeRepresentation); } @Override @@ -63,17 +69,4 @@ public class EventsListenerProvider implements EventListenerProvider { public static void clearAdminEvents() { adminEvents.clear(); } - - private AdminEvent copy(AdminEvent adminEvent) { - AdminEvent newEvent = new AdminEvent(); - newEvent.setAuthDetails(adminEvent.getAuthDetails()); - newEvent.setError(adminEvent.getError()); - newEvent.setOperationType(adminEvent.getOperationType()); - newEvent.setResourceType(adminEvent.getResourceType()); - newEvent.setRealmId(adminEvent.getRealmId()); - newEvent.setRepresentation(adminEvent.getRepresentation()); - newEvent.setResourcePath(adminEvent.getResourcePath()); - newEvent.setTime(adminEvent.getTime()); - return newEvent; - } } diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/events/EventsListenerProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/events/TestEventsListenerProviderFactory.java similarity index 87% rename from testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/events/EventsListenerProviderFactory.java rename to testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/events/TestEventsListenerProviderFactory.java index af2e2414e9..b22fc1d449 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/events/EventsListenerProviderFactory.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/events/TestEventsListenerProviderFactory.java @@ -26,15 +26,13 @@ import org.keycloak.models.KeycloakSessionFactory; /** * @author Marko Strukelj */ -public class EventsListenerProviderFactory implements EventListenerProviderFactory { +public class TestEventsListenerProviderFactory implements EventListenerProviderFactory { public static final String PROVIDER_ID = "event-queue"; - private static final EventsListenerProvider INSTANCE = new EventsListenerProvider(); - @Override public EventListenerProvider create(KeycloakSession session) { - return INSTANCE; + return new TestEventsListenerProvider(session); } @Override diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/FailableHardcodedStorageProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/FailableHardcodedStorageProvider.java index 6a93f2d358..fc50470e60 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/FailableHardcodedStorageProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/FailableHardcodedStorageProvider.java @@ -243,7 +243,7 @@ public class FailableHardcodedStorageProvider implements UserStorageProvider, Us } @Override - public Stream searchForUserStream(String search, RealmModel realm, int firstResult, int maxResults) { + public Stream searchForUserStream(String search, RealmModel realm, Integer firstResult, Integer maxResults) { checkForceFail(); if (!search.equals(username)) return Stream.empty(); UserModel model = getUserByUsername(username, realm); @@ -259,7 +259,7 @@ public class FailableHardcodedStorageProvider implements UserStorageProvider, Us } @Override - public Stream searchForUserStream(Map params, RealmModel realm, int firstResult, int maxResults) { + public Stream searchForUserStream(Map params, RealmModel realm, Integer firstResult, Integer maxResults) { checkForceFail(); if (!username.equals(params.get("username")))return Stream.empty(); UserModel model = getUserByUsername(username, realm); @@ -267,7 +267,7 @@ public class FailableHardcodedStorageProvider implements UserStorageProvider, Us } @Override - public Stream getGroupMembersStream(RealmModel realm, GroupModel group, int firstResult, int maxResults) { + public Stream getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults) { checkForceFail(); return Stream.empty(); } diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/UserMapStorage.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/UserMapStorage.java index 9a40a1854b..b8d538db77 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/UserMapStorage.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/UserMapStorage.java @@ -315,14 +315,14 @@ public class UserMapStorage implements UserLookupProvider, UserStorageProvider, } @Override - public Stream searchForUserStream(String search, RealmModel realm, int firstResult, int maxResults) { + public Stream searchForUserStream(String search, RealmModel realm, Integer firstResult, Integer maxResults) { String tSearch = translateUserName(search); Stream userStream = userPasswords.keySet().stream() .sorted() .filter(userName -> translateUserName(userName).contains(search)); - if (firstResult > 0) + if (firstResult != null && firstResult > 0) userStream = userStream.skip(firstResult); - if (maxResults >= 0) + if (maxResults != null && maxResults >= 0) userStream = userStream.limit(maxResults); return userStream.map(userName -> createUser(realm, userName)); } @@ -333,7 +333,7 @@ public class UserMapStorage implements UserLookupProvider, UserStorageProvider, } @Override - public Stream searchForUserStream(Map params, RealmModel realm, int firstResult, int maxResults) { + public Stream searchForUserStream(Map params, RealmModel realm, Integer firstResult, Integer maxResults) { Stream userStream = userPasswords.keySet().stream() .sorted(); @@ -356,15 +356,15 @@ public class UserMapStorage implements UserLookupProvider, UserStorageProvider, } } - if (firstResult > 0) + if (firstResult != null && firstResult > 0) userStream = userStream.skip(firstResult); - if (maxResults >= 0) + if (maxResults != null && maxResults >= 0) userStream = userStream.limit(maxResults); return userStream.map(userName -> createUser(realm, userName)); } @Override - public Stream getGroupMembersStream(RealmModel realm, GroupModel group, int firstResult, int maxResults) { + public Stream getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults) { return getMembershipStream(realm, group, firstResult, maxResults) .map(userName -> createUser(realm, userName)); } diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java index 6c4f2c207b..92cc19cf58 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java @@ -62,7 +62,7 @@ import org.keycloak.services.util.CookieHelper; import org.keycloak.storage.UserStorageProvider; import org.keycloak.testsuite.components.TestProvider; import org.keycloak.testsuite.components.TestProviderFactory; -import org.keycloak.testsuite.events.EventsListenerProvider; +import org.keycloak.testsuite.events.TestEventsListenerProvider; import org.keycloak.testsuite.federation.DummyUserFederationProviderFactory; import org.keycloak.testsuite.forms.PassThroughAuthenticator; import org.keycloak.testsuite.forms.PassThroughClientAuthenticator; @@ -227,7 +227,7 @@ public class TestingResourceProvider implements RealmResourceProvider { @Path("/poll-event-queue") @Produces(MediaType.APPLICATION_JSON) public EventRepresentation getEvent() { - Event event = EventsListenerProvider.poll(); + Event event = TestEventsListenerProvider.poll(); if (event != null) { return ModelToRepresentation.toRepresentation(event); } else { @@ -239,7 +239,7 @@ public class TestingResourceProvider implements RealmResourceProvider { @Path("/poll-admin-event-queue") @Produces(MediaType.APPLICATION_JSON) public AdminEventRepresentation getAdminEvent() { - AdminEvent adminEvent = EventsListenerProvider.pollAdminEvent(); + AdminEvent adminEvent = TestEventsListenerProvider.pollAdminEvent(); if (adminEvent != null) { return ModelToRepresentation.toRepresentation(adminEvent); } else { @@ -251,7 +251,7 @@ public class TestingResourceProvider implements RealmResourceProvider { @Path("/clear-event-queue") @Produces(MediaType.APPLICATION_JSON) public Response clearEventQueue() { - EventsListenerProvider.clear(); + TestEventsListenerProvider.clear(); return Response.noContent().build(); } @@ -259,7 +259,7 @@ public class TestingResourceProvider implements RealmResourceProvider { @Path("/clear-admin-event-queue") @Produces(MediaType.APPLICATION_JSON) public Response clearAdminEventQueue() { - EventsListenerProvider.clearAdminEvents(); + TestEventsListenerProvider.clearAdminEvents(); return Response.noContent().build(); } diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory index e995149b93..6c28a262b5 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory @@ -32,4 +32,4 @@ # limitations under the License. # -org.keycloak.testsuite.events.EventsListenerProviderFactory \ No newline at end of file +org.keycloak.testsuite.events.TestEventsListenerProviderFactory \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AbstractAdminTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AbstractAdminTest.java index 7d466a6ea8..ba4f5cb030 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AbstractAdminTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/AbstractAdminTest.java @@ -23,7 +23,7 @@ import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.events.log.JBossLoggingEventListenerProviderFactory; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; -import org.keycloak.testsuite.events.EventsListenerProviderFactory; +import org.keycloak.testsuite.events.TestEventsListenerProviderFactory; import org.keycloak.testsuite.util.TestCleanup; import org.keycloak.testsuite.util.AssertAdminEvents; import org.keycloak.util.JsonSerialization; @@ -73,7 +73,7 @@ public abstract class AbstractAdminTest extends AbstractTestRealmKeycloakTest { List eventListeners = new ArrayList<>(); eventListeners.add(JBossLoggingEventListenerProviderFactory.ID); - eventListeners.add(EventsListenerProviderFactory.PROVIDER_ID); + eventListeners.add(TestEventsListenerProviderFactory.PROVIDER_ID); adminRealmRep.setEventsListeners(eventListeners); testRealms.add(adminRealmRep); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java index 13179330ae..e8d78c3cda 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java @@ -46,6 +46,7 @@ import org.keycloak.models.credential.OTPCredentialModel; import org.keycloak.models.credential.PasswordCredentialModel; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.idm.AdminEventRepresentation; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; @@ -1847,9 +1848,7 @@ public class UserTest extends AbstractAdminTest { } catch (ClientErrorException e) { assertEquals(409, e.getResponse().getStatus()); - // TODO adminEvents: Event queue should be empty, but it's not because of bug in UsersResource.updateUser, which sends event earlier than transaction commit. - // assertAdminEvents.assertEmpty(); - assertAdminEvents.poll(); + assertAdminEvents.assertEmpty(); } finally { enableBruteForce(false); switchEditUsernameAllowedOn(false); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/AbstractClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/AbstractClientTest.java index 60c0417063..1e26a4ef75 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/AbstractClientTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/AbstractClientTest.java @@ -29,7 +29,7 @@ import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.AbstractAuthTest; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.admin.ApiUtil; -import org.keycloak.testsuite.events.EventsListenerProviderFactory; +import org.keycloak.testsuite.events.TestEventsListenerProviderFactory; import org.keycloak.testsuite.util.AdminEventPaths; import org.keycloak.testsuite.util.AssertAdminEvents; import org.keycloak.testsuite.util.RealmBuilder; @@ -56,7 +56,7 @@ public abstract class AbstractClientTest extends AbstractAuthTest { @Before public void setupAdminEvents() { RealmRepresentation realm = testRealmResource().toRepresentation(); - if (realm.getEventsListeners() == null || !realm.getEventsListeners().contains(EventsListenerProviderFactory.PROVIDER_ID)) { + if (realm.getEventsListeners() == null || !realm.getEventsListeners().contains(TestEventsListenerProviderFactory.PROVIDER_ID)) { realm = RealmBuilder.edit(testRealmResource().toRepresentation()).testEventListener().build(); testRealmResource().update(realm); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/partialimport/PartialImportTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/partialimport/PartialImportTest.java index 98f7e5b690..8a0872a613 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/partialimport/PartialImportTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/partialimport/PartialImportTest.java @@ -54,6 +54,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.startsWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -376,8 +378,8 @@ public class PartialImportTest extends AbstractAuthTest { String id = result.getId(); UserResource userRsc = testRealmResource().users().get(id); UserRepresentation user = userRsc.toRepresentation(); - assertTrue(user.getUsername().startsWith(USER_PREFIX)); - Assert.assertTrue(userIds.contains(id)); + Assert.assertThat(user.getUsername(), startsWith(USER_PREFIX)); + Assert.assertThat(userIds, hasItem(id)); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java index 4e24092e47..ca2c170de5 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java @@ -51,7 +51,7 @@ import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; import org.keycloak.testsuite.auth.page.AuthRealm; import org.keycloak.testsuite.client.KeycloakTestingClient; -import org.keycloak.testsuite.events.EventsListenerProviderFactory; +import org.keycloak.testsuite.events.TestEventsListenerProviderFactory; import org.keycloak.testsuite.runonserver.RunHelpers; import org.keycloak.testsuite.updaters.Creator; import org.keycloak.testsuite.util.AdminEventPaths; @@ -358,7 +358,7 @@ public class RealmTest extends AbstractAdminTest { RealmEventsConfigRepresentation repOrig = copyRealmEventsConfigRepresentation(rep); // the "event-queue" listener should be enabled by default - assertTrue("event-queue should be enabled initially", rep.getEventsListeners().contains(EventsListenerProviderFactory.PROVIDER_ID)); + assertTrue("event-queue should be enabled initially", rep.getEventsListeners().contains(TestEventsListenerProviderFactory.PROVIDER_ID)); // first modification => remove "event-queue", should be sent to the queue rep.setEnabledEventTypes(Arrays.asList(EventType.LOGIN.name(), EventType.LOGIN_ERROR.name())); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java index f6f2a16e9d..357e4acdff 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/crossdc/AbstractAdminCrossDCTest.java @@ -21,7 +21,7 @@ import org.keycloak.events.log.JBossLoggingEventListenerProviderFactory; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.common.util.Retry; import org.keycloak.testsuite.arquillian.InfinispanStatistics; -import org.keycloak.testsuite.events.EventsListenerProviderFactory; +import org.keycloak.testsuite.events.TestEventsListenerProviderFactory; import org.keycloak.testsuite.util.TestCleanup; import java.util.ArrayList; import java.util.HashMap; @@ -73,7 +73,7 @@ public abstract class AbstractAdminCrossDCTest extends AbstractCrossDCTest { List eventListeners = new ArrayList<>(); eventListeners.add(JBossLoggingEventListenerProviderFactory.ID); - eventListeners.add(EventsListenerProviderFactory.PROVIDER_ID); + eventListeners.add(TestEventsListenerProviderFactory.PROVIDER_ID); adminRealmRep.setEventsListeners(eventListeners); testRealms.add(adminRealmRep); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPBinaryAttributesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPBinaryAttributesTest.java index 7f1e5d8014..3290039b4a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPBinaryAttributesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPBinaryAttributesTest.java @@ -47,6 +47,10 @@ import java.util.Collections; import java.util.List; import java.util.stream.Stream; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + /** * @author Marek Posolda */ @@ -84,11 +88,9 @@ public class LDAPBinaryAttributesTest extends AbstractLDAPTest { } - private static final String JPEG_PHOTO_BASE64 = "/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAMDAwMDAwQEBAQFBQUFBQcHBgYHBwsICQgJCAsRCwwLCwwLEQ8SDw4PEg8bFRMTFRsfGhkaHyYiIiYwLTA+PlQBAwMDAwMDBAQEBAUFBQUFBwcGBgcHCwgJCAkICxELDAsLDAsRDxIPDg8SDxsVExMVGx8aGRofJiIiJjAtMD4+VP/CABEIAFIAWAMBIgACEQEDEQH/xAAdAAACAgMBAQEAAAAAAAAAAAAGCAUHAwQJAgAB/9oACAEBAAAAAElmzK1aOaraUmpiktrD10DayAIMkKunPQdk+hrZwkUNkMrM88VDtt7r7C1KCJtprbSBP2J6K+VDqUlwErkDnOm/HF9IPDaWQ+cP85sXS7OCj1Iybjj2zVvJA0b91BqJ1JuQDYfkuSWrGdFzsMsWkaIUGHh4IuY1glqtWrK8G89YeJk+ldfT1pHz9//EABoBAAIDAQEAAAAAAAAAAAAAAAUHAwYIBAD/2gAIAQIQAAAApKICefJOj5haleI6vyhC1+FEa9UydaFaD7hDLtU9jttBsCJni6f/xAAaAQADAQEBAQAAAAAAAAAAAAAFBgcDBAII/9oACAEDEAAAAF5+Mdc4X6LUuz2kyGquiYYI/PzvUctR8d5289ghjS48fuOK/wD/xAA5EAABAwIEBAMGAwcFAAAAAAABAgMEABEFBhIhEzFBURQiMgdCYXGBwSOhsRUkM3KCkdFSU2J04f/aAAgBAQABPwGHLbkIVbmjmPhSF60BSL2VUPDhLYLjL4KuotyP0NQPZq5iWBmUiWPGFarIV/DIHS/MGpeC4lhrnh5MdxLgJBuna/wPWsFyW2/kVaH2/wB7dK5LSyPMDbb6KtWD+zqBKypKTIZQJ89q4dPNG10f+1GydizuAPSSgstwlHiawQVnVby/LrTrfBOkqF6gyWmEJQSoKvfVYUiawVbrubc6bQEr4qSkKpDj/iEMKSdLoDYSm/XkRWSco+Bg4dORxo8lCyJcd1NgqxI68tjSG2Wm/wAFKUi97DYVi2GRcZh8F+9goLSR0IpC02TakqA2rE4acSgPw9WgPJ0qPYE71mL2fQJLanYiOGmJh5DKEep125IualtvxJDrDo0OtLKFjsQd6U8tJ2686deDa/Ii1/STvWU8KwDOOVoaZ8VPioV2VLQdC0WNwQRSU2QEk3sNyetPExzq9zr8KEwpulXTrTWJx0OLQp5IOskC/Sm5iFpSpCr3A/S9IeF9zQJPI2r2g5Tw3AWFS0uPOPzpay3q9KE8zc9Tvzpxv6HrWUcCh5hnmC/iQiv6bskp18XvbcVlXJE3K0xbrOJJfadFnGi2U/UG5peocqxPEmYrCtak3I2FTcckBwp1q9PIck87Hbn3oLllYWTvzvUbFpkE7XIv1+FYFizOIM60W18inqmmeIrnXtTaxmW82p2I61CYFkL2Ui6upI5XrhKS0nWL6VadQ95J7VkzKcZzEor4lsHgkL4KwSrWO1ulJdWbeUHbcjaluqAPkX+VZjxMLkur07Nk7cjYdjyvTSzJcceWLFe4+9Mt6x5eQNOBCiR8enSsqylxcSLSdR1g7X273pmVYedJT9Kx7L+G5mjBp911sp9KkqOx+KTsazDl+Rl3EBHcktvNKuptSFbEfEdCKyEGHNi+OKhZUGFNg/1JJ3BoSU99/lTuzZUs2FutZhaSZT5SggJVcG29r9bU1p8OhQuRalyHgBa4TyriWTfqayjEck4kp1CiA0j63VyplTqPKtQV89qnfsl5jhzyzw1/7igP7XrPOF4fgmMNrg4ozIjSFH8BCwtbI7E9u1ZCwvCpjrcyRKQHW3PLG1WJ+N/8U65o3sBSWCs8R3c9B2rN2BGWy7ISQDpsRb86SuRC1IlNLQb9ep+B60ZUbYnaokOXijwaYQdJ5q90bd6wDBF4JESlJC7+Zdhvc00W3htWbZmHYVhi3J8PxUVR0rb06t+l+3zrFXMLU6VQUvJJcWVBQ0pSOiUi6jt3Jpryea52r2c+KxKap9995TEVvyjWbFajtfvWsL+VKW1iBJQoKZbV03uof4qThEWWyriNJWee++9R8uYTH06YrYIH+kU9BaCNKU2t6aw93iJ0KO6axfFIOFSW1OSEsFwn1GwJHOsdbYzhlybEgy21ugC4QoG+nfSfnU6MAtY9JR7vfferqPQmsOzTOw9bDMdTLZTfhtcklZFrnufnWLe0HM+JLMbi+FSfIpDW3zuedZXQzGyxh/IDw4Wf6/Mo1h8pqbCaktnyOi6T8L1PxlqFj0OE5sJLK7fzDkKOlbYVWM4yzl7FY63dmJKTqI90o62r2pQXnDExpg8aK4gNmytkHoR8DTMx6MVLYdW2u1uo/NNL1JSt3UFq08qCVN+pR2pS9SiR32oyPElp824iLIc/5dlUjOstvLyMKCAAEFBdJ9y/Kms2QMNyvBjofC3ww2opQb8/MAfvWPY/IxzFvFqUW9B/BsfSByFSfaGpzAm+E4WZrTyF2725/Q9qx7NT+ZHY7i20t8JrTpBvc33NSJkzgmKH3THJ1cLUdN/lQTtvUSO2664FXA0LWr6C9KJJNConrP8AIaRzXTPo/t+tO/f71J9X0H6U1T1L+1QNncQ/6n3Ff//EACQQAQEAAgICAQQDAQAAAAAAAAERACExQVGBYXGRobEQwfDR/9oACAEBAAE/EFxCJHa+QcvGUwhESkN9fiYHv0VpOm0o9OFKc/OP0nyMqyNS7O0x2FTnAH6HHF+oMkQP6cPbVCpqdHusdzBsTjjKJKrAZwN411hOmhN3Q+OdYFicQWN6QTJzrwEHC7aJwbcdwysS/AW7GNhVpxF5hwesFHQhHh77IyY3UAIHQHgwuHXP36x4apQUBQcWcXJmcwJakbelduaVYQKsBS3eAVJyB+cQnvn1w6o9ZDI6pnUIiPfdxG9iCBQRXreNy18f2PjFCeB0fH3MMVGwtGz1hMB2JKPunWGBO9cLh1omlDTJZj6GlAPSm3RMCgLAgSfWedY3okyFyEYQWneraZLkQPZNfjHeyDpVdBP7dYuNwjIibANNDHmgaDAeXRptkBMDpGVE6Hqjvltx4aCVkJq9o4gOA4y6pRz7bXiMrUKoBArI5ZdacH0tDvKBFDsjg3oRL8G/tceEiW1Y6UAXW0jxK47AXAL0gOg3HkF1MeStC2JfnHWoCStw/wDe804AlIhCOmmMAu7jZ+Fw0B2U6H2hTGAuDJHQLuYV1xK4W0jhBExMKEcCV+5m01yqkMRn8VrZVUN24oNBOeYF1eI6cEGRpvfJrB+GQLVd9ZIRJROhHod4eCYNA8PTGhag8kNPJivYOADUtVxNVsC44dbHxjhJe2AYOXuc16+cQODu1ej3TF3gwCyreivI5JUksX4fesSG1rUxOwRWcCuJY2pBRrL0AQyrY62UfZjjgyEKuhQpIYLK9dtHzfVC4V2Gib2vOsI+EqqQI4gFwY1Py185FplUDmj8LU84jKWQGhYb+GYoKQdBVa8fLgkXPHI8s+uWSNh+pzh6/EJ6CF0MduPBMS2w88OjhgEW0VUC+Ht6xbrYBeevOWtwmrpdV6RSGDx83G9WqMDRe3oFCj2uUoN4FoGLoFWobz7guMGnj2W4jCzVWD1RwXq8VTsWxxafUEV2R2WOBoHSEnGyczAFEcmv43hrndLdMA4GT4cJ8xi4mEFtpKDrTMB0WxjaWEKwJpfaeX95xkQjKxHJ5FgWWx4WjvAwAztJmx6tsUyaoEOUrgVl3jsSC8V1lgV0fwbzc/pzg9R17zkOp+jGh6Z5/wDGuf2M4nvOf+9ZxOos/wD/xAAvEQABAwMCBQMDAwUAAAAAAAACAQMEAAUREiEGBxMxQSJRcSMzYRQygRUXQpGy/9oACAECAQE/AHIDLhl0i0+VQvHvXF3MCTaZ7kW0voatv+o1FP8ABcKBfhavPM+dcbZESOCsSQki49pzj0dk+Fqy81r3FkSBkfUbkzAdQjVS6I9lQUqw8SWzi6GRRjVSHTrBdiHPaisAEuUNa5mXmBDgLFC4lBubGHmCRDFT8YQsYwtQmHpkpBFOorpepPfPmoPAIq0RPvCCGKYRKv3B71pZV8fqte6ePmuVEmaxxMLDUkWmDTL45RNenslA5lNiT/aVzPmzJt7EHzJ1GQwKKONOd65cxYz9wdUw3ANqfaOQaiC6UHbHxVxYaGzTlfX6aNL37ZqG49CurT0UnWzE8obKay/hPNWtXpdqhSFdV7qsoqmrStqq+cj4WuYPB5SVfu7twJWmAXDKhnT7COKslxnW6cMtvIg0qIW22Pav7i2hwOq7EdF3VujZJiuI+MnrzDVuO30owEmtvuq/lVrh+2G1dbZLfbkDBceDW4iEmlM99SVFVhY7e6l6e5KhLTtmjXAFafQTbwuyjsua454JC4223QLXHajNhJQnhBNO2MZ/NWLl0/Luk39Q2rcQeu2Cki6s9gJK4e5ZXNqUSzxT9KXWbcHOC2T0qlcPWz+k2dmC+YPi1kQXTjLefShJ7pTUiO2pAaIqpjG2MJQft/irl+9Ka+4XwtJ9qmu4/NF99z4D/mv/xAAsEQACAQMCBAUDBQAAAAAAAAABAgMABBEFEgYhMUETFCIycVFSYRUzNGJy/9oACAEDAQE/AE09bmQ+F8kNWhcIsrs9wAYmHp+vMVYcIx291cO+JInQqoPXJ61c8MWzW6Rx5HhoQP7HHKtZ0W5tMJMMhjkN8V5B/uFcM2EodZDAjxMNpY9qtoRyIxtHUVLNawYyBQEM6+nGcVxVFB5F96bmHsI7ZokZ61wdEP0pJcDdJzPqzSnZESprWricTAktWgtNtBfJJ7GtRtYZonWRAQQeXatdsXtr90VNoxyAbIrhDXbaJYrKO1IZurA5yfqajYk7GwAwqWxikOSVNJFHbx5BBP4q9uikEojdC/hnCEg1LNHM5Zwuc9xWnRywSq8UrJjmDitP4ha2neS6kYnwiFB+7NXvFFtawxEOrTPtJUdgetapxVBDZFrdwZPSy5/PUGtS1iO7n8xEGQuPUuT1qEhgx25yerVb9Kv/AHU/8l/8VcftrSe5fmovYK//2Q=="; - @After public void after() { ComponentRepresentation jpegMapper = adminClient.realm("test").components().query(ldapModelId, LDAPStorageMapper.class.getName(), "jpeg-mapper").get(0); @@ -156,18 +158,16 @@ public class LDAPBinaryAttributesTest extends AbstractLDAPTest { // Assert he is found including jpegPhoto joe = getUserAndAssertPhoto("joephoto", true); + // Assert that local storage doesn't contain LDAPConstants.JPEG_PHOTO, it should be stored in the LDAP + String joeId = joe.getId(); + testingClient.server().run(session -> { + RealmModel test = session.realms().getRealmByName("test"); + UserModel userById = session.userLocalStorage().getUserById(joeId, test); + + assertThat(userById.getAttributes().get(LDAPConstants.JPEG_PHOTO), is(nullValue())); + }); - // Try to update him with some big non-LDAP mapped attribute. It will fail - try { - joe.getAttributes().put("someOtherPhoto", Arrays.asList(JPEG_PHOTO_BASE64)); - adminClient.realm("test").users().get(joe.getId()).update(joe); - Assert.fail("Not expected to successfully update user"); - } catch (ClientErrorException cee) { - // Expected - } - // Remove jpegPhoto attribute and assert it was successfully removed - joe.getAttributes().remove("someOtherPhoto"); joe.getAttributes().remove(LDAPConstants.JPEG_PHOTO); adminClient.realm("test").users().get(joe.getId()).update(joe); getUserAndAssertPhoto("joephoto", false); @@ -190,7 +190,7 @@ public class LDAPBinaryAttributesTest extends AbstractLDAPTest { private static LDAPObject addLDAPUser(LDAPStorageProvider ldapProvider, RealmModel realm, final String username, - final String firstName, final String lastName, final String email, String jpegPhoto) { + final String firstName, final String lastName, final String email, String jpegPhoto) { UserModel helperUser = new UserModelDelegate(null) { @Override diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java index 36f0d98784..cf40f0db6f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java @@ -69,6 +69,7 @@ import javax.ws.rs.core.UriBuilder; import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.UUID; import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.RandomStringUtils; @@ -93,7 +94,7 @@ public class LoginTest extends AbstractTestRealmKeycloakTest { @Override public void configureTestRealm(RealmRepresentation testRealm) { UserRepresentation user = UserBuilder.create() - .id("login-test") + .id(UUID.randomUUID().toString()) .username("login-test") .email("login@test.com") .enabled(true) @@ -102,7 +103,7 @@ public class LoginTest extends AbstractTestRealmKeycloakTest { userId = user.getId(); UserRepresentation user2 = UserBuilder.create() - .id("login-test2") + .id(UUID.randomUUID().toString()) .username("login-test2") .email("login2@test.com") .enabled(true) @@ -299,15 +300,15 @@ public class LoginTest extends AbstractTestRealmKeycloakTest { .assertEvent(); } - private void setUserEnabled(String userName, boolean enabled) { - UserRepresentation rep = adminClient.realm("test").users().get(userName).toRepresentation(); + private void setUserEnabled(String id, boolean enabled) { + UserRepresentation rep = adminClient.realm("test").users().get(id).toRepresentation(); rep.setEnabled(enabled); - adminClient.realm("test").users().get(userName).update(rep); + adminClient.realm("test").users().get(id).update(rep); } @Test public void loginInvalidPasswordDisabledUser() { - setUserEnabled("login-test", false); + setUserEnabled(userId, false); try { loginPage.open(); @@ -327,13 +328,13 @@ public class LoginTest extends AbstractTestRealmKeycloakTest { .removeDetail(Details.CONSENT) .assertEvent(); } finally { - setUserEnabled("login-test", true); + setUserEnabled(userId, true); } } @Test public void loginDisabledUser() { - setUserEnabled("login-test", false); + setUserEnabled(userId, false); try { loginPage.open(); @@ -353,7 +354,7 @@ public class LoginTest extends AbstractTestRealmKeycloakTest { .removeDetail(Details.CONSENT) .assertEvent(); } finally { - setUserEnabled("login-test", true); + setUserEnabled(userId, true); } } @@ -548,7 +549,7 @@ public class LoginTest extends AbstractTestRealmKeycloakTest { } finally { setPasswordPolicy(null); - UserResource userRsc = adminClient.realm("test").users().get("login-test"); + UserResource userRsc = adminClient.realm("test").users().get(userId); ApiUtil.resetUserPassword(userRsc, "password", false); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ScriptAuthenticatorTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ScriptAuthenticatorTest.java index ad36217ed1..eec3a3d819 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ScriptAuthenticatorTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ScriptAuthenticatorTest.java @@ -48,6 +48,7 @@ import org.keycloak.testsuite.util.UserBuilder; import javax.ws.rs.core.Response; import java.io.IOException; import java.util.Map; +import java.util.UUID; /** * Tests for {@link org.keycloak.authentication.authenticators.browser.ScriptBasedAuthenticator} @@ -64,6 +65,8 @@ public class ScriptAuthenticatorTest extends AbstractFlowTest { public AssertEvents events = new AssertEvents(this); private AuthenticationFlowRepresentation flow; + private final static String userId = UUID.randomUUID().toString(); + private final static String failId = UUID.randomUUID().toString(); public static final String EXECUTION_ID = "scriptAuth"; @@ -71,7 +74,7 @@ public class ScriptAuthenticatorTest extends AbstractFlowTest { public void configureTestRealm(RealmRepresentation testRealm) { UserRepresentation failUser = UserBuilder.create() - .id("fail") + .id(failId) .username("fail") .email("fail@test.com") .enabled(true) @@ -79,7 +82,7 @@ public class ScriptAuthenticatorTest extends AbstractFlowTest { .build(); UserRepresentation okayUser = UserBuilder.create() - .id("user") + .id(userId) .username("user") .email("user@test.com") .enabled(true) @@ -153,7 +156,7 @@ public class ScriptAuthenticatorTest extends AbstractFlowTest { loginPage.login("user", "password"); - events.expectLogin().user("user").detail(Details.USERNAME, "user").assertEvent(); + events.expectLogin().user(userId).detail(Details.USERNAME, "user").assertEvent(); } /** @@ -184,7 +187,7 @@ public class ScriptAuthenticatorTest extends AbstractFlowTest { loginPage.login("user", "password"); - events.expectLogin().user("user").detail(Details.USERNAME, "user").assertEvent(); + events.expectLogin().user(userId).detail(Details.USERNAME, "user").assertEvent(); } private void addConfigFromFile(String filename) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java index f444253d91..af18b514c6 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/RealmBuilder.java @@ -23,7 +23,7 @@ import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RolesRepresentation; import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.testsuite.events.EventsListenerProviderFactory; +import org.keycloak.testsuite.events.TestEventsListenerProviderFactory; import java.util.ArrayList; import java.util.Collections; @@ -105,16 +105,16 @@ public class RealmBuilder { rep.setEventsListeners(new LinkedList()); } - if (!rep.getEventsListeners().contains(EventsListenerProviderFactory.PROVIDER_ID)) { - rep.getEventsListeners().add(EventsListenerProviderFactory.PROVIDER_ID); + if (!rep.getEventsListeners().contains(TestEventsListenerProviderFactory.PROVIDER_ID)) { + rep.getEventsListeners().add(TestEventsListenerProviderFactory.PROVIDER_ID); } return this; } public RealmBuilder removeTestEventListener() { - if (rep.getEventsListeners() != null && rep.getEventsListeners().contains(EventsListenerProviderFactory.PROVIDER_ID)) { - rep.getEventsListeners().remove(EventsListenerProviderFactory.PROVIDER_ID); + if (rep.getEventsListeners() != null && rep.getEventsListeners().contains(TestEventsListenerProviderFactory.PROVIDER_ID)) { + rep.getEventsListeners().remove(TestEventsListenerProviderFactory.PROVIDER_ID); } return this; 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 695483c8a4..6e073523ea 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 @@ -37,10 +37,12 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; import org.hamcrest.Matchers; import org.junit.Test; + import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeThat; @@ -97,6 +99,7 @@ public class UserModelTest extends KeycloakModelTest { assertTrue(session.users().removeUser(realm, user)); assertFalse(session.users().removeUser(realm, user)); + assertNull(session.users().getUserByUsername(user.getUsername(), realm)); } @Test @@ -191,6 +194,7 @@ public class UserModelTest extends KeycloakModelTest { final RealmModel realm = session.realms().getRealm(realmId); final UserModel user = session.users().addUser(realm, "user-" + i); user.joinGroup(session.groups().getGroupById(realm, groupId)); + log.infof("Created user with id: %s", user.getId()); userIds.add(user.getId()); })); @@ -205,11 +209,11 @@ public class UserModelTest extends KeycloakModelTest { }); }); - inComittedTransaction(1, (session, i) -> { + IntStream.range(0, 7).parallel().forEach(index -> inComittedTransaction(index, (session, i) -> { final RealmModel realm = session.realms().getRealm(realmId); final GroupModel group = session.groups().getGroupById(realm, groupId); assertThat(session.users().getGroupMembersStream(realm, group).count(), is(100L - DELETED_USER_COUNT)); - }); + })); // Now delete the users, and count those that were not found to be deleted. This should be equal to the number // of users removed directly in the user federation. diff --git a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json index c0793b9123..c3c249b23e 100755 --- a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json +++ b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json @@ -42,7 +42,7 @@ }, "user": { - "provider": "${keycloak.user.provider:}" + "provider": "${keycloak.user.provider:jpa}" }, "userFederatedStorage": {