From e8840df0e0fe03c944e420b9e80896d620a2549e Mon Sep 17 00:00:00 2001 From: yelhouti Date: Wed, 21 Aug 2024 13:58:36 +0100 Subject: [PATCH] Fix: admin GUI not working with 1000s of realms Search by RealmName is done before loading all realms when filtering Closes #31956 Signed-off-by: Youssef El Houti Signed-off-by: Alexander Schwartz Co-authored-by: Alexander Schwartz --- .../upgrading/topics/changes/changes-26_0_0.adoc | 5 +++++ .../models/cache/infinispan/RealmCacheSession.java | 6 ++++++ .../org/keycloak/models/jpa/JpaRealmProvider.java | 10 ++++++++++ .../org/keycloak/models/jpa/entities/RealmEntity.java | 1 + .../org/keycloak/admin/ui/rest/UIRealmsResource.java | 3 +-- .../main/java/org/keycloak/models/RealmProvider.java | 11 ++++++++++- 6 files changed, 33 insertions(+), 3 deletions(-) diff --git a/docs/documentation/upgrading/topics/changes/changes-26_0_0.adoc b/docs/documentation/upgrading/topics/changes/changes-26_0_0.adoc index 2739c53f2c..a37922af55 100644 --- a/docs/documentation/upgrading/topics/changes/changes-26_0_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-26_0_0.adoc @@ -145,6 +145,11 @@ The CLI command `kc.[sh|bat] import` now has placeholder replacement enabled. Pr If you wish to disable placeholder replacement for the `import` command, add the system property `-Dkeycloak.migration.replace-placeholders=false` += New Java API to search realms by name + +The `RealmProvider` Java API now contains a new method `Stream getRealmsStream(String search)` which allows searching for a realm by name. +While there is a default implementation which filters the stream after loading it from the provider, implementations are encouraged to provide this with more efficient implementation. + = Keystore and trust store default format change {project_name} now determines the format of the keystore and trust store based on the file extension. If the file extension is `.p12`, `.pkcs12` or `.pfx`, the format is PKCS12. If the file extension is `.jks`, `.keystore` or `.truststore`, the format is JKS. If the file extension is `.pem`, `.crt` or `.key`, the format is PEM. diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java index 397ee71638..653e3d8047 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java @@ -563,6 +563,12 @@ public class RealmCacheSession implements CacheRealmProvider { return getRealms(getRealmDelegate().getRealmsStream()); } + @Override + public Stream getRealmsStream(String search) { + // Retrieve realms from backend + return getRealms(getRealmDelegate().getRealmsStream(search)); + } + private Stream getRealms(Stream backendRealms) { // Return cache delegates to ensure cache invalidated during write operations return backendRealms.map(RealmModel::getId).map(this::getRealm); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java index 9e027a20bd..df4b51e94c 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java @@ -152,6 +152,16 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc return getRealms(query); } + @Override + public Stream getRealmsStream(String search) { + if (search.trim().isEmpty()) { + return getRealmsStream(); + } + TypedQuery query = em.createNamedQuery("getRealmIdsWithNameContaining", String.class); + query.setParameter("search", search); + return getRealms(query); + } + private Stream getRealms(TypedQuery query) { return closing(query.getResultStream().map(session.realms()::getRealm).filter(Objects::nonNull)); } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java index ae68584e45..9b7eafad65 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java @@ -49,6 +49,7 @@ import java.util.Set; @Entity @NamedQueries({ @NamedQuery(name="getAllRealmIds", query="select realm.id from RealmEntity realm"), + @NamedQuery(name="getRealmIdsWithNameContaining", query="select realm.id from RealmEntity realm where LOWER(realm.name) like CONCAT('%', LOWER(:search), '%')"), @NamedQuery(name="getRealmIdByName", query="select realm.id from RealmEntity realm where realm.name = :name"), @NamedQuery(name="getRealmIdsWithProviderType", query="select distinct c.realm.id from ComponentEntity c where c.providerType = :providerType"), }) diff --git a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/UIRealmsResource.java b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/UIRealmsResource.java index 7935db5f46..01c03c28f0 100644 --- a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/UIRealmsResource.java +++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/UIRealmsResource.java @@ -55,9 +55,8 @@ public class UIRealmsResource { @QueryParam("search") @DefaultValue("") String search) { final RealmsPermissionEvaluator eval = AdminPermissions.realms(session, auth.adminAuth()); - return session.realms().getRealmsStream() + return session.realms().getRealmsStream(search) .filter(realm -> eval.canView(realm) || eval.isAdmin(realm)) - .filter(realm -> search.isEmpty() || realm.getName().toLowerCase().contains(search.trim().toLowerCase())) .skip(first) .limit(max) .map((RealmModel realm) -> new RealmNameRepresentation(realm.getName(), realm.getDisplayName())); diff --git a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java index de72b1554a..7061327a40 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java @@ -64,6 +64,15 @@ public interface RealmProvider extends Provider { */ Stream getRealmsStream(); + /** + * Returns realms as a stream filtered by search. + * @param search String to search for in realm names + * @return Stream of {@link RealmModel}. Never returns {@code null}. + */ + default Stream getRealmsStream(String search) { + return getRealmsStream().filter(realm -> search.isEmpty() || realm.getName().toLowerCase().contains(search.trim().toLowerCase())); + } + /** * Returns stream of realms which has component with the given provider type. * @param type {@code Class} Type of the provider. @@ -101,7 +110,7 @@ public interface RealmProvider extends Provider { * Removes all expired client initial accesses from all realms. */ void removeExpiredClientInitialAccess(); - + default void decreaseRemainingCount(RealmModel realm, ClientInitialAccessModel clientInitialAccess) { // Separate provider method to ensure we decrease remainingCount atomically instead of doing classic update realm.decreaseRemainingCount(clientInitialAccess); }