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 <youssef.elhouti@gmail.com>
Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
Co-authored-by: Alexander Schwartz <aschwart@redhat.com>
This commit is contained in:
yelhouti 2024-08-21 13:58:36 +01:00 committed by GitHub
parent e2d7a94459
commit e8840df0e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 33 additions and 3 deletions

View file

@ -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<RealmModel> 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.

View file

@ -563,6 +563,12 @@ public class RealmCacheSession implements CacheRealmProvider {
return getRealms(getRealmDelegate().getRealmsStream());
}
@Override
public Stream<RealmModel> getRealmsStream(String search) {
// Retrieve realms from backend
return getRealms(getRealmDelegate().getRealmsStream(search));
}
private Stream<RealmModel> getRealms(Stream<RealmModel> backendRealms) {
// Return cache delegates to ensure cache invalidated during write operations
return backendRealms.map(RealmModel::getId).map(this::getRealm);

View file

@ -152,6 +152,16 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
return getRealms(query);
}
@Override
public Stream<RealmModel> getRealmsStream(String search) {
if (search.trim().isEmpty()) {
return getRealmsStream();
}
TypedQuery<String> query = em.createNamedQuery("getRealmIdsWithNameContaining", String.class);
query.setParameter("search", search);
return getRealms(query);
}
private Stream<RealmModel> getRealms(TypedQuery<String> query) {
return closing(query.getResultStream().map(session.realms()::getRealm).filter(Objects::nonNull));
}

View file

@ -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"),
})

View file

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

View file

@ -64,6 +64,15 @@ public interface RealmProvider extends Provider {
*/
Stream<RealmModel> 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<RealmModel> 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);
}