Skip to content

Commit

Permalink
Cannot display 'Authentication Flows' screen when a realm contains mo…
Browse files Browse the repository at this point in the history
…re than ~4000 clients (keycloak#21058)

closes keycloak#21010 

Signed-off-by: Réda Housni Alaoui <reda-alaoui@hey.com>
  • Loading branch information
reda-alaoui authored and sschu committed Nov 15, 2023
1 parent bfa1b2e commit 5d32855
Show file tree
Hide file tree
Showing 11 changed files with 102 additions and 9 deletions.
Expand Up @@ -799,6 +799,11 @@ public Stream<ClientModel> searchClientByAttributes(Map<String, String> attribut
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
public Stream<ClientModel> getClientsStream(Integer firstResult, Integer maxResults) {
return cacheSession.getClientsStream(this, firstResult, maxResults);
Expand Down
Expand Up @@ -1216,6 +1216,11 @@ public Stream<ClientModel> searchClientsByAttributes(RealmModel realm, Map<Strin
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
public ClientModel getClientByClientId(RealmModel realm, String clientId) {
String cacheKey = getClientByClientIdCacheKey(clientId, realm.getId());
Expand Down
Expand Up @@ -28,6 +28,8 @@
import jakarta.persistence.criteria.CriteriaDelete;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.JoinType;
import jakarta.persistence.criteria.MapJoin;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import java.util.ArrayList;
Expand Down Expand Up @@ -871,6 +873,51 @@ public Stream<ClientModel> searchClientsByAttributes(RealmModel realm, Map<Strin
.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
public void removeClients(RealmModel realm) {
TypedQuery<String> query = em.createNamedQuery("getClientIdsByRealm", String.class);
Expand Down
Expand Up @@ -775,6 +775,11 @@ public Stream<ClientModel> searchClientByAttributes(Map<String, String> attribut
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.";

@Override
Expand Down
Expand Up @@ -167,6 +167,11 @@ public Stream<ClientModel> searchClientsByAttributes(RealmModel realm, Map<Strin
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
interface PaginatedQuery {
Stream<ClientModel> query(ClientLookupProvider provider, Integer firstResult, Integer maxResults);
Expand Down
@@ -1,5 +1,6 @@
package org.keycloak.admin.ui.rest.model;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand All @@ -15,7 +16,6 @@ public class AuthenticationMapper {
public static Authentication convertToModel(AuthenticationFlowModel flow, RealmModel realm) {

final Stream<IdentityProviderModel> identityProviders = realm.getIdentityProvidersStream();
final Stream<ClientModel> clients = realm.getClientsStream();

final Authentication authentication = new Authentication();
authentication.setId(flow.getId());
Expand All @@ -30,11 +30,12 @@ public static Authentication convertToModel(AuthenticationFlowModel flow, RealmM
authentication.setUsedBy(new UsedBy(UsedBy.UsedByType.SPECIFIC_PROVIDERS, usedByIdp));
}

final List<String> usedClients = clients.filter(
c -> c.getAuthenticationFlowBindingOverrides().get("browser") != null && c.getAuthenticationFlowBindingOverrides()
.get("browser").equals(flow.getId()) || c.getAuthenticationFlowBindingOverrides()
.get("direct_grant") != null && c.getAuthenticationFlowBindingOverrides().get("direct_grant").equals(flow.getId()))
.map(ClientModel::getClientId).limit(MAX_USED_BY).collect(Collectors.toList());

Stream<ClientModel> browserFlowOverridingClients = realm.searchClientByAuthenticationFlowBindingOverrides(Collections.singletonMap("browser", flow.getId()), 0, MAX_USED_BY);
Stream<ClientModel> directGrantFlowOverridingClients = realm.searchClientByAuthenticationFlowBindingOverrides(Collections.singletonMap("direct_grant", flow.getId()), 0, MAX_USED_BY);
final List<String> usedClients = Stream.concat(browserFlowOverridingClients, directGrantFlowOverridingClients)
.limit(MAX_USED_BY)
.map(ClientModel::getClientId).collect(Collectors.toList());

if (!usedClients.isEmpty()) {
authentication.setUsedBy(new UsedBy(UsedBy.UsedByType.SPECIFIC_CLIENTS, usedClients));
Expand Down
Expand Up @@ -1143,6 +1143,12 @@ public Stream<ClientModel> searchClientByAttributes(Map<String, String> attribut
return null;
}

@Override
public Stream<ClientModel> searchClientByAuthenticationFlowBindingOverrides(Map<String, String> overrides, Integer firstResult, Integer maxResults) {
return null;
}


@Override
public void updateRequiredCredentials(Set<String> creds) {

Expand Down
2 changes: 2 additions & 0 deletions server-spi/src/main/java/org/keycloak/models/ClientModel.java
Expand Up @@ -54,6 +54,8 @@ public static class SearchableFields {
* 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> AUTHENTICATION_FLOW_BINDING_OVERRIDE = new SearchableModelField<>("authenticationFlowBindingOverrides", String[].class);
}

interface ClientCreationEvent extends ProviderEvent {
Expand Down
2 changes: 2 additions & 0 deletions server-spi/src/main/java/org/keycloak/models/RealmModel.java
Expand Up @@ -358,6 +358,8 @@ default Boolean getAttribute(String name, Boolean defaultValue) {

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

Map<String, String> getBrowserSecurityHeaders();
Expand Down
Expand Up @@ -20,9 +20,7 @@
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.RealmModel;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
Expand All @@ -32,7 +30,7 @@
* @version $Revision: 1 $
*/
public interface ClientLookupProvider {

/**
* Exact search for a client by its internal ID.
* @param realm Realm to limit the search.
Expand Down Expand Up @@ -63,6 +61,18 @@ public interface ClientLookupProvider {

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
*
Expand Down
Expand Up @@ -97,6 +97,11 @@ public Stream<ClientModel> searchClientsByAttributes(RealmModel realm, Map<Strin
return Stream.empty();
}

@Override
public Stream<ClientModel> searchClientsByAuthenticationFlowBindingOverrides(RealmModel realm, Map<String, String> overrides, Integer firstResult, Integer maxResults) {
return Stream.empty();
}

@Override
public Map<String, ClientScopeModel> getClientScopes(RealmModel realm, ClientModel client, boolean defaultScope) {
if (defaultScope) {
Expand Down

0 comments on commit 5d32855

Please sign in to comment.