From dab794ebca0be31434c1026cb496b98a559759c3 Mon Sep 17 00:00:00 2001 From: Iestyn <33298011+IestynGage@users.noreply.github.com> Date: Sat, 28 Mar 2026 17:58:52 +0000 Subject: [PATCH] Add `@DefaultValue` to max param used in Admin REST APIs This is used to generate Keycloak Admin REST API. Without this a 'null' value is used, which suggests that returning a paginated list is optional. Closes #47560 Signed-off-by: Iestyn <33298011+IestynGage@users.noreply.github.com> --- .../src/main/java/org/keycloak/models/Constants.java | 4 ++++ .../keycloak/authorization/admin/PolicyService.java | 3 ++- .../authorization/admin/ResourceSetService.java | 5 +++-- .../services/resources/admin/ClientResource.java | 9 +++++---- .../services/resources/admin/RealmAdminResource.java | 2 +- .../resources/admin/RoleContainerResource.java | 12 ++++++------ 6 files changed, 21 insertions(+), 14 deletions(-) diff --git a/server-spi-private/src/main/java/org/keycloak/models/Constants.java b/server-spi-private/src/main/java/org/keycloak/models/Constants.java index 560b63cb6b12..41b077be71ac 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/Constants.java +++ b/server-spi-private/src/main/java/org/keycloak/models/Constants.java @@ -130,6 +130,10 @@ public final class Constants { public static final String GENERATE = "GENERATE"; public static final int DEFAULT_MAX_RESULTS = 100; + /** + * Used by {@code DefaultValue} annotation for when a REST endpoints max size default is set by {@link #DEFAULT_MAX_RESULTS}. + */ + public static final String DEFAULT_MAX_RESULTS_STR = "" + DEFAULT_MAX_RESULTS; // Delimiter to be used in the configuration of authenticators (and some other components) in case that we need to save // multiple values into single string diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java index cd05297ab920..ab2488a7786f 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java @@ -26,6 +26,7 @@ import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; @@ -216,7 +217,7 @@ public Response findAll(@QueryParam("policyId") String id, @QueryParam("owner") String owner, @QueryParam("fields") String fields, @QueryParam("first") Integer firstResult, - @QueryParam("max") Integer maxResult) { + @QueryParam("max") @DefaultValue(Constants.DEFAULT_MAX_RESULTS_STR) Integer maxResult) { if (auth != null) { this.auth.realm().requireViewAuthorization(resourceServer); } diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java index fc8a8cf88224..16ab292cae8b 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java @@ -31,6 +31,7 @@ import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.POST; @@ -402,7 +403,7 @@ public Response find(@QueryParam("_id") String id, @QueryParam("exactName") Boolean exactName, @QueryParam("deep") Boolean deep, @QueryParam("first") Integer firstResult, - @QueryParam("max") Integer maxResult) { + @QueryParam("max") @DefaultValue(Constants.DEFAULT_MAX_RESULTS_STR) Integer maxResult) { return find(id, name, uri, owner, type, scope, matchingUri, exactName, deep, firstResult, maxResult, (BiFunction) (resource, deep1) -> toRepresentation(resource, resourceServer, authorization, deep1)); } @@ -416,7 +417,7 @@ public Response find(@QueryParam("_id") String id, @QueryParam("exactName") Boolean exactName, @QueryParam("deep") Boolean deep, @QueryParam("first") Integer firstResult, - @QueryParam("max") Integer maxResult, + @QueryParam("max") @DefaultValue(Constants.DEFAULT_MAX_RESULTS_STR) Integer maxResult, BiFunction toRepresentation) { requireView(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java index 9bbe16947c24..e901ffaa5290 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java @@ -24,6 +24,7 @@ import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.POST; @@ -569,8 +570,8 @@ public Map getApplicationSessionCount() { @NoCache @Produces(MediaType.APPLICATION_JSON) @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) - @Operation( summary = "Get user sessions for client Returns a list of user sessions associated with this client\n") - public Stream getUserSessions(@Parameter(description = "Paging offset") @QueryParam("first") Integer firstResult, @Parameter(description = "Maximum results size (defaults to 100)") @QueryParam("max") Integer maxResults) { + @Operation( summary = "Get user sessions for client. Returns a list of user sessions associated with this client.\n") + public Stream getUserSessions(@Parameter(description = "Paging offset") @QueryParam("first") Integer firstResult, @Parameter(description = "Maximum results size.") @QueryParam("max") @DefaultValue(Constants.DEFAULT_MAX_RESULTS_STR) Integer maxResults) { auth.clients().requireView(client); return session.sessions() .readOnlyStreamUserSessions(client.getRealm(), client, computeFirstResult(firstResult), computeMaxResults(maxResults)) @@ -616,8 +617,8 @@ public Map getOfflineSessionCount() { @NoCache @Produces(MediaType.APPLICATION_JSON) @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) - @Operation( summary = "Get offline sessions for client Returns a list of offline user sessions associated with this client") - public Stream getOfflineUserSessions(@Parameter(description = "Paging offset") @QueryParam("first") Integer firstResult, @Parameter(description = "Maximum results size (defaults to 100)") @QueryParam("max") Integer maxResults) { + @Operation( summary = "Get offline sessions for client. Returns a list of offline user sessions associated with this client") + public Stream getOfflineUserSessions(@Parameter(description = "Paging offset") @QueryParam("first") Integer firstResult, @Parameter(description = "Maximum results size.") @QueryParam("max") @DefaultValue(Constants.DEFAULT_MAX_RESULTS_STR) Integer maxResults) { auth.clients().requireView(client); return session.sessions() .readOnlyStreamOfflineUserSessions(client.getRealm(), client, computeFirstResult(firstResult), computeMaxResults(maxResults)) diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index 8c9f424f3d00..226dee90a271 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -902,7 +902,7 @@ public Stream getEvents(@Parameter(description = "The types @Parameter(description = "To (inclusive) date (yyyy-MM-dd) or time in Epoch timestamp millis (number of milliseconds since January 1, 1970, 00:00:00 GMT)") @QueryParam("dateTo") String dateTo, @Parameter(description = "IP Address") @QueryParam("ipAddress") String ipAddress, @Parameter(description = "Paging offset") @QueryParam("first") Integer firstResult, - @Parameter(description = "Maximum results size (defaults to 100)") @QueryParam("max") Integer maxResults, + @Parameter(description = "Maximum results size") @QueryParam("max") @DefaultValue(Constants.DEFAULT_MAX_RESULTS_STR) Integer maxResults, @Parameter(description = "The direction to sort events by (asc or desc)") @QueryParam("direction") String direction) { auth.realm().requireViewEvents(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java index cc8788d656a5..ce084e77b71d 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java @@ -563,7 +563,7 @@ public ManagementPermissionReference setManagementPermissionsEnabled(final @Path * * @param roleName the role name. * @param firstResult first result to return. Ignored if negative or {@code null}. - * @param maxResults maximum number of results to return. Ignored if negative or {@code null}. + * @param maxResults maximum number of results to return. Unbounded if negative. * @param briefRepresentation Boolean which defines whether brief representations are returned (default: false) * @return a non-empty {@code Stream} of users. */ @@ -581,7 +581,7 @@ public ManagementPermissionReference setManagementPermissionsEnabled(final @Path public Stream getUsersInRole(final @Parameter(description = "the role name.") @PathParam("role-name") String roleName, @Parameter(description = "Boolean which defines whether brief representations are returned (default: false)") @QueryParam("briefRepresentation") Boolean briefRepresentation, @Parameter(description = "first result to return. Ignored if negative or {@code null}.") @QueryParam("first") Integer firstResult, - @Parameter(description = "maximum number of results to return. Ignored if negative or {@code null}.") @QueryParam("max") Integer maxResults) { + @Parameter(description = "Maximum number of results to return. Unbounded if negative.") @QueryParam("max") @DefaultValue(Constants.DEFAULT_MAX_RESULTS_STR) Integer maxResults) { auth.roles().requireView(roleContainer); auth.users().requireQuery(); @@ -607,7 +607,7 @@ public Stream getUsersInRole(final @Parameter(description = * * @param roleName the role name. * @param firstResult first result to return. Ignored if negative or {@code null}. - * @param maxResults maximum number of results to return. Ignored if negative or {@code null}. + * @param maxResults maximum number of results to return. Unbounded if negative. * @param briefRepresentation if false, return a full representation of the {@code GroupRepresentation} objects. * @return a non-empty {@code Stream} of groups. */ @@ -623,9 +623,9 @@ public Stream getUsersInRole(final @Parameter(description = @APIResponse(responseCode = "404", description = "Not Found") }) public Stream getGroupsInRole(final @Parameter(description = "the role name.") @PathParam("role-name") String roleName, - @Parameter(description = "first result to return. Ignored if negative or {@code null}.") @QueryParam("first") Integer firstResult, - @Parameter(description = "maximum number of results to return. Ignored if negative or {@code null}.") @QueryParam("max") Integer maxResults, - @Parameter(description = "if false, return a full representation of the {@code GroupRepresentation} objects.") @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) { + @Parameter(description = "First result to return. Ignored if negative or {@code null}.") @QueryParam("first") Integer firstResult, + @Parameter(description = "Maximum number of results to return. Unbounded if negative.") @QueryParam(Constants.DEFAULT_MAX_RESULTS_STR) Integer maxResults, + @Parameter(description = "If false, return a full representation of the {@code GroupRepresentation} objects.") @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) { auth.roles().requireView(roleContainer); firstResult = firstResult != null ? firstResult : 0;