Cannot display 'Authentication Flows' screen when a realm contains more than ~4000 clients (#21058)

closes #21010 

Signed-off-by: Réda Housni Alaoui <reda-alaoui@hey.com>
This commit is contained in:
Réda Housni Alaoui 2023-11-13 19:13:01 +01:00 committed by GitHub
parent fe7833c957
commit 3f014c7299
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 102 additions and 9 deletions

View file

@ -799,6 +799,11 @@ public class RealmAdapter implements CachedRealmModel {
return cacheSession.searchClientsByAttributes(this, attributes, firstResult, maxResults); return cacheSession.searchClientsByAttributes(this, attributes, firstResult, maxResults);
} }
@Override
public Stream<ClientModel> searchClientByAuthenticationFlowBindingOverrides(Map<String, String> overrides, Integer firstResult, Integer maxResults) {
return cacheSession.searchClientsByAuthenticationFlowBindingOverrides(this, overrides, firstResult, maxResults);
}
@Override @Override
public Stream<ClientModel> getClientsStream(Integer firstResult, Integer maxResults) { public Stream<ClientModel> getClientsStream(Integer firstResult, Integer maxResults) {
return cacheSession.getClientsStream(this, firstResult, maxResults); return cacheSession.getClientsStream(this, firstResult, maxResults);

View file

@ -1212,6 +1212,11 @@ public class RealmCacheSession implements CacheRealmProvider {
return getClientDelegate().searchClientsByAttributes(realm, attributes, firstResult, maxResults); return getClientDelegate().searchClientsByAttributes(realm, attributes, firstResult, maxResults);
} }
@Override
public Stream<ClientModel> searchClientsByAuthenticationFlowBindingOverrides(RealmModel realm, Map<String, String> overrides, Integer firstResult, Integer maxResults) {
return getClientDelegate().searchClientsByAuthenticationFlowBindingOverrides(realm, overrides, firstResult, maxResults);
}
@Override @Override
public ClientModel getClientByClientId(RealmModel realm, String clientId) { public ClientModel getClientByClientId(RealmModel realm, String clientId) {
String cacheKey = getClientByClientIdCacheKey(clientId, realm.getId()); String cacheKey = getClientByClientIdCacheKey(clientId, realm.getId());

View file

@ -28,6 +28,8 @@ import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaDelete; import jakarta.persistence.criteria.CriteriaDelete;
import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Join; import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.JoinType;
import jakarta.persistence.criteria.MapJoin;
import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root; import jakarta.persistence.criteria.Root;
import java.util.ArrayList; import java.util.ArrayList;
@ -838,6 +840,51 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
.map(id -> session.clients().getClientById(realm, id)); .map(id -> session.clients().getClientById(realm, id));
} }
@Override
public Stream<ClientModel> searchClientsByAuthenticationFlowBindingOverrides(RealmModel realm, Map<String, String> overrides, Integer firstResult, Integer maxResults) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<String> queryBuilder = builder.createQuery(String.class);
Root<ClientEntity> root = queryBuilder.from(ClientEntity.class);
queryBuilder.select(root.get("id"));
List<Predicate> predicates = new ArrayList<>();
predicates.add(builder.equal(root.get("realmId"), realm.getId()));
//noinspection resource
String dbProductName = em.unwrap(Session.class).doReturningWork(connection -> connection.getMetaData().getDatabaseProductName());
for (Map.Entry<String, String> entry : overrides.entrySet()) {
String bindingName = entry.getKey();
String authenticationFlowId = entry.getValue();
MapJoin<ClientEntity, String, String> authFlowBindings = root.joinMap("authFlowBindings", JoinType.LEFT);
Predicate attrNamePredicate = builder.equal(authFlowBindings.key(), bindingName);
Predicate attrValuePredicate;
if (dbProductName.equals("Oracle")) {
// SELECT * FROM client_attributes WHERE ... DBMS_LOB.COMPARE(value, '0') = 0 ...;
// Oracle is not able to compare a CLOB with a VARCHAR unless it being converted with TO_CHAR
// But for this all values in the table need to be smaller than 4K, otherwise the cast will fail with
// "ORA-22835: Buffer too small for CLOB to CHAR" (even if it is in another row).
// This leaves DBMS_LOB.COMPARE as the option to compare the CLOB with the value.
attrValuePredicate = builder.equal(builder.function("DBMS_LOB.COMPARE", Integer.class, authFlowBindings.value(), builder.literal(authenticationFlowId)), 0);
} else {
attrValuePredicate = builder.equal(authFlowBindings.value(), authenticationFlowId);
}
predicates.add(builder.and(attrNamePredicate, attrValuePredicate));
}
Predicate finalPredicate = builder.and(predicates.toArray(new Predicate[0]));
queryBuilder.where(finalPredicate).orderBy(builder.asc(root.get("clientId")));
TypedQuery<String> query = em.createQuery(queryBuilder);
return closing(paginateQuery(query, firstResult, maxResults).getResultStream())
.map(id -> session.clients().getClientById(realm, id));
}
@Override @Override
public void removeClients(RealmModel realm) { public void removeClients(RealmModel realm) {
TypedQuery<String> query = em.createNamedQuery("getClientIdsByRealm", String.class); TypedQuery<String> query = em.createNamedQuery("getClientIdsByRealm", String.class);

View file

@ -775,6 +775,11 @@ public class RealmAdapter implements LegacyRealmModel, JpaModel<RealmEntity> {
return session.clients().searchClientsByAttributes(this, attributes, firstResult, maxResults); return session.clients().searchClientsByAttributes(this, attributes, firstResult, maxResults);
} }
@Override
public Stream<ClientModel> searchClientByAuthenticationFlowBindingOverrides(Map<String, String> overrides, Integer firstResult, Integer maxResults) {
return session.clients().searchClientsByAuthenticationFlowBindingOverrides(this, overrides, firstResult, maxResults);
}
private static final String BROWSER_HEADER_PREFIX = "_browser_header."; private static final String BROWSER_HEADER_PREFIX = "_browser_header.";
@Override @Override

View file

@ -167,6 +167,11 @@ public class ClientStorageManager implements ClientProvider {
return query((p, f, m) -> p.searchClientsByAttributes(realm, attributes, f, m), realm, firstResult, maxResults); return query((p, f, m) -> p.searchClientsByAttributes(realm, attributes, f, m), realm, firstResult, maxResults);
} }
@Override
public Stream<ClientModel> searchClientsByAuthenticationFlowBindingOverrides(RealmModel realm, Map<String, String> overrides, Integer firstResult, Integer maxResults) {
return query((p, f, m) -> p.searchClientsByAuthenticationFlowBindingOverrides(realm, overrides, f, m), realm, firstResult, maxResults);
}
@FunctionalInterface @FunctionalInterface
interface PaginatedQuery { interface PaginatedQuery {
Stream<ClientModel> query(ClientLookupProvider provider, Integer firstResult, Integer maxResults); Stream<ClientModel> query(ClientLookupProvider provider, Integer firstResult, Integer maxResults);

View file

@ -1,5 +1,6 @@
package org.keycloak.admin.ui.rest.model; package org.keycloak.admin.ui.rest.model;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -15,7 +16,6 @@ public class AuthenticationMapper {
public static Authentication convertToModel(AuthenticationFlowModel flow, RealmModel realm) { public static Authentication convertToModel(AuthenticationFlowModel flow, RealmModel realm) {
final Stream<IdentityProviderModel> identityProviders = realm.getIdentityProvidersStream(); final Stream<IdentityProviderModel> identityProviders = realm.getIdentityProvidersStream();
final Stream<ClientModel> clients = realm.getClientsStream();
final Authentication authentication = new Authentication(); final Authentication authentication = new Authentication();
authentication.setId(flow.getId()); authentication.setId(flow.getId());
@ -30,11 +30,12 @@ public class AuthenticationMapper {
authentication.setUsedBy(new UsedBy(UsedBy.UsedByType.SPECIFIC_PROVIDERS, usedByIdp)); authentication.setUsedBy(new UsedBy(UsedBy.UsedByType.SPECIFIC_PROVIDERS, usedByIdp));
} }
final List<String> usedClients = clients.filter(
c -> c.getAuthenticationFlowBindingOverrides().get("browser") != null && c.getAuthenticationFlowBindingOverrides() Stream<ClientModel> browserFlowOverridingClients = realm.searchClientByAuthenticationFlowBindingOverrides(Collections.singletonMap("browser", flow.getId()), 0, MAX_USED_BY);
.get("browser").equals(flow.getId()) || c.getAuthenticationFlowBindingOverrides() Stream<ClientModel> directGrantFlowOverridingClients = realm.searchClientByAuthenticationFlowBindingOverrides(Collections.singletonMap("direct_grant", flow.getId()), 0, MAX_USED_BY);
.get("direct_grant") != null && c.getAuthenticationFlowBindingOverrides().get("direct_grant").equals(flow.getId())) final List<String> usedClients = Stream.concat(browserFlowOverridingClients, directGrantFlowOverridingClients)
.map(ClientModel::getClientId).limit(MAX_USED_BY).collect(Collectors.toList()); .limit(MAX_USED_BY)
.map(ClientModel::getClientId).collect(Collectors.toList());
if (!usedClients.isEmpty()) { if (!usedClients.isEmpty()) {
authentication.setUsedBy(new UsedBy(UsedBy.UsedByType.SPECIFIC_CLIENTS, usedClients)); authentication.setUsedBy(new UsedBy(UsedBy.UsedByType.SPECIFIC_CLIENTS, usedClients));

View file

@ -1143,6 +1143,12 @@ public class IdentityBrokerStateTestHelpers {
return null; return null;
} }
@Override
public Stream<ClientModel> searchClientByAuthenticationFlowBindingOverrides(Map<String, String> overrides, Integer firstResult, Integer maxResults) {
return null;
}
@Override @Override
public void updateRequiredCredentials(Set<String> creds) { public void updateRequiredCredentials(Set<String> creds) {

View file

@ -54,6 +54,8 @@ public interface ClientModel extends ClientScopeModel, RoleContainerModel, Prot
* is always checked for equality, and the value is checked per the operator. * is always checked for equality, and the value is checked per the operator.
*/ */
public static final SearchableModelField<ClientModel> ATTRIBUTE = new SearchableModelField<>("attribute", String[].class); public static final SearchableModelField<ClientModel> ATTRIBUTE = new SearchableModelField<>("attribute", String[].class);
public static final SearchableModelField<ClientModel> AUTHENTICATION_FLOW_BINDING_OVERRIDE = new SearchableModelField<>("authenticationFlowBindingOverrides", String[].class);
} }
interface ClientCreationEvent extends ProviderEvent { interface ClientCreationEvent extends ProviderEvent {

View file

@ -358,6 +358,8 @@ public interface RealmModel extends RoleContainerModel {
Stream<ClientModel> searchClientByAttributes(Map<String, String> attributes, Integer firstResult, Integer maxResults); Stream<ClientModel> searchClientByAttributes(Map<String, String> attributes, Integer firstResult, Integer maxResults);
Stream<ClientModel> searchClientByAuthenticationFlowBindingOverrides(Map<String, String> overrides, Integer firstResult, Integer maxResults);
void updateRequiredCredentials(Set<String> creds); void updateRequiredCredentials(Set<String> creds);
Map<String, String> getBrowserSecurityHeaders(); Map<String, String> getBrowserSecurityHeaders();

View file

@ -20,9 +20,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel; import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
/** /**
@ -32,7 +30,7 @@ import java.util.stream.Stream;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public interface ClientLookupProvider { public interface ClientLookupProvider {
/** /**
* Exact search for a client by its internal ID. * Exact search for a client by its internal ID.
* @param realm Realm to limit the search. * @param realm Realm to limit the search.
@ -63,6 +61,18 @@ public interface ClientLookupProvider {
Stream<ClientModel> searchClientsByAttributes(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults); Stream<ClientModel> searchClientsByAttributes(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults);
default Stream<ClientModel> searchClientsByAuthenticationFlowBindingOverrides(RealmModel realm, Map<String, String> overrides, Integer firstResult, Integer maxResults) {
Stream<ClientModel> clients = searchClientsByAttributes(realm, Map.of(), null, null)
.filter(client -> overrides.entrySet().stream().allMatch(override -> override.getValue().equals(client.getAuthenticationFlowBindingOverrides().get(override.getKey()))));
if (firstResult != null && firstResult >= 0) {
clients = clients.skip(firstResult);
}
if (maxResults != null && maxResults >= 0 ) {
clients = clients.limit(maxResults);
}
return clients;
}
/** /**
* Return all default scopes (if {@code defaultScope} is {@code true}) or all optional scopes (if {@code defaultScope} is {@code false}) linked with the client * Return all default scopes (if {@code defaultScope} is {@code true}) or all optional scopes (if {@code defaultScope} is {@code false}) linked with the client
* *

View file

@ -97,6 +97,11 @@ public class HardcodedClientStorageProvider implements ClientStorageProvider, Cl
return Stream.empty(); return Stream.empty();
} }
@Override
public Stream<ClientModel> searchClientsByAuthenticationFlowBindingOverrides(RealmModel realm, Map<String, String> overrides, Integer firstResult, Integer maxResults) {
return Stream.empty();
}
@Override @Override
public Map<String, ClientScopeModel> getClientScopes(RealmModel realm, ClientModel client, boolean defaultScope) { public Map<String, ClientScopeModel> getClientScopes(RealmModel realm, ClientModel client, boolean defaultScope) {
if (defaultScope) { if (defaultScope) {