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:
parent
fe7833c957
commit
3f014c7299
11 changed files with 102 additions and 9 deletions
|
@ -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);
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
*
|
*
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue