KEYCLOAK-15847 Add MapUserProvider

This commit is contained in:
Michal Hajas 2020-12-10 08:57:53 +01:00 committed by GitHub
parent 3ddedc49f5
commit 8e376aef51
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 2573 additions and 244 deletions

View file

@ -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"

View file

@ -353,7 +353,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
}
@Override
public Stream<UserModel> searchForUserStream(String search, RealmModel realm, int firstResult, int maxResults) {
public Stream<UserModel> searchForUserStream(String search, RealmModel realm, Integer firstResult, Integer maxResults) {
Map<String, String> attributes = new HashMap<String, String>();
attributes.put(UserModel.SEARCH,search);
return searchForUserStream(attributes, realm, firstResult, maxResults);
@ -365,7 +365,7 @@ public class LDAPStorageProvider implements UserStorageProvider,
}
@Override
public Stream<UserModel> searchForUserStream(Map<String, String> params, RealmModel realm, int firstResult, int maxResults) {
public Stream<UserModel> searchForUserStream(Map<String, String> 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<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
public Stream<UserModel> 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<UserModel> getRoleMembersStream(RealmModel realm, RoleModel role, int firstResult, int maxResults) {
public Stream<UserModel> 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))

View file

@ -454,7 +454,7 @@ public class UserCacheSession implements UserCache.Streams {
}
@Override
public Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
public Stream<UserModel> 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<UserModel> getRoleMembersStream(RealmModel realm, RoleModel role, int firstResult, int maxResults) {
public Stream<UserModel> getRoleMembersStream(RealmModel realm, RoleModel role, Integer firstResult, Integer maxResults) {
return getDelegate().getRoleMembersStream(realm, role, firstResult, maxResults);
}
@ -576,7 +576,7 @@ public class UserCacheSession implements UserCache.Streams {
}
@Override
public Stream<UserModel> getUsersStream(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) {
public Stream<UserModel> 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<UserModel> searchForUserStream(String search, RealmModel realm, int firstResult, int maxResults) {
public Stream<UserModel> 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<UserModel> searchForUserStream(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
public Stream<UserModel> searchForUserStream(Map<String, String> attributes, RealmModel realm, Integer firstResult, Integer maxResults) {
return getDelegate().searchForUserStream(attributes, realm, firstResult, maxResults);
}

View file

@ -98,6 +98,18 @@ public class JpaUserProvider implements UserProvider.Streams, UserCredentialStor
credentialStore = new JpaUserCredentialStore(session, em);
}
private static <T> TypedQuery<T> paginateQuery(TypedQuery<T> 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<String> 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<UserModel> getUsersStream(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) {
public Stream<UserModel> getUsersStream(RealmModel realm, Integer firstResult, Integer maxResults, boolean includeServiceAccounts) {
String queryName = includeServiceAccounts ? "getAllUsersByRealm" : "getAllUsersByRealmExcludeServiceAccount" ;
TypedQuery<UserEntity> 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<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
public Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults) {
TypedQuery<UserEntity> 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<UserModel> getRoleMembersStream(RealmModel realm, RoleModel role, int firstResult, int maxResults) {
public Stream<UserModel> getRoleMembersStream(RealmModel realm, RoleModel role, Integer firstResult, Integer maxResults) {
TypedQuery<UserEntity> 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<UserModel> searchForUserStream(String search, RealmModel realm, int firstResult, int maxResults) {
public Stream<UserModel> searchForUserStream(String search, RealmModel realm, Integer firstResult, Integer maxResults) {
Map<String, String> 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<UserModel> searchForUserStream(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
public Stream<UserModel> searchForUserStream(Map<String, String> attributes, RealmModel realm, Integer firstResult, Integer maxResults) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<UserEntity> queryBuilder = builder.createQuery(UserEntity.class);
Root<UserEntity> root = queryBuilder.from(UserEntity.class);
@ -915,16 +902,9 @@ public class JpaUserProvider implements UserProvider.Streams, UserCredentialStor
TypedQuery<UserEntity> 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

View file

@ -263,12 +263,6 @@ public class UserAdapter implements UserModel.Streams, JpaModel<UserEntity> {
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<UserEntity> {
user.getRequiredActions().add(attr);
}
@Override
public void removeRequiredAction(RequiredAction action) {
String actionName = action.name();
removeRequiredAction(actionName);
}
@Override
public void removeRequiredAction(String actionName) {
Iterator<UserRequiredActionEntity> it = user.getRequiredActions().iterator();

View file

@ -110,7 +110,7 @@ public class UserEntity {
@BatchSize(size = 20)
protected Collection<CredentialEntity> credentials;
@OneToMany(mappedBy="user")
@OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="user")
@Fetch(FetchMode.SELECT)
@BatchSize(size = 20)
protected Collection<FederatedIdentityEntity> federatedIdentities;

View file

@ -37,6 +37,11 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View file

@ -51,6 +51,7 @@ public class Serialization {
}
try {
// Naive solution but will do.
@SuppressWarnings("unchecked")
final T res = MAPPER.readValue(MAPPER.writeValueAsBytes(orig), (Class<T>) orig.getClass());
return res;
} catch (IOException ex) {

View file

@ -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<K> implements AbstractEntity<K> {
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<String, List<String>> attributes = new HashMap<>();
private Set<String> requiredActions = new HashSet<>();
private final Map<String, UserCredentialEntity> credentials = new HashMap<>();
private final List<String> credentialsOrder = new LinkedList<>();
private final Map<String, UserFederatedIdentityEntity> federatedIdentities = new HashMap<>();
private final Map<String, UserConsentEntity> userConsents = new HashMap<>();
private Set<String> groupsMembership = new HashSet<>();
private Set<String> rolesMembership = new HashSet<>();
private String federationLink;
private String serviceAccountClientLink;
private int notBefore;
static Comparator<AbstractUserEntity<?>> 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<String, List<String>> getAttributes() {
return attributes;
}
public List<String> getAttribute(String name) {
return attributes.getOrDefault(name, Collections.emptyList());
}
public void setAttributes(Map<String, List<String>> attributes) {
this.updated |= !Objects.equals(this.attributes, attributes);
this.attributes = attributes;
}
public void setAttribute(String name, List<String> 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<String> getRequiredActions() {
return requiredActions;
}
public void setRequiredActions(Set<String> 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<UserCredentialEntity> 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<UserFederatedIdentityEntity> getFederatedIdentities() {
return federatedIdentities.values().stream();
}
public void setFederatedIdentities(Collection<UserFederatedIdentityEntity> 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<UserConsentEntity> 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<String> getGroupsMembership() {
return groupsMembership;
}
public void setGroupsMembership(Set<String> 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<String> getRolesMembership() {
return rolesMembership;
}
public void setRolesMembership(Set<String> 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;
}
}

View file

@ -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<E extends AbstractEntity> 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();
}
}

View file

@ -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<MapUserEntity> {
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<String> 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<String> 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<String> getAttributeStream(String name) {
return getSpecialAttributeValue(name).map(Collections::singletonList)
.orElseGet(() -> entity.getAttribute(name)).stream();
}
@Override
public Map<String, List<String>> getAttributes() {
MultivaluedHashMap<String, String> 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<String> 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<GroupModel> 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<RoleModel> getRealmRoleMappingsStream() {
return getRoleMappingsStream().filter(RoleUtils::isRealmRole);
}
@Override
public Stream<RoleModel> 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<RoleModel> getRoleMappingsStream() {
return entity.getRolesMembership().stream().map(realm::getRoleById);
}
@Override
public void deleteRoleMapping(RoleModel role) {
entity.removeRolesMembership(role.getId());
}
}

View file

@ -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<UUID> {
public static final Comparator<MapUserEntity> COMPARE_BY_USERNAME = Comparator.comparing(MapUserEntity::getUsername, String.CASE_INSENSITIVE_ORDER);
protected MapUserEntity() {
super();
}
public MapUserEntity(UUID id, String realmId) {
super(id, realmId);
}
}

View file

@ -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<MapUserEntity> ALWAYS_FALSE = c -> { return false; };
private final KeycloakSession session;
final MapKeycloakTransaction<UUID, MapUserEntity> tx;
private final MapStorage<UUID, MapUserEntity> userStore;
public MapUserProvider(KeycloakSession session, MapStorage<UUID, MapUserEntity> 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<MapUserEntity, UserModel> entityToAdapterFunc(RealmModel realm) {
// Clone entity before returning back, to avoid giving away a reference to the live object to the caller
return origEntity -> new MapUserAdapter(session, realm, 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<MapUserEntity> 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<MapUserEntity> 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<MapUserEntity> 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<MapUserEntity> getRegisteredEntityById(RealmModel realm, String id) {
return getEntityById(realm, id).map(this::registerEntityForChanges);
}
private Stream<MapUserEntity> getNotRemovedUpdatedUsersStream() {
Stream<MapUserEntity> 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<MapUserEntity> getUnsortedUserEntitiesStream(RealmModel realm) {
return getNotRemovedUpdatedUsersStream()
.filter(entityRealmFilter(realm));
}
private <T> Stream<T> paginatedStream(Stream<T> 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<FederatedIdentityModel> 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<UserConsentModel> 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<MapUserEntity> removeConsentsForExternalClient(String componentId) {
return userEntity -> {
List<UserConsentEntity> 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<MapUserEntity> 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<MapUserEntity> unsortedUserEntitiesStream = getUnsortedUserEntitiesStream(realm);
if (!includeServiceAccount) {
unsortedUserEntitiesStream = unsortedUserEntitiesStream
.filter(userEntity -> Objects.isNull(userEntity.getServiceAccountClientLink()));
}
return (int) unsortedUserEntitiesStream.count();
}
@Override
public Stream<UserModel> getUsersStream(RealmModel realm, Integer firstResult, Integer maxResults, boolean includeServiceAccounts) {
LOG.tracef("getUsersStream(%s, %d, %d, %s)%s", realm, firstResult, maxResults, includeServiceAccounts, getShortStackTrace());
Stream<MapUserEntity> 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<UserModel> getUsersStream(RealmModel realm) {
LOG.tracef("getUsersStream(%s)%s", realm, getShortStackTrace());
return getUsersStream(realm, null, null, false);
}
@Override
public Stream<UserModel> getUsersStream(RealmModel realm, boolean includeServiceAccounts) {
LOG.tracef("getUsersStream(%s)%s", realm, getShortStackTrace());
return getUsersStream(realm, null, null, includeServiceAccounts);
}
@Override
public Stream<UserModel> 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<UserModel> searchForUserStream(String search, RealmModel realm) {
LOG.tracef("searchForUserStream(%s, %s)%s", realm, search, getShortStackTrace());
return searchForUserStream(search, realm, null, null);
}
@Override
public Stream<UserModel> searchForUserStream(String search, RealmModel realm, Integer firstResult, Integer maxResults) {
LOG.tracef("searchForUserStream(%s, %s, %d, %d)%s", realm, search, firstResult, maxResults, getShortStackTrace());
Map<String, String> attributes = new HashMap<>();
attributes.put(UserModel.SEARCH, search);
session.setAttribute(UserModel.INCLUDE_SERVICE_ACCOUNT, false);
return searchForUserStream(attributes, realm, firstResult, maxResults);
}
@Override
public Stream<UserModel> searchForUserStream(Map<String, String> params, RealmModel realm) {
LOG.tracef("searchForUserStream(%s, %s)%s", realm, params, getShortStackTrace());
return searchForUserStream(params, realm, null, null);
}
@Override
public Stream<UserModel> searchForUserStream(Map<String, String> 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<Predicate<MapUserEntity>> 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<String, String> entry : attributes.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (value == null) {
continue;
}
final String searchedString = value.toLowerCase();
Function<Function<MapUserEntity, String>, Predicate<MapUserEntity>> containsOrExactPredicate =
func -> {
return userEntity -> testContainsOrExact(func.apply(userEntity), searchedString, exactSearch);
};
switch (key) {
case UserModel.SEARCH:
List<Predicate<MapUserEntity>> 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<String> userGroups = (Set<String>) session.getAttribute(UserModel.GROUPS);
if (userGroups != null && userGroups.size() > 0) {
final ResourceStore resourceStore = session.getProvider(AuthorizationProvider.class).getStoreFactory()
.getResourceStore();
final Predicate<String> 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<MapUserEntity> 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<MapUserEntity> 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<UserModel> 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<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group) {
LOG.tracef("getGroupMembersStream(%s, %s)%s", realm, group.getId(), getShortStackTrace());
return getGroupMembersStream(realm, group, null, null);
}
@Override
public Stream<UserModel> 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<MapUserEntity> userById = getEntityById(realm, userId);
if (userById.isPresent()) {
tx.remove(UUID.fromString(userId));
return true;
}
return false;
}
@Override
public Stream<UserModel> 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<MapUserEntity> 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<CredentialModel> 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<CredentialModel> 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() {
}
}

View file

@ -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<UserProvider> implements UserProviderFactory {
private MapStorage<UUID, MapUserEntity> 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);
}
}

View file

@ -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<String> 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<String> getGrantedClientScopesIds() {
return grantedClientScopesIds;
}
public void addGrantedClientScopeId(String scope) {
this.updated |= grantedClientScopesIds.add(scope);
}
public void setGrantedClientScopesIds(Set<String> 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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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

View file

@ -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<Integer> user;
@Before
public void init() {
user = new AbstractUserEntity<Integer>(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<Integer> 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);
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*
* This interface provides a way to listen to events that happen during the keycloak run.
* <p/>
* There are two types of events:
* <ul>
* <li>Event - User events (fired when users do some action, like log in, register etc.)</li>
* <li>Admin event - An administrator did some action like client created/updated etc.</li>
* </ul>
*
*
* 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.
* <p/>
* 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.
* <p/>
* 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);
}

View file

@ -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<AdminEventEntry> adminEventsToSend = new LinkedList<>();
private final List<Event> eventsToSend = new LinkedList<>();
private final BiConsumer<AdminEvent, Boolean> adminEventConsumer;
private final Consumer<Event> eventConsumer;
public EventListenerTransaction(BiConsumer<AdminEvent, Boolean> adminEventConsumer, Consumer<Event> 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();
}
}

View file

@ -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
*

View file

@ -30,6 +30,14 @@ public class AuthDetails {
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;
}

View file

@ -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<UserModel> 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();

View file

@ -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<UserModel> 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<UserModel> getUsersStream(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts) {
List<UserModel> value = this.getUsers(realm, firstResult, maxResults, includeServiceAccounts);
default Stream<UserModel> getUsersStream(RealmModel realm, Integer firstResult, Integer maxResults, boolean includeServiceAccounts) {
List<UserModel> 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<UserModel> getUsersStream(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts);
Stream<UserModel> getUsersStream(RealmModel realm, Integer firstResult, Integer maxResults, boolean includeServiceAccounts);
}
}

View file

@ -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);
}

View file

@ -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<UserModel> 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<UserModel> searchForUserStream(String search, RealmModel realm, int firstResult, int maxResults) {
List<UserModel> value = this.searchForUser(search, realm, firstResult, maxResults);
default Stream<UserModel> searchForUserStream(String search, RealmModel realm, Integer firstResult, Integer maxResults) {
List<UserModel> 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<UserModel> searchForUser(Map<String, String> 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<UserModel> searchForUserStream(Map<String, String> params, RealmModel realm, int firstResult, int maxResults) {
List<UserModel> value = this.searchForUser(params, realm, firstResult, maxResults);
default Stream<UserModel> searchForUserStream(Map<String, String> params, RealmModel realm, Integer firstResult, Integer maxResults) {
List<UserModel> 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<UserModel> 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<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
List<UserModel> value = this.getGroupMembers(realm, group, firstResult, maxResults);
default Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults) {
List<UserModel> 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<UserModel> 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<UserModel> 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<UserModel> getRoleMembersStream(RealmModel realm, RoleModel role, int firstResult, int maxResults) {
default Stream<UserModel> getRoleMembersStream(RealmModel realm, RoleModel role, Integer firstResult, Integer maxResults) {
return Stream.empty();
}
@ -506,7 +506,7 @@ public interface UserQueryProvider {
}
@Override
Stream<UserModel> searchForUserStream(String search, RealmModel realm, int firstResult, int maxResults);
Stream<UserModel> searchForUserStream(String search, RealmModel realm, Integer firstResult, Integer maxResults);
@Override
default List<UserModel> searchForUser(Map<String, String> params, RealmModel realm) {
@ -522,7 +522,7 @@ public interface UserQueryProvider {
}
@Override
Stream<UserModel> searchForUserStream(Map<String, String> params, RealmModel realm, int firstResult, int maxResults);
Stream<UserModel> searchForUserStream(Map<String, String> params, RealmModel realm, Integer firstResult, Integer maxResults);
@Override
default List<UserModel> getGroupMembers(RealmModel realm, GroupModel group) {
@ -538,7 +538,7 @@ public interface UserQueryProvider {
}
@Override
Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, int firstResult, int maxResults);
Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults);
@Override
default List<UserModel> searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) {

View file

@ -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<EventType> includedEvents;
private EventListenerTransaction tx = new EventListenerTransaction(null, this::sendEmail);
public EmailEventListenerProvider(KeycloakSession session, EmailTemplateProvider emailTemplateProvider, Set<EventType> 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);
}
}
}

View file

@ -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)) {
@ -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)) {

View file

@ -100,7 +100,7 @@ public abstract class AbstractPartialImport<T> implements PartialImport<T> {
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<T> implements PartialImport<T> {
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;
}
}

View file

@ -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) {

View file

@ -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);
}

View file

@ -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");

View file

@ -128,13 +128,25 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
if (userCache != null) {
userCache.evict(realm, user);
}
// This needs to be running in separate transaction because removing the user may end up with throwing
// PessimisticLockException which also rollbacks Jpa transaction, hence when it is running in current transaction
// it will become not usable for all consequent jpa calls. It will end up with Transaction is in rolled back
// state error
runJobInTransaction(session.getKeycloakSessionFactory(), session -> {
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<UserStorageProvid
}
@Override
public Stream<UserModel> getGroupMembersStream(final RealmModel realm, final GroupModel group, int firstResult, int maxResults) {
public Stream<UserModel> getGroupMembersStream(final RealmModel realm, final GroupModel group, Integer firstResult, Integer maxResults) {
Stream<UserModel> results = query((provider) -> {
if (provider instanceof UserQueryProvider) {
return ((UserQueryProvider)provider).getGroupMembersStream(realm, group);
@ -289,7 +301,7 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
}
@Override
public Stream<UserModel> getRoleMembersStream(final RealmModel realm, final RoleModel role, int firstResult, int maxResults) {
public Stream<UserModel> getRoleMembersStream(final RealmModel realm, final RoleModel role, Integer firstResult, Integer maxResults) {
Stream<UserModel> results = query((provider) -> {
if (provider instanceof UserQueryProvider) {
return ((UserQueryProvider)provider).getRoleMembersStream(realm, role);
@ -316,7 +328,7 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
}
@Override
public Stream<UserModel> getUsersStream(final RealmModel realm, int firstResult, int maxResults, final boolean includeServiceAccounts) {
public Stream<UserModel> getUsersStream(final RealmModel realm, Integer firstResult, Integer maxResults, final boolean includeServiceAccounts) {
Stream<UserModel> 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<UserStorageProvid
}
@Override
public Stream<UserModel> searchForUserStream(String search, RealmModel realm, int firstResult, int maxResults) {
public Stream<UserModel> searchForUserStream(String search, RealmModel realm, Integer firstResult, Integer maxResults) {
Stream<UserModel> results = query((provider) -> {
if (provider instanceof UserQueryProvider) {
return ((UserQueryProvider)provider).searchForUserStream(search, realm);
@ -391,7 +403,7 @@ public class UserStorageManager extends AbstractStorageManager<UserStorageProvid
}
@Override
public Stream<UserModel> searchForUserStream(Map<String, String> attributes, RealmModel realm, int firstResult, int maxResults) {
public Stream<UserModel> searchForUserStream(Map<String, String> attributes, RealmModel realm, Integer firstResult, Integer maxResults) {
Stream<UserModel> results = query((provider) -> {
if (provider instanceof UserQueryProvider) {
if (attributes.containsKey(UserModel.SEARCH)) {

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
public class EventsListenerProvider implements EventListenerProvider {
public class TestEventsListenerProvider implements EventListenerProvider {
private static final BlockingQueue<Event> events = new LinkedBlockingQueue<Event>();
private static final BlockingQueue<AdminEvent> 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;
}
}

View file

@ -26,15 +26,13 @@ import org.keycloak.models.KeycloakSessionFactory;
/**
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
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

View file

@ -243,7 +243,7 @@ public class FailableHardcodedStorageProvider implements UserStorageProvider, Us
}
@Override
public Stream<UserModel> searchForUserStream(String search, RealmModel realm, int firstResult, int maxResults) {
public Stream<UserModel> 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<UserModel> searchForUserStream(Map<String, String> params, RealmModel realm, int firstResult, int maxResults) {
public Stream<UserModel> searchForUserStream(Map<String, String> 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<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
public Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults) {
checkForceFail();
return Stream.empty();
}

View file

@ -315,14 +315,14 @@ public class UserMapStorage implements UserLookupProvider, UserStorageProvider,
}
@Override
public Stream<UserModel> searchForUserStream(String search, RealmModel realm, int firstResult, int maxResults) {
public Stream<UserModel> searchForUserStream(String search, RealmModel realm, Integer firstResult, Integer maxResults) {
String tSearch = translateUserName(search);
Stream<String> 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<UserModel> searchForUserStream(Map<String, String> params, RealmModel realm, int firstResult, int maxResults) {
public Stream<UserModel> searchForUserStream(Map<String, String> params, RealmModel realm, Integer firstResult, Integer maxResults) {
Stream<String> 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<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
public Stream<UserModel> getGroupMembersStream(RealmModel realm, GroupModel group, Integer firstResult, Integer maxResults) {
return getMembershipStream(realm, group, firstResult, maxResults)
.map(userName -> createUser(realm, userName));
}

View file

@ -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();
}

View file

@ -32,4 +32,4 @@
# limitations under the License.
#
org.keycloak.testsuite.events.EventsListenerProviderFactory
org.keycloak.testsuite.events.TestEventsListenerProviderFactory

View file

@ -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<String> eventListeners = new ArrayList<>();
eventListeners.add(JBossLoggingEventListenerProviderFactory.ID);
eventListeners.add(EventsListenerProviderFactory.PROVIDER_ID);
eventListeners.add(TestEventsListenerProviderFactory.PROVIDER_ID);
adminRealmRep.setEventsListeners(eventListeners);
testRealms.add(adminRealmRep);

View file

@ -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);

View file

@ -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);
}

View file

@ -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));
}
}

View file

@ -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()));

View file

@ -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<String> eventListeners = new ArrayList<>();
eventListeners.add(JBossLoggingEventListenerProviderFactory.ID);
eventListeners.add(EventsListenerProviderFactory.PROVIDER_ID);
eventListeners.add(TestEventsListenerProviderFactory.PROVIDER_ID);
adminRealmRep.setEventsListeners(eventListeners);
testRealms.add(adminRealmRep);

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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<String>());
}
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;

View file

@ -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.

View file

@ -42,7 +42,7 @@
},
"user": {
"provider": "${keycloak.user.provider:}"
"provider": "${keycloak.user.provider:jpa}"
},
"userFederatedStorage": {