Skip to content

Commit

Permalink
Fix: admin GUI not working with 1000s of realms
Browse files Browse the repository at this point in the history
Search by RealmName is done before loading all realms when filtering

Closes keycloak#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>
  • Loading branch information
yelhouti and ahus1 authored Aug 21, 2024
1 parent e2d7a94 commit e8840df
Show file tree
Hide file tree
Showing 6 changed files with 33 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,12 @@ public Stream<RealmModel> getRealmsStream() {
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,16 @@ public Stream<RealmModel> getRealmsStream() {
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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
@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"),
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,8 @@ public Stream<RealmNameRepresentation> getRealms(@QueryParam("first") @DefaultVa
@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()));
Expand Down
11 changes: 10 additions & 1 deletion server-spi/src/main/java/org/keycloak/models/RealmProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -101,7 +110,7 @@ default Stream<ClientInitialAccessModel> listClientInitialAccessStream(RealmMode
* 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);
}
Expand Down

0 comments on commit e8840df

Please sign in to comment.