Added ModelIllegalStateException to handle lazy loading exception.

Closes #9645
This commit is contained in:
Alexander Schwartz 2022-01-24 14:59:34 +01:00 committed by Hynek Mlnařík
parent 64cbbde7cf
commit df7ddbf9b3
14 changed files with 236 additions and 70 deletions

View file

@ -0,0 +1,52 @@
/*
* Copyright 2022. Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.storage.jpa;
import org.keycloak.models.ModelIllegalStateException;
import org.keycloak.models.map.common.AbstractEntity;
/**
* Base class for all delegate providers for the JPA storage.
*
* Wraps the delegate so that it can be safely updated during lazy loading.
*/
public abstract class JpaDelegateProvider<T extends JpaRootEntity & AbstractEntity> {
private T delegate;
protected JpaDelegateProvider(T delegate) {
this.delegate = delegate;
}
protected T getDelegate() {
return delegate;
}
/**
* Validates the new entity.
*
* Will throw {@link ModelIllegalStateException} if the entity has been deleted or changed in the meantime.
*/
protected void setDelegate(T newDelegate) {
if (newDelegate == null) {
throw new ModelIllegalStateException("Unable to retrieve entity: " + delegate.getClass().getName() + "#" + delegate.getId());
}
if (newDelegate.getVersion() != delegate.getVersion()) {
throw new ModelIllegalStateException("Version of entity changed between two loads: " + delegate.getClass().getName() + "#" + delegate.getId());
}
this.delegate = newDelegate;
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.storage.jpa;
/**
* Interface for all root entities in the JPA storage.
*/
public interface JpaRootEntity {
/**
* Version of the JPA entity used for optimistic locking
*/
int getVersion();
}

View file

@ -76,6 +76,7 @@ public class JpaClientMapKeycloakTransaction extends JpaKeycloakTransaction impl
Root<JpaClientEntity> root = query.from(JpaClientEntity.class); Root<JpaClientEntity> root = query.from(JpaClientEntity.class);
query.select(cb.construct(JpaClientEntity.class, query.select(cb.construct(JpaClientEntity.class,
root.get("id"), root.get("id"),
root.get("version"),
root.get("entityVersion"), root.get("entityVersion"),
root.get("realmId"), root.get("realmId"),
root.get("clientId"), root.get("clientId"),

View file

@ -28,21 +28,21 @@ import org.keycloak.models.map.client.MapClientEntity;
import org.keycloak.models.map.client.MapClientEntityFields; import org.keycloak.models.map.client.MapClientEntityFields;
import org.keycloak.models.map.common.EntityField; import org.keycloak.models.map.common.EntityField;
import org.keycloak.models.map.common.delegate.DelegateProvider; import org.keycloak.models.map.common.delegate.DelegateProvider;
import org.keycloak.models.map.storage.jpa.JpaDelegateProvider;
import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity; import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity;
public class JpaClientDelegateProvider implements DelegateProvider<MapClientEntity> { public class JpaClientDelegateProvider extends JpaDelegateProvider<JpaClientEntity> implements DelegateProvider<MapClientEntity> {
private JpaClientEntity delegate;
private final EntityManager em; private final EntityManager em;
public JpaClientDelegateProvider(JpaClientEntity delegate, EntityManager em) { public JpaClientDelegateProvider(JpaClientEntity delegate, EntityManager em) {
this.delegate = delegate; super(delegate);
this.em = em; this.em = em;
} }
@Override @Override
public MapClientEntity getDelegate(boolean isRead, Enum<? extends EntityField<MapClientEntity>> field, Object... parameters) { public MapClientEntity getDelegate(boolean isRead, Enum<? extends EntityField<MapClientEntity>> field, Object... parameters) {
if (delegate.isMetadataInitialized()) return delegate; if (getDelegate().isMetadataInitialized()) return getDelegate();
if (isRead) { if (isRead) {
if (field instanceof MapClientEntityFields) { if (field instanceof MapClientEntityFields) {
switch ((MapClientEntityFields) field) { switch ((MapClientEntityFields) field) {
@ -51,26 +51,26 @@ public class JpaClientDelegateProvider implements DelegateProvider<MapClientEnti
case CLIENT_ID: case CLIENT_ID:
case PROTOCOL: case PROTOCOL:
case ENABLED: case ENABLED:
return delegate; return getDelegate();
case ATTRIBUTES: case ATTRIBUTES:
CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<JpaClientEntity> query = cb.createQuery(JpaClientEntity.class); CriteriaQuery<JpaClientEntity> query = cb.createQuery(JpaClientEntity.class);
Root<JpaClientEntity> root = query.from(JpaClientEntity.class); Root<JpaClientEntity> root = query.from(JpaClientEntity.class);
root.fetch("attributes", JoinType.INNER); root.fetch("attributes", JoinType.INNER);
query.select(root).where(cb.equal(root.get("id"), UUID.fromString(delegate.getId()))); query.select(root).where(cb.equal(root.get("id"), UUID.fromString(getDelegate().getId())));
delegate = em.createQuery(query).getSingleResult(); setDelegate(em.createQuery(query).getSingleResult());
break; break;
default: default:
delegate = em.find(JpaClientEntity.class, UUID.fromString(delegate.getId())); setDelegate(em.find(JpaClientEntity.class, UUID.fromString(getDelegate().getId())));
} }
} else throw new IllegalStateException("Not a valid client field: " + field); } else throw new IllegalStateException("Not a valid client field: " + field);
} else { } else {
delegate = em.find(JpaClientEntity.class, UUID.fromString(delegate.getId())); setDelegate(em.find(JpaClientEntity.class, UUID.fromString(getDelegate().getId())));
} }
return delegate; return getDelegate();
} }
} }

View file

@ -45,6 +45,8 @@ import org.keycloak.models.map.client.MapClientEntity.AbstractClientEntity;
import org.keycloak.models.map.client.MapProtocolMapperEntity; import org.keycloak.models.map.client.MapProtocolMapperEntity;
import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.DeepCloner;
import static org.keycloak.models.map.storage.jpa.Constants.SUPPORTED_VERSION_CLIENT; import static org.keycloak.models.map.storage.jpa.Constants.SUPPORTED_VERSION_CLIENT;
import org.keycloak.models.map.storage.jpa.JpaRootEntity;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType; import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
/** /**
@ -60,7 +62,7 @@ import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
) )
}) })
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)}) @TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
public class JpaClientEntity extends AbstractClientEntity implements Serializable { public class JpaClientEntity extends AbstractClientEntity implements Serializable, JpaRootEntity {
@Id @Id
@Column @Column
@ -113,9 +115,10 @@ public class JpaClientEntity extends AbstractClientEntity implements Serializabl
* Used by hibernate when calling cb.construct from read(QueryParameters) method. * Used by hibernate when calling cb.construct from read(QueryParameters) method.
* It is used to select client without metadata(json) field. * It is used to select client without metadata(json) field.
*/ */
public JpaClientEntity(UUID id, Integer entityVersion, String realmId, String clientId, public JpaClientEntity(UUID id, int version, Integer entityVersion, String realmId, String clientId,
String protocol, Boolean enabled) { String protocol, Boolean enabled) {
this.id = id; this.id = id;
this.version = version;
this.entityVersion = entityVersion; this.entityVersion = entityVersion;
this.realmId = realmId; this.realmId = realmId;
this.clientId = clientId; this.clientId = clientId;
@ -148,6 +151,7 @@ public class JpaClientEntity extends AbstractClientEntity implements Serializabl
metadata.setEntityVersion(entityVersion); metadata.setEntityVersion(entityVersion);
} }
@Override
public int getVersion() { public int getVersion() {
return version; return version;
} }

View file

@ -76,6 +76,7 @@ public class JpaRoleMapKeycloakTransaction extends JpaKeycloakTransaction implem
Root<JpaRoleEntity> root = query.from(JpaRoleEntity.class); Root<JpaRoleEntity> root = query.from(JpaRoleEntity.class);
query.select(cb.construct(JpaRoleEntity.class, query.select(cb.construct(JpaRoleEntity.class,
root.get("id"), root.get("id"),
root.get("version"),
root.get("entityVersion"), root.get("entityVersion"),
root.get("realmId"), root.get("realmId"),
root.get("clientId"), root.get("clientId"),

View file

@ -26,21 +26,21 @@ import org.keycloak.models.map.common.EntityField;
import org.keycloak.models.map.common.delegate.DelegateProvider; import org.keycloak.models.map.common.delegate.DelegateProvider;
import org.keycloak.models.map.role.MapRoleEntity; import org.keycloak.models.map.role.MapRoleEntity;
import org.keycloak.models.map.role.MapRoleEntityFields; import org.keycloak.models.map.role.MapRoleEntityFields;
import org.keycloak.models.map.storage.jpa.JpaDelegateProvider;
import org.keycloak.models.map.storage.jpa.role.entity.JpaRoleEntity; import org.keycloak.models.map.storage.jpa.role.entity.JpaRoleEntity;
public class JpaRoleDelegateProvider implements DelegateProvider<MapRoleEntity> { public class JpaRoleDelegateProvider extends JpaDelegateProvider<JpaRoleEntity> implements DelegateProvider<MapRoleEntity> {
private JpaRoleEntity delegate;
private final EntityManager em; private final EntityManager em;
public JpaRoleDelegateProvider(JpaRoleEntity delegate, EntityManager em) { public JpaRoleDelegateProvider(JpaRoleEntity delegate, EntityManager em) {
this.delegate = delegate; super(delegate);
this.em = em; this.em = em;
} }
@Override @Override
public JpaRoleEntity getDelegate(boolean isRead, Enum<? extends EntityField<MapRoleEntity>> field, Object... parameters) { public JpaRoleEntity getDelegate(boolean isRead, Enum<? extends EntityField<MapRoleEntity>> field, Object... parameters) {
if (delegate.isMetadataInitialized()) return delegate; if (getDelegate().isMetadataInitialized()) return getDelegate();
if (isRead) { if (isRead) {
if (field instanceof MapRoleEntityFields) { if (field instanceof MapRoleEntityFields) {
switch ((MapRoleEntityFields) field) { switch ((MapRoleEntityFields) field) {
@ -49,26 +49,26 @@ public class JpaRoleDelegateProvider implements DelegateProvider<MapRoleEntity>
case CLIENT_ID: case CLIENT_ID:
case NAME: case NAME:
case DESCRIPTION: case DESCRIPTION:
return delegate; return getDelegate();
case ATTRIBUTES: case ATTRIBUTES:
CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<JpaRoleEntity> query = cb.createQuery(JpaRoleEntity.class); CriteriaQuery<JpaRoleEntity> query = cb.createQuery(JpaRoleEntity.class);
Root<JpaRoleEntity> root = query.from(JpaRoleEntity.class); Root<JpaRoleEntity> root = query.from(JpaRoleEntity.class);
root.fetch("attributes", JoinType.INNER); root.fetch("attributes", JoinType.INNER);
query.select(root).where(cb.equal(root.get("id"), UUID.fromString(delegate.getId()))); query.select(root).where(cb.equal(root.get("id"), UUID.fromString(getDelegate().getId())));
delegate = em.createQuery(query).getSingleResult(); setDelegate(em.createQuery(query).getSingleResult());
break; break;
default: default:
delegate = em.find(JpaRoleEntity.class, UUID.fromString(delegate.getId())); setDelegate(em.find(JpaRoleEntity.class, UUID.fromString(getDelegate().getId())));
} }
} else throw new IllegalStateException("Not a valid role field: " + field); } else throw new IllegalStateException("Not a valid role field: " + field);
} else { } else {
delegate = em.find(JpaRoleEntity.class, UUID.fromString(delegate.getId())); setDelegate(em.find(JpaRoleEntity.class, UUID.fromString(getDelegate().getId())));
} }
return delegate; return getDelegate();
} }
} }

View file

@ -43,6 +43,8 @@ import org.hibernate.annotations.TypeDefs;
import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.role.MapRoleEntity.AbstractRoleEntity; import org.keycloak.models.map.role.MapRoleEntity.AbstractRoleEntity;
import static org.keycloak.models.map.storage.jpa.Constants.SUPPORTED_VERSION_ROLE; import static org.keycloak.models.map.storage.jpa.Constants.SUPPORTED_VERSION_ROLE;
import org.keycloak.models.map.storage.jpa.JpaRootEntity;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType; import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
/** /**
@ -53,7 +55,7 @@ import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
@Entity @Entity
@Table(name = "role", uniqueConstraints = {@UniqueConstraint(columnNames = {"realmId", "clientId", "name"})}) @Table(name = "role", uniqueConstraints = {@UniqueConstraint(columnNames = {"realmId", "clientId", "name"})})
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)}) @TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
public class JpaRoleEntity extends AbstractRoleEntity implements Serializable { public class JpaRoleEntity extends AbstractRoleEntity implements Serializable, JpaRootEntity {
@Id @Id
@Column @Column
@ -106,8 +108,9 @@ public class JpaRoleEntity extends AbstractRoleEntity implements Serializable {
* Used by hibernate when calling cb.construct from read(QueryParameters) method. * Used by hibernate when calling cb.construct from read(QueryParameters) method.
* It is used to select role without metadata(json) field. * It is used to select role without metadata(json) field.
*/ */
public JpaRoleEntity(UUID id, Integer entityVersion, String realmId, String clientId, String name, String description) { public JpaRoleEntity(UUID id, int version, Integer entityVersion, String realmId, String clientId, String name, String description) {
this.id = id; this.id = id;
this.version = version;
this.entityVersion = entityVersion; this.entityVersion = entityVersion;
this.realmId = realmId; this.realmId = realmId;
this.clientId = clientId; this.clientId = clientId;
@ -140,6 +143,7 @@ public class JpaRoleEntity extends AbstractRoleEntity implements Serializable {
metadata.setEntityVersion(entityVersion); metadata.setEntityVersion(entityVersion);
} }
@Override
public int getVersion() { public int getVersion() {
return version; return version;
} }

View file

@ -19,6 +19,7 @@ package org.keycloak.models.delegate;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel; import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelIllegalStateException;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
@ -83,7 +84,7 @@ public class ClientModelLazyDelegate implements ClientModel {
} }
ClientModel ref = delegate.getReference(); ClientModel ref = delegate.getReference();
if (ref == null) { if (ref == null) {
throw new IllegalStateException("Invalid delegate obtained"); throw new ModelIllegalStateException("Invalid delegate obtained");
} }
return ref; return ref;
} }

View file

@ -17,6 +17,7 @@
package org.keycloak.models.utils; package org.keycloak.models.utils;
import org.jboss.logging.Logger;
import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.PermissionTicket; import org.keycloak.authorization.model.PermissionTicket;
import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.Policy;
@ -46,8 +47,10 @@ import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -101,6 +104,7 @@ public class ModelToRepresentation {
REALM_EXCLUDED_ATTRIBUTES.add(Constants.CLIENT_PROFILES); REALM_EXCLUDED_ATTRIBUTES.add(Constants.CLIENT_PROFILES);
} }
private static final Logger LOG = Logger.getLogger(ModelToRepresentation.class);
public static void buildGroupPath(StringBuilder sb, GroupModel group) { public static void buildGroupPath(StringBuilder sb, GroupModel group) {
if (group.getParent() != null) { if (group.getParent() != null) {
@ -554,6 +558,25 @@ public class ModelToRepresentation {
return rep; return rep;
} }
/**
* Handles exceptions that occur when transforming the model to a representation and will remove
* all null objects from the stream.
*
* Entities that have been removed from the store or where a lazy loading exception occurs will not show up
* in the output stream.
*/
public static <M, R> Stream<R> filterValidRepresentations(Stream<M> models, Function<M, R> transformer) {
return models.map(m -> {
try {
return transformer.apply(m);
} catch (ModelIllegalStateException e) {
LOG.warn("unable to retrieve model information, skipping entity", e);
return null;
}
})
.filter(Objects::nonNull);
}
public static CredentialRepresentation toRepresentation(UserCredentialModel cred) { public static CredentialRepresentation toRepresentation(UserCredentialModel cred) {
CredentialRepresentation rep = new CredentialRepresentation(); CredentialRepresentation rep = new CredentialRepresentation();
rep.setType(CredentialRepresentation.SECRET); rep.setType(CredentialRepresentation.SECRET);

View file

@ -0,0 +1,49 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models;
import java.util.function.Function;
import java.util.stream.Stream;
/**
* Thrown when data can't be retrieved for the model.
*
* This occurs when an entity has been removed or updated in the meantime. This might wrap an optimistic lock exception
* depending on the store.
*
* Callers might use this exception to filter out entities that are in an illegal state, see
* <code>org.keycloak.models.utils.ModelToRepresentation#toRepresentation(Stream, Function)</code>
*
* @author <a href="mailto:aschwart@redhat.com">Alexander Schwartz</a>
*/
public class ModelIllegalStateException extends ModelException {
public ModelIllegalStateException() {
}
public ModelIllegalStateException(String message) {
super(message);
}
public ModelIllegalStateException(String message, Throwable cause) {
super(message, cause);
}
public ModelIllegalStateException(Throwable cause) {
super(cause);
}
}

View file

@ -17,22 +17,11 @@
package org.keycloak.exportimport.util; package org.keycloak.exportimport.util;
import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation; import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonFactory;
import java.io.IOException; import com.fasterxml.jackson.core.JsonGenerator;
import java.io.OutputStream; import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList; import com.fasterxml.jackson.databind.SerializationFeature;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.AuthorizationProviderFactory; import org.keycloak.authorization.AuthorizationProviderFactory;
import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.Policy;
@ -71,11 +60,20 @@ import org.keycloak.representations.idm.authorization.ResourceServerRepresentati
import org.keycloak.representations.idm.authorization.ScopeRepresentation; import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import com.fasterxml.jackson.core.JsonEncoding; import java.io.IOException;
import com.fasterxml.jackson.core.JsonFactory; import java.io.OutputStream;
import com.fasterxml.jackson.core.JsonGenerator; import java.util.ArrayList;
import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Collection;
import com.fasterxml.jackson.databind.SerializationFeature; import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -103,15 +101,17 @@ public class ExportUtils {
.map(ClientScopeModel::getName).collect(Collectors.toList())); .map(ClientScopeModel::getName).collect(Collectors.toList()));
// Clients // Clients
List<ClientModel> clients = Collections.emptyList(); List<ClientModel> clients = new LinkedList<>();
if (options.isClientsIncluded()) { if (options.isClientsIncluded()) {
clients = realm.getClientsStream() // we iterate over all clients in the stream.
.filter(c -> { try { c.getClientId(); return true; } catch (Exception ex) { return false; } } ) // only those client models that can be translated into a valid client representation will be added to the client list
.collect(Collectors.toList()); // that is later used to retrieve related information about groups and roles
List<ClientRepresentation> clientReps = clients.stream() List<ClientRepresentation> clientReps = ModelToRepresentation.filterValidRepresentations(realm.getClientsStream(), app -> {
.map(app -> exportClient(session, app)) ClientRepresentation clientRepresentation = exportClient(session, app);
.collect(Collectors.toList()); clients.add(app);
return clientRepresentation;
}).collect(Collectors.toList());
rep.setClients(clientReps); rep.setClients(clientReps);
} }

View file

@ -33,6 +33,7 @@ import org.keycloak.constants.AdapterConstants;
import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelIllegalStateException;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol;
@ -241,15 +242,18 @@ public class ResourceAdminManager {
public GlobalRequestResult logoutAll(RealmModel realm) { public GlobalRequestResult logoutAll(RealmModel realm) {
realm.setNotBefore(Time.currentTime()); realm.setNotBefore(Time.currentTime());
Stream<ClientModel> resources = realm.getClientsStream()
.filter(c -> { try { c.getClientId(); return true; } catch (Exception ex) { return false; } } );
GlobalRequestResult finalResult = new GlobalRequestResult(); GlobalRequestResult finalResult = new GlobalRequestResult();
AtomicInteger counter = new AtomicInteger(0); AtomicInteger counter = new AtomicInteger(0);
resources.forEach(r -> { realm.getClientsStream().forEach(c -> {
try {
counter.getAndIncrement(); counter.getAndIncrement();
GlobalRequestResult currentResult = logoutClient(realm, r, realm.getNotBefore()); GlobalRequestResult currentResult = logoutClient(realm, c, realm.getNotBefore());
finalResult.addAll(currentResult); finalResult.addAll(currentResult);
} catch (ModelIllegalStateException ex) {
// currently, GlobalRequestResult doesn't allow for information about clients that we were unable to retrieve.
logger.warn("unable to retrieve client information for logout, skipping resource", ex);
}
}); });
logger.debugv("logging out {0} resources ", counter); logger.debugv("logging out {0} resources ", counter);

View file

@ -57,7 +57,6 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream; import java.util.stream.Stream;
import static java.lang.Boolean.TRUE; import static java.lang.Boolean.TRUE;
@ -87,9 +86,11 @@ public class ClientsResource {
} }
/** /**
* Get clients belonging to the realm * Get clients belonging to the realm.
* *
* Returns a list of clients belonging to the realm * If a client can't be retrieved from the storage due to a problem with the underlying storage,
* it is silently removed from the returned list.
* This ensures that concurrent modifications to the list don't prevent callers from retrieving this list.
* *
* @param clientId filter by clientId * @param clientId filter by clientId
* @param viewableOnly filter clients that cannot be viewed in full by admin * @param viewableOnly filter clients that cannot be viewed in full by admin
@ -131,9 +132,8 @@ public class ClientsResource {
} }
} }
Stream<ClientRepresentation> s = clientModels Stream<ClientRepresentation> s = ModelToRepresentation.filterValidRepresentations(clientModels,
.filter(c -> { try { c.getClientId(); return true; } catch (Exception ex) { return false; } } ) c -> {
.map(c -> {
ClientRepresentation representation = null; ClientRepresentation representation = null;
if (canView || auth.clients().canView(c)) { if (canView || auth.clients().canView(c)) {
representation = ModelToRepresentation.toRepresentation(c, session); representation = ModelToRepresentation.toRepresentation(c, session);
@ -146,8 +146,7 @@ public class ClientsResource {
} }
return representation; return representation;
}) });
.filter(Objects::nonNull);
if (!canView) { if (!canView) {
s = paginatedStream(s, firstResult, maxResults); s = paginatedStream(s, firstResult, maxResults);