diff --git a/pom.xml b/pom.xml index b5d03b7203b..b758dfa7d89 100644 --- a/pom.xml +++ b/pom.xml @@ -188,6 +188,7 @@ https://issues.redhat.com/browse/KEYCLOAK-17585?focusedCommentId=16002705&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-16002705 --> 3.0.1 + 3.1 1.17.5 @@ -202,6 +203,8 @@ 0.40.3 1.1 3.4.1 + 3.1.2 + 6.3.0 512m @@ -1825,6 +1828,12 @@ + + org.eclipse.microprofile.openapi + microprofile-openapi-api + ${microprofile-openapi-api.version} + + org.keycloak @@ -2007,6 +2016,16 @@ wildfly-server-provisioning-maven-plugin ${wildfly.build-tools.version} + + io.smallrye + smallrye-open-api-maven-plugin + ${smallrye.openapi.generator.plugin.version} + + + org.openapitools + openapi-generator-maven-plugin + ${openapi.generator.plugin.version} + org.wildfly.galleon-plugins wildfly-galleon-maven-plugin diff --git a/rest/admin-ui-ext/pom.xml b/rest/admin-ui-ext/pom.xml index 8cda111ce81..b4f0a7e714b 100644 --- a/rest/admin-ui-ext/pom.xml +++ b/rest/admin-ui-ext/pom.xml @@ -45,7 +45,6 @@ org.eclipse.microprofile.openapi microprofile-openapi-api - 3.1 @@ -54,7 +53,6 @@ smallrye-open-api-maven-plugin io.smallrye - 3.1.2 org.keycloak.admin.ui.rest diff --git a/services/pom.xml b/services/pom.xml index 8ee835a0130..49c152048d6 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -30,10 +30,6 @@ Keycloak REST Services - - 1.1.2 - - org.keycloak @@ -201,6 +197,10 @@ com.github.ua-parser uap-java + + org.eclipse.microprofile.openapi + microprofile-openapi-api + @@ -244,83 +244,57 @@ - org.apache.maven.plugins - maven-javadoc-plugin - - 2.10.3 + io.smallrye + smallrye-open-api-maven-plugin + + admin + ${project.build.directory}/apidocs-rest/swagger/apidocs + Keycloak Admin REST API + This is a REST API reference for the Keycloak Admin REST API. + - generate-service-docs - generate-resources - - com.carma.swagger.doclet.ServiceDoclet - - com.carma - swagger-doclet - ${version.swagger.doclet} - - - org.keycloak.services.resources.admin:org.keycloak.protocol.oidc - false - - - ../javadocs - ${project.basedir}/../target/site/apidocs - - - - ${project.basedir}/target/apidocs-rest/swagger - false - -skipUiFiles -apiVersion 1 -includeResourcePrefixes org.keycloak.services.resources.admin,org.keycloak.protocol.oidc -docBasePath /apidocs -apiBasePath http://localhost:8080/auth -apiInfoFile ${project.basedir}/target/docs/swagger/apiinfo.json - - javadoc + generate-schema - - io.github.swagger2markup - swagger2markup-maven-plugin - 1.1.0 - - - - - ca.szc.thirdparty.nl.jworks.markdown_to_asciidoc - markdown_to_asciidoc - - 1.0 - - - io.github.swagger2markup - swagger2markup - - 1.1.0 - - - nl.jworks.markdown_to_asciidoc - markdown_to_asciidoc - - - - - + org.openapitools + openapi-generator-maven-plugin - gen-asciidoc - process-resources + generate-ascii-docs + prepare-package - convertSwagger2markup + generate - ${project.build.directory}/apidocs-rest/swagger/apidocs/service.json - ${project.build.directory}/apidocs-rest/asciidoc/ - - ASCIIDOC - TAGS - + + true + true + false + false + false + + ${project.build.directory}/apidocs-rest/swagger/apidocs/openapi.yaml + ${project.basedir}/target/docs/asciidoc/ + asciidoc + true + true + true + ${project.groupId} + ${project.artifactId} + src/docs/openapi-generator-templates/keycloak-admin-api + + true + false + true + false + true + + true diff --git a/services/src/docs/asciidoc/index.adoc b/services/src/docs/asciidoc/index.adoc deleted file mode 100644 index 17ffc523b35..00000000000 --- a/services/src/docs/asciidoc/index.adoc +++ /dev/null @@ -1,3 +0,0 @@ -include::overview.adoc[] -include::{generated}/paths.adoc[] -include::{generated}/definitions.adoc[] \ No newline at end of file diff --git a/services/src/docs/asciidoc/overview.adoc b/services/src/docs/asciidoc/overview.adoc deleted file mode 100644 index a340c9538ca..00000000000 --- a/services/src/docs/asciidoc/overview.adoc +++ /dev/null @@ -1,15 +0,0 @@ -= Keycloak Admin REST API - -== Overview -This is a REST API reference for the Keycloak Admin REST API. - -=== Version information -Version: 1 - -=== URI scheme - -``` -{base url}/admin/realms -``` - -For example `http://localhost:8080/admin/realms` diff --git a/services/src/docs/openapi-generator-templates/keycloak-admin-api/index.mustache b/services/src/docs/openapi-generator-templates/keycloak-admin-api/index.mustache new file mode 100644 index 00000000000..7356c82bd0b --- /dev/null +++ b/services/src/docs/openapi-generator-templates/keycloak-admin-api/index.mustache @@ -0,0 +1,132 @@ += {{{appName}}} +{{#headerAttributes}} +:toc: left +:toclevels: 2 +:keywords: openapi, rest, {{appName}} +:specDir: {{specDir}} +:snippetDir: {{snippetDir}} +:generator-template: v1 2019-12-20 +:info-url: {{infoUrl}} +:app-name: {{appName}} +{{/headerAttributes}} + +{{#useIntroduction}} +== Overview +{{/useIntroduction}} +{{^useIntroduction}} +[abstract] +.Abstract +{{/useIntroduction}} +{{{appDescription}}} + +=== Version information +Version: {{version}} + +=== URI scheme + +``` +{base url}/admin/realms +``` + +For example `http://localhost:8080/admin/realms` + +{{#specinclude}}intro.adoc{{/specinclude}} + +{{#hasAuthMethods}} +== Access + +{{#authMethods}} +{{#isBasic}} +{{#isBasicBasic}}* *HTTP Basic* Authentication _{{{name}}}_{{/isBasicBasic}} +{{#isBasicBearer}}* *Bearer* Authentication {{/isBasicBearer}} +{{/isBasic}} +{{#isOAuth}}* *OAuth* AuthorizationUrl: _{{authorizationUrl}}_, TokenUrl: _{{tokenUrl}}_ {{/isOAuth}} +{{#isApiKey}}* *APIKey* KeyParamName: _{{keyParamName}}_, KeyInQuery: _{{isKeyInQuery}}_, KeyInHeader: _{{isKeyInHeader}}_{{/isApiKey}} +{{/authMethods}} + +{{/hasAuthMethods}} + +== Resources + +{{#apiInfo}} +{{#apis}} +{{#operations}} + +[.{{baseName}}] +// this is a better name +=== {{operation.0.tags.0.name}} + + +{{#operation}} + +[.{{nickname}}] +{{#useMethodAndPath}} +==== {{httpMethod}} {{path}} + +// Operation Id:: {{nickname}} + +{{/useMethodAndPath}} +{{^useMethodAndPath}} +==== {{nickname}} + +`{{httpMethod}} {{path}}` +{{/useMethodAndPath}} + +{{{summary}}} + +// conditionally add description if there are notes +{{#notes}} + +===== Description + +{{{.}}} +{{/notes}} +{{#specinclude}}{{path}}/{{httpMethod}}/spec.adoc{{/specinclude}} + + +{{> params}} + +{{#hasProduces}} +===== Content Type + +{{#produces}} +* `+{{mediaType}}+` +{{/produces}} +{{/hasProduces}} + +===== Responses + +[cols="2,3,1"] +|=== +| Code | Message | Datatype + +{{#responses}} + +| {{^isDefault}}{{code}}{{/isDefault}}{{#isDefault}}*default*{{/isDefault}} +| {{^isDefault}}{{message}}{{/isDefault}}{{#isDefault}}success{{/isDefault}} +| {{#containerType}}{{dataType}}[<<{{baseType}}>>]{{/containerType}} {{^containerType}}<<{{dataType}}>>{{/containerType}} + +{{/responses}} +|=== + +{{^skipExamples}} +===== Samples + +{{#snippetinclude}}{{path}}/{{httpMethod}}/http-request.adoc{{/snippetinclude}} +{{#snippetinclude}}{{path}}/{{httpMethod}}/http-response.adoc{{/snippetinclude}} + +{{#snippetlink}}* wiremock data, {{path}}/{{httpMethod}}/{{httpMethod}}.json{{/snippetlink}} +{{/skipExamples}} + +ifdef::internal-generation[] +===== Implementation +{{#specinclude}}{{path}}/{{httpMethod}}/implementation.adoc{{/specinclude}} + +endif::internal-generation[] + +{{/operation}} +{{/operations}} +{{/apis}} +{{/apiInfo}} + +{{> model}} diff --git a/services/src/docs/openapi-generator-templates/keycloak-admin-api/model.mustache b/services/src/docs/openapi-generator-templates/keycloak-admin-api/model.mustache new file mode 100644 index 00000000000..8a83d1a6ca4 --- /dev/null +++ b/services/src/docs/openapi-generator-templates/keycloak-admin-api/model.mustache @@ -0,0 +1,27 @@ +[#models] +== Definitions + +{{#models}} + {{#model}} + +[#{{classname}}] +=== {{classname}} {{title}} + +{{unescapedDescription}} + +[.fields-{{classname}}] +[cols="2,2,1"] +|=== +| Name| Type| Format + +{{#vars}} +| *{{baseName}}* + +{{#required}}_required_{{/required}}{{^required}}_optional_{{/required}} +| {{dataType}} {{#isContainer}} of <<{{complexType}}>>{{/isContainer}} +| {{{dataFormat}}} {{#isEnum}}enum ({{#_enum}}{{this}}, {{/_enum}}){{/isEnum}} + +{{/vars}} +|=== + + {{/model}} +{{/models}} diff --git a/services/src/docs/openapi-generator-templates/keycloak-admin-api/param.mustache b/services/src/docs/openapi-generator-templates/keycloak-admin-api/param.mustache new file mode 100644 index 00000000000..02c599f4875 --- /dev/null +++ b/services/src/docs/openapi-generator-templates/keycloak-admin-api/param.mustache @@ -0,0 +1,5 @@ +| *{{baseName}}* + +{{^required}}_optional_{{/required}}{{#required}}_required_{{/required}} +| {{description}} {{#baseType}}<<{{.}}>>{{/baseType}} +| {{defaultValue}} +| {{{pattern}}} diff --git a/services/src/docs/openapi-generator-templates/keycloak-admin-api/params.mustache b/services/src/docs/openapi-generator-templates/keycloak-admin-api/params.mustache new file mode 100644 index 00000000000..c7ccdd57110 --- /dev/null +++ b/services/src/docs/openapi-generator-templates/keycloak-admin-api/params.mustache @@ -0,0 +1,91 @@ +===== Parameters + +{{#hasPathParams}} +{{^useTableTitles}} +====== Path Parameters +{{/useTableTitles}} + +[cols="2,3,1,1"] +{{#useTableTitles}} +.Path Parameters +{{/useTableTitles}} +|=== +|Name| Description| Default| Pattern + +{{#pathParams}} +{{>param}} +{{/pathParams}} +|=== +{{/hasPathParams}} + +{{#hasBodyParam}} +{{^useTableTitles}} +====== Body Parameter +{{/useTableTitles}} + +[cols="2,3,1,1"] +{{#useTableTitles}} +.Body Parameter +{{/useTableTitles}} +|=== +|Name| Description| Default| Pattern + +{{#bodyParams}} +{{>param}} +{{/bodyParams}} +|=== +{{/hasBodyParam}} + +{{#hasFormParams}} +{{^useTableTitles}} +====== Form Parameters +{{/useTableTitles}} + +[cols="2,3,1,1"] +{{#useTableTitles}} +.Form Parameters +{{/useTableTitles}} +|=== +|Name| Description| Default| Pattern + +{{#formParams}} +{{>param}} +{{/formParams}} +|=== +{{/hasFormParams}} + +{{#hasHeaderParams}} +{{^useTableTitles}} +====== Header Parameters +{{/useTableTitles}} + +[cols="2,3,1,1"] +{{#useTableTitles}} +.Header Parameters +{{/useTableTitles}} +|=== +|Name| Description| Default| Pattern + +{{#headerParams}} +{{>param}} +{{/headerParams}} +|=== +{{/hasHeaderParams}} + +{{#hasQueryParams}} +{{^useTableTitles}} +====== Query Parameters +{{/useTableTitles}} + +[cols="2,3,1,1"] +{{#useTableTitles}} +.Query Parameters +{{/useTableTitles}} +|=== +|Name| Description| Default| Pattern + +{{#queryParams}} +{{>param}} +{{/queryParams}} +|=== +{{/hasQueryParams}} diff --git a/services/src/docs/openapi-generator-templates/keycloak-admin-api/stubs/empty.adoc b/services/src/docs/openapi-generator-templates/keycloak-admin-api/stubs/empty.adoc new file mode 100644 index 00000000000..c6d8ea163e7 --- /dev/null +++ b/services/src/docs/openapi-generator-templates/keycloak-admin-api/stubs/empty.adoc @@ -0,0 +1 @@ +// openapi generator built documentation, see https://github.com/OpenAPITools/openapi-generator diff --git a/services/src/docs/swagger/apiinfo.json b/services/src/docs/swagger/apiinfo.json index 575955f57dd..117234a5918 100644 --- a/services/src/docs/swagger/apiinfo.json +++ b/services/src/docs/swagger/apiinfo.json @@ -1,4 +1,8 @@ { - "title": "Keycloak Admin REST API", - "description": "This is a REST API reference for the Keycloak Admin" -} \ No newline at end of file + "openapi": "3.0.1", + "info": { + "title": "Keycloak Admin REST API", + "description": "This is a REST API reference for the Keycloak Admin REST API", + "version": 1 + } +} diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakOpenAPI.java b/services/src/main/java/org/keycloak/services/resources/KeycloakOpenAPI.java new file mode 100644 index 00000000000..16ad437e084 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakOpenAPI.java @@ -0,0 +1,62 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.services.resources; + +/** + * Class of constants relating to the OpenAPI annotations in Keycloak and the Keycloak Admin REST API + */ +public class KeycloakOpenAPI { + + private KeycloakOpenAPI() { } + public static class Profiles { + + public static final String ADMIN = "x-smallrye-profile-admin"; + + private Profiles() { } + } + + public static class Admin { + + private Admin() { } + + public static class Tags { + public static final String ATTACK_DETECTION = "Attack Detection"; + public static final String AUTHENTICATION_MANAGEMENT = "Authentication Management"; + public static final String CLIENTS = "Clients"; + public static final String CLIENT_ATTRIBUTE_CERTIFICATE = "Client Attribute Certificate"; + public static final String CLIENT_INITIAL_ACCESS = "Client Initial Access"; + public static final String CLIENT_REGISTRATION_POLICY = "Client Registration Policy"; + public static final String CLIENT_ROLE_MAPPINGS = "Client Role Mappings"; + public static final String CLIENT_SCOPES = "Client Scopes"; + public static final String COMPONENT = "Component"; + public static final String GROUPS = "Groups"; + public static final String IDENTITY_PROVIDERS = "Identity Providers"; + public static final String KEY = "Key"; + public static final String PROTOCOL_MAPPERS = "Protocol Mappers"; + public static final String REALMS_ADMIN = "Realms Admin"; + public static final String ROLES = "Roles"; + public static final String ROLES_BY_ID = "Roles (by ID)"; + public static final String ROLE_MAPPER = "Role Mapper"; + public static final String ROOT = "Root"; + public static final String SCOPE_MAPPINGS = "Scope Mappings"; + public static final String USERS = "Users"; + private Tags() { } + } + + } +} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java index 8c12500f358..7c98ca0007c 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java @@ -16,6 +16,7 @@ */ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; import org.jboss.logging.Logger; import org.keycloak.http.HttpRequest; import org.keycloak.http.HttpResponse; @@ -86,6 +87,7 @@ public class AdminRoot { * @return */ @GET + @Operation(hidden = true) public Response masterRealmAdminConsoleRedirect() { if (!isAdminConsoleEnabled()) { @@ -106,6 +108,7 @@ public class AdminRoot { */ @Path("index.{html:html}") // expression is actually "index.html" but this is a hack to get around jax-doclet bug @GET + @Operation(hidden = true) public Response masterRealmAdminConsoleRedirectHtml() { if (!isAdminConsoleEnabled()) { @@ -141,6 +144,7 @@ public class AdminRoot { * @return */ @Path("{realm}/console") + @Operation(hidden = true) public AdminConsole getAdminConsole(final @PathParam("realm") String name) { if (!isAdminConsoleEnabled()) { @@ -200,7 +204,7 @@ public class AdminRoot { * @return */ @Path("realms") - public Object getRealmsAdmin() { + public RealmsAdminResource getRealmsAdmin() { HttpRequest request = getHttpRequest(); if (!isAdminApiEnabled()) { @@ -208,7 +212,7 @@ public class AdminRoot { } if (request.getHttpMethod().equals(HttpMethod.OPTIONS)) { - return new AdminCorsPreflightService(request); + return new RealmsAdminResourcePreflight(session, null, tokenManager, request); } AdminAuth auth = authenticateRealmAdminRequest(session.getContext().getRequestHeaders()); @@ -226,6 +230,7 @@ public class AdminRoot { @Path("{any:.*}") @OPTIONS + @Operation(hidden = true) public Object preFlight() { HttpRequest request = getHttpRequest(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AttackDetectionResource.java b/services/src/main/java/org/keycloak/services/resources/admin/AttackDetectionResource.java index 2dfad692614..691f005d40c 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AttackDetectionResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AttackDetectionResource.java @@ -16,6 +16,9 @@ */ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.common.ClientConnection; @@ -27,6 +30,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserLoginFailureModel; import org.keycloak.models.UserModel; import org.keycloak.services.managers.BruteForceProtector; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import jakarta.ws.rs.DELETE; @@ -46,6 +50,7 @@ import java.util.Map; * @author Bill Burke * @version $Revision: 1 $ */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class AttackDetectionResource { protected static final Logger logger = Logger.getLogger(AttackDetectionResource.class); protected final AdminPermissionEvaluator auth; @@ -77,6 +82,8 @@ public class AttackDetectionResource { @Path("brute-force/users/{userId}") @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.ATTACK_DETECTION) + @Operation( summary = "Get status of a username in brute force detection") public Map bruteForceUserStatus(@PathParam("userId") String userId) { UserModel user = session.users().getUserById(realm, userId); if (user == null) { @@ -121,6 +128,8 @@ public class AttackDetectionResource { */ @Path("brute-force/users/{userId}") @DELETE + @Tag(name = KeycloakOpenAPI.Admin.Tags.ATTACK_DETECTION) + @Operation( summary="Clear any user login failures for the user This can release temporary disabled user") public void clearBruteForceForUser(@PathParam("userId") String userId) { UserModel user = session.users().getUserById(realm, userId); if (user == null) { @@ -143,6 +152,8 @@ public class AttackDetectionResource { */ @Path("brute-force/users") @DELETE + @Tag(name = KeycloakOpenAPI.Admin.Tags.ATTACK_DETECTION) + @Operation( summary = "Clear any user login failures for all users This can release temporary disabled users") public void clearAllBruteForce() { auth.users().requireManage(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java index e9b2613effc..86df1b3fa5e 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java @@ -16,6 +16,10 @@ */ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import jakarta.ws.rs.BadRequestException; @@ -53,6 +57,7 @@ import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; import org.keycloak.representations.idm.ConfigPropertyRepresentation; import org.keycloak.representations.idm.RequiredActionProviderRepresentation; import org.keycloak.services.ErrorResponse; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.utils.CredentialHelper; @@ -86,6 +91,7 @@ import org.keycloak.utils.ReservedCharValidator; * @resource Authentication Management * @author Bill Burke */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class AuthenticationManagementResource { private final RealmModel realm; @@ -111,6 +117,8 @@ public class AuthenticationManagementResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation(summary = "Get form providers Returns a stream of form providers.") public Stream> getFormProviders() { auth.realm().requireViewRealm(); @@ -126,6 +134,8 @@ public class AuthenticationManagementResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Get authenticator providers Returns a stream of authenticator providers.") public Stream> getAuthenticatorProviders() { auth.realm().requireViewRealm(); @@ -141,6 +151,8 @@ public class AuthenticationManagementResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Get client authenticator providers Returns a stream of client authenticator providers.") public Stream> getClientAuthenticatorProviders() { auth.realm().requireViewClientAuthenticatorProviders(); @@ -167,6 +179,9 @@ public class AuthenticationManagementResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Get form action providers Returns a stream of form action providers." + ) public Stream> getFormActionProviders() { auth.realm().requireViewRealm(); @@ -183,6 +198,8 @@ public class AuthenticationManagementResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Get authentication flows Returns a stream of authentication flows.") public Stream getFlows() { auth.realm().requireViewAuthenticationFlows(); @@ -201,7 +218,9 @@ public class AuthenticationManagementResource { @POST @NoCache @Consumes(MediaType.APPLICATION_JSON) - public Response createFlow(AuthenticationFlowRepresentation flow) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Create a new authentication flow") + public Response createFlow(@Parameter( description = "Authentication flow representation") AuthenticationFlowRepresentation flow) { auth.realm().requireManageRealm(); if (flow.getAlias() == null || flow.getAlias().isEmpty()) { @@ -236,7 +255,9 @@ public class AuthenticationManagementResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public AuthenticationFlowRepresentation getFlow(@PathParam("id") String id) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Get authentication flow for id") + public AuthenticationFlowRepresentation getFlow(@Parameter(description = "Flow id") @PathParam("id") String id) { auth.realm().requireViewRealm(); AuthenticationFlowModel flow = realm.getAuthenticationFlowById(id); @@ -257,6 +278,8 @@ public class AuthenticationManagementResource { @NoCache @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Update an authentication flow") public Response updateFlow(@PathParam("id") String id, AuthenticationFlowRepresentation flow) { auth.realm().requireManageRealm(); @@ -307,7 +330,9 @@ public class AuthenticationManagementResource { @Path("/flows/{id}") @DELETE @NoCache - public void deleteFlow(@PathParam("id") String id) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Delete an authentication flow") + public void deleteFlow(@Parameter(description = "Flow id") @PathParam("id") String id) { auth.realm().requireManageRealm(); KeycloakModelUtils.deepDeleteAuthenticationFlow(realm, realm.getAuthenticationFlowById(id), @@ -335,7 +360,9 @@ public class AuthenticationManagementResource { @POST @NoCache @Consumes(MediaType.APPLICATION_JSON) - public Response copy(@PathParam("flowAlias") String flowAlias, Map data) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Copy existing authentication flow under a new name The new name is given as 'newName' attribute of the passed JSON object") + public Response copy(@Parameter(description="name of the existing authentication flow") @PathParam("flowAlias") String flowAlias, Map data) { auth.realm().requireManageRealm(); String newName = data.get("newName"); @@ -419,7 +446,9 @@ public class AuthenticationManagementResource { @POST @NoCache @Consumes(MediaType.APPLICATION_JSON) - public Response addExecutionFlow(@PathParam("flowAlias") String flowAlias, Map data) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Add new flow with new execution to existing flow") + public Response addExecutionFlow(@Parameter(description = "Alias of parent authentication flow") @PathParam("flowAlias") String flowAlias, @Parameter(description = "New authentication flow / execution JSON data containing 'alias', 'type', 'provider', and 'description' attributes") Map data) { auth.realm().requireManageRealm(); AuthenticationFlowModel parentFlow = realm.getFlowByAlias(flowAlias); @@ -480,7 +509,9 @@ public class AuthenticationManagementResource { @POST @NoCache @Consumes(MediaType.APPLICATION_JSON) - public Response addExecutionToFlow(@PathParam("flowAlias") String flowAlias, Map data) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary="Add new authentication execution to a flow") + public Response addExecutionToFlow(@Parameter(description = "Alias of parent flow") @PathParam("flowAlias") String flowAlias, @Parameter(description = "New execution JSON data containing 'provider' attribute") Map data) { auth.realm().requireManageRealm(); AuthenticationFlowModel parentFlow = realm.getFlowByAlias(flowAlias); @@ -549,7 +580,9 @@ public class AuthenticationManagementResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public Response getExecutions(@PathParam("flowAlias") String flowAlias) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Get authentication executions for a flow") + public Response getExecutions(@Parameter(description = "Flow alias") @PathParam("flowAlias") String flowAlias) { auth.realm().requireViewRealm(); AuthenticationFlowModel flow = realm.getFlowByAlias(flowAlias); @@ -650,7 +683,9 @@ public class AuthenticationManagementResource { @NoCache @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - public Response updateExecutions(@PathParam("flowAlias") String flowAlias, AuthenticationExecutionInfoRepresentation rep) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Update authentication executions of a Flow") + public Response updateExecutions(@Parameter(description = "Flow alias") @PathParam("flowAlias") String flowAlias, @Parameter(description = "AuthenticationExecutionInfoRepresentation") AuthenticationExecutionInfoRepresentation rep) { auth.realm().requireManageRealm(); AuthenticationFlowModel flow = realm.getFlowByAlias(flowAlias); @@ -715,6 +750,8 @@ public class AuthenticationManagementResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Get Single Execution") public Response getExecution(final @PathParam("executionId") String executionId) { //http://localhost:8080/auth/admin/realms/master/authentication/executions/cf26211b-9e68-4788-b754-1afd02e59d7f auth.realm().requireManageRealm(); @@ -737,7 +774,9 @@ public class AuthenticationManagementResource { @POST @NoCache @Consumes(MediaType.APPLICATION_JSON) - public Response addExecution(AuthenticationExecutionRepresentation execution) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Add new authentication execution") + public Response addExecution(@Parameter(description = "JSON model describing authentication execution") AuthenticationExecutionRepresentation execution) { auth.realm().requireManageRealm(); AuthenticationExecutionModel model = RepresentationToModel.toModel(realm, execution); @@ -773,7 +812,9 @@ public class AuthenticationManagementResource { @Path("/executions/{executionId}/raise-priority") @POST @NoCache - public void raisePriority(@PathParam("executionId") String execution) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Raise execution's priority") + public void raisePriority(@Parameter(description = "Execution id") @PathParam("executionId") String execution) { auth.realm().requireManageRealm(); AuthenticationExecutionModel model = realm.getAuthenticationExecutionById(execution); @@ -813,7 +854,9 @@ public class AuthenticationManagementResource { @Path("/executions/{executionId}/lower-priority") @POST @NoCache - public void lowerPriority(@PathParam("executionId") String execution) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Lower execution's priority") + public void lowerPriority(@Parameter( description = "Execution id") @PathParam("executionId") String execution) { auth.realm().requireManageRealm(); AuthenticationExecutionModel model = realm.getAuthenticationExecutionById(execution); @@ -853,7 +896,9 @@ public class AuthenticationManagementResource { @Path("/executions/{executionId}") @DELETE @NoCache - public void removeExecution(@PathParam("executionId") String execution) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Delete execution") + public void removeExecution(@Parameter(description = "Execution id") @PathParam("executionId") String execution) { auth.realm().requireManageRealm(); AuthenticationExecutionModel model = realm.getAuthenticationExecutionById(execution); @@ -889,7 +934,9 @@ public class AuthenticationManagementResource { @POST @NoCache @Consumes(MediaType.APPLICATION_JSON) - public Response newExecutionConfig(@PathParam("executionId") String execution, AuthenticatorConfigRepresentation json) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Update execution with new configuration") + public Response newExecutionConfig(@Parameter(description = "Execution id") @PathParam("executionId") String execution, @Parameter(description = "JSON with new configuration") AuthenticatorConfigRepresentation json) { auth.realm().requireManageRealm(); ReservedCharValidator.validate(json.getAlias()); @@ -924,7 +971,9 @@ public class AuthenticationManagementResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache - public AuthenticatorConfigRepresentation getAuthenticatorConfig(@PathParam("executionId") String execution,@PathParam("id") String id) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Get execution's configuration", deprecated = true) + public AuthenticatorConfigRepresentation getAuthenticatorConfig(@Parameter(description = "Execution id") @PathParam("executionId") String execution, @Parameter(description = "Configuration id") @PathParam("id") String id) { auth.realm().requireViewRealm(); AuthenticatorConfigModel config = realm.getAuthenticatorConfigById(id); @@ -944,6 +993,8 @@ public class AuthenticationManagementResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Get unregistered required actions Returns a stream of unregistered required actions.") public Stream> getUnregisteredRequiredActions() { auth.realm().requireViewRealm(); @@ -970,7 +1021,9 @@ public class AuthenticationManagementResource { @POST @Consumes(MediaType.APPLICATION_JSON) @NoCache - public void registerRequiredAction(Map data) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Register a new required actions") + public void registerRequiredAction(@Parameter(description = "JSON containing 'providerId', and 'name' attributes.") Map data) { auth.realm().requireManageRealm(); String providerId = data.get("providerId"); @@ -1003,6 +1056,8 @@ public class AuthenticationManagementResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Get required actions Returns a stream of required actions.") public Stream getRequiredActions() { auth.realm().requireViewRequiredActions(); @@ -1029,7 +1084,9 @@ public class AuthenticationManagementResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache - public RequiredActionProviderRepresentation getRequiredAction(@PathParam("alias") String alias) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Get required action for alias") + public RequiredActionProviderRepresentation getRequiredAction(@Parameter(description = "Alias of required action") @PathParam("alias") String alias) { auth.realm().requireViewRealm(); RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(alias); @@ -1049,7 +1106,9 @@ public class AuthenticationManagementResource { @Path("required-actions/{alias}") @PUT @Consumes(MediaType.APPLICATION_JSON) - public void updateRequiredAction(@PathParam("alias") String alias, RequiredActionProviderRepresentation rep) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Update required action") + public void updateRequiredAction(@Parameter(description = "Alias of required action") @PathParam("alias") String alias, @Parameter(description = "JSON describing new state of required action") RequiredActionProviderRepresentation rep) { auth.realm().requireManageRealm(); RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(alias); @@ -1076,7 +1135,9 @@ public class AuthenticationManagementResource { */ @Path("required-actions/{alias}") @DELETE - public void removeRequiredAction(@PathParam("alias") String alias) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Delete required action") + public void removeRequiredAction(@Parameter(description = "Alias of required action") @PathParam("alias") String alias) { auth.realm().requireManageRealm(); RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(alias); @@ -1096,7 +1157,9 @@ public class AuthenticationManagementResource { @Path("required-actions/{alias}/raise-priority") @POST @NoCache - public void raiseRequiredActionPriority(@PathParam("alias") String alias) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Raise required action's priority") + public void raiseRequiredActionPriority(@Parameter(description = "Alias of required action") @PathParam("alias") String alias) { auth.realm().requireManageRealm(); RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(alias); @@ -1129,7 +1192,9 @@ public class AuthenticationManagementResource { @Path("/required-actions/{alias}/lower-priority") @POST @NoCache - public void lowerRequiredActionPriority(@PathParam("alias") String alias) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Lower required action's priority") + public void lowerRequiredActionPriority(@Parameter(description = "Alias of required action") @PathParam("alias") String alias) { auth.realm().requireManageRealm(); RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(alias); @@ -1162,6 +1227,8 @@ public class AuthenticationManagementResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Get authenticator provider's configuration description") public AuthenticatorConfigInfoRepresentation getAuthenticatorConfigDescription(@PathParam("providerId") String providerId) { auth.realm().requireViewRealm(); @@ -1200,6 +1267,8 @@ public class AuthenticationManagementResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Get configuration descriptions for all clients") public Map> getPerClientConfigDescription() { auth.realm().requireViewClientAuthenticatorProviders(); @@ -1223,7 +1292,9 @@ public class AuthenticationManagementResource { @POST @NoCache @Consumes(MediaType.APPLICATION_JSON) - public Response createAuthenticatorConfig(AuthenticatorConfigRepresentation rep) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Create new authenticator configuration", deprecated = true) + public Response createAuthenticatorConfig(@Parameter(description = "JSON describing new authenticator configuration") AuthenticatorConfigRepresentation rep) { auth.realm().requireManageRealm(); ReservedCharValidator.validate(rep.getAlias()); @@ -1241,7 +1312,9 @@ public class AuthenticationManagementResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache - public AuthenticatorConfigRepresentation getAuthenticatorConfig(@PathParam("id") String id) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Get authenticator configuration") + public AuthenticatorConfigRepresentation getAuthenticatorConfig(@Parameter(description = "Configuration id") @PathParam("id") String id) { auth.realm().requireViewRealm(); AuthenticatorConfigModel config = realm.getAuthenticatorConfigById(id); @@ -1259,7 +1332,9 @@ public class AuthenticationManagementResource { @Path("config/{id}") @DELETE @NoCache - public void removeAuthenticatorConfig(@PathParam("id") String id) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Delete authenticator configuration") + public void removeAuthenticatorConfig(@Parameter(description = "Configuration id") @PathParam("id") String id) { auth.realm().requireManageRealm(); AuthenticatorConfigModel config = realm.getAuthenticatorConfigById(id); @@ -1288,7 +1363,9 @@ public class AuthenticationManagementResource { @PUT @Consumes(MediaType.APPLICATION_JSON) @NoCache - public void updateAuthenticatorConfig(@PathParam("id") String id, AuthenticatorConfigRepresentation rep) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.AUTHENTICATION_MANAGEMENT) + @Operation( summary = "Update authenticator configuration") + public void updateAuthenticatorConfig(@Parameter(description = "Configuration id") @PathParam("id") String id, @Parameter(description = "JSON describing new state of authenticator configuration") AuthenticatorConfigRepresentation rep) { auth.realm().requireManageRealm(); ReservedCharValidator.validate(rep.getAlias()); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java index 883dd126a61..b660ee4cfaa 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientAttributeCertificateResource.java @@ -17,6 +17,10 @@ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.resteasy.annotations.cache.NoCache; import jakarta.ws.rs.NotAcceptableException; import jakarta.ws.rs.NotFoundException; @@ -39,6 +43,7 @@ import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.KeyStoreConfig; import org.keycloak.representations.idm.CertificateRepresentation; import org.keycloak.services.ErrorResponseException; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.util.CertificateInfoHelper; import org.keycloak.util.JWKSUtils; @@ -69,6 +74,7 @@ import java.util.stream.Collectors; * @author Bill Burke * @version $Revision: 1 $ */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class ClientAttributeCertificateResource { public static final String CERTIFICATE_PEM = "Certificate PEM"; @@ -99,6 +105,8 @@ public class ClientAttributeCertificateResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_ATTRIBUTE_CERTIFICATE) + @Operation( summary = "Get key info") public CertificateRepresentation getKeyInfo() { auth.clients().requireView(client); @@ -115,6 +123,8 @@ public class ClientAttributeCertificateResource { @NoCache @Path("generate") @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_ATTRIBUTE_CERTIFICATE) + @Operation( summary = "Generate a new certificate with new key pair") public CertificateRepresentation generate() { auth.clients().requireConfigure(client); @@ -138,6 +148,8 @@ public class ClientAttributeCertificateResource { @Path("upload") @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_ATTRIBUTE_CERTIFICATE) + @Operation( summary = "Upload certificate and eventually private key") public CertificateRepresentation uploadJks() throws IOException { auth.clients().requireConfigure(client); @@ -163,6 +175,8 @@ public class ClientAttributeCertificateResource { @Path("upload-certificate") @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_ATTRIBUTE_CERTIFICATE) + @Operation( summary = "Upload only certificate, not private key") public CertificateRepresentation uploadJksCertificate() throws IOException { auth.clients().requireConfigure(client); @@ -265,7 +279,9 @@ public class ClientAttributeCertificateResource { @Path("/download") @Produces(MediaType.APPLICATION_OCTET_STREAM) @Consumes(MediaType.APPLICATION_JSON) - public byte[] getKeystore(final KeyStoreConfig config) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_ATTRIBUTE_CERTIFICATE) + @Operation( summary = "Get a keystore file for the client, containing private key and public certificate") + public byte[] getKeystore(@Parameter(description = "Keystore configuration as JSON") final KeyStoreConfig config) { auth.clients().requireView(client); checkKeystoreFormat(config); @@ -302,7 +318,13 @@ public class ClientAttributeCertificateResource { @Path("/generate-and-download") @Produces(MediaType.APPLICATION_OCTET_STREAM) @Consumes(MediaType.APPLICATION_JSON) - public byte[] generateAndGetKeystore(final KeyStoreConfig config) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_ATTRIBUTE_CERTIFICATE) + @Operation( summary = + "Generate a new keypair and certificate, and get the private key file\n" + + "\n" + + "Generates a keypair and certificate and serves the private key in a specified keystore format.\n" + + "Only generated public certificate is saved in Keycloak DB - the private key is not.") + public byte[] generateAndGetKeystore(@Parameter(description = "Keystore configuration as JSON") final KeyStoreConfig config) { auth.clients().requireConfigure(client); checkKeystoreFormat(config); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientInitialAccessResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientInitialAccessResource.java index 4f0f1d2013c..82bff510966 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientInitialAccessResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientInitialAccessResource.java @@ -17,6 +17,9 @@ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.keycloak.http.HttpResponse; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; @@ -26,6 +29,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation; import org.keycloak.representations.idm.ClientInitialAccessPresentation; import org.keycloak.services.clientregistration.ClientRegistrationTokenUtils; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import jakarta.ws.rs.Consumes; @@ -44,6 +48,7 @@ import java.util.stream.Stream; * @resource Client Initial Access * @author Stian Thorgersen */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class ClientInitialAccessResource { private final AdminPermissionEvaluator auth; @@ -69,6 +74,8 @@ public class ClientInitialAccessResource { @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_INITIAL_ACCESS) + @Operation( summary = "Create a new initial access token.") public ClientInitialAccessPresentation create(ClientInitialAccessCreatePresentation config) { auth.clients().requireManage(); @@ -94,6 +101,8 @@ public class ClientInitialAccessResource { @GET @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_INITIAL_ACCESS) + @Operation() public Stream list() { auth.clients().requireView(); @@ -102,6 +111,8 @@ public class ClientInitialAccessResource { @DELETE @Path("{id}") + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_INITIAL_ACCESS) + @Operation() public void delete(final @PathParam("id") String id) { auth.clients().requireManage(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientPoliciesResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientPoliciesResource.java index 6390a842d02..b6de1d0a9bc 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientPoliciesResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientPoliciesResource.java @@ -17,6 +17,7 @@ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; @@ -25,6 +26,8 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.http.HttpRequest; @@ -34,8 +37,10 @@ import org.keycloak.models.RealmModel; import org.keycloak.representations.idm.ClientPoliciesRepresentation; import org.keycloak.services.ErrorResponse; import org.keycloak.services.clientpolicy.ClientPolicyException; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class ClientPoliciesResource { protected static final Logger logger = Logger.getLogger(ClientPoliciesResource.class); @@ -59,6 +64,8 @@ public class ClientPoliciesResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public ClientPoliciesRepresentation getPolicies() { auth.realm().requireViewRealm(); @@ -71,6 +78,8 @@ public class ClientPoliciesResource { @PUT @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public Response updatePolicies(final ClientPoliciesRepresentation clientPolicies) { auth.realm().requireManageRealm(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientProfilesResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientProfilesResource.java index 8469e415a59..2189d804cda 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientProfilesResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientProfilesResource.java @@ -17,6 +17,7 @@ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; @@ -26,6 +27,8 @@ import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.http.HttpRequest; @@ -35,8 +38,10 @@ import org.keycloak.models.RealmModel; import org.keycloak.representations.idm.ClientProfilesRepresentation; import org.keycloak.services.ErrorResponse; import org.keycloak.services.clientpolicy.ClientPolicyException; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class ClientProfilesResource { protected static final Logger logger = Logger.getLogger(ClientProfilesResource.class); @@ -60,6 +65,8 @@ public class ClientProfilesResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public ClientProfilesRepresentation getProfiles(@QueryParam("include-global-profiles") boolean includeGlobalProfiles) { auth.realm().requireViewRealm(); @@ -72,6 +79,8 @@ public class ClientProfilesResource { @PUT @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public Response updateProfiles(final ClientProfilesRepresentation clientProfiles) { auth.realm().requireManageRealm(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientRegistrationPolicyResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientRegistrationPolicyResource.java index b545ab90261..365238bd049 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientRegistrationPolicyResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientRegistrationPolicyResource.java @@ -17,6 +17,9 @@ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.events.admin.ResourceType; import org.keycloak.models.KeycloakSession; @@ -27,6 +30,7 @@ import org.keycloak.provider.ProviderFactory; import org.keycloak.representations.idm.ComponentTypeRepresentation; import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy; import org.keycloak.services.clientregistration.policy.ClientRegistrationPolicyFactory; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import jakarta.ws.rs.GET; @@ -40,6 +44,7 @@ import java.util.stream.Stream; * @resource Client Registration Policy * @author Marek Posolda */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class ClientRegistrationPolicyResource { private final AdminPermissionEvaluator auth; @@ -66,6 +71,8 @@ public class ClientRegistrationPolicyResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_REGISTRATION_POLICY) + @Operation( summary="Base path for retrieve providers with the configProperties properly filled") public Stream getProviders() { return session.getKeycloakSessionFactory().getProviderFactoriesStream(ClientRegistrationPolicy.class) .map((ProviderFactory factory) -> { 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 59673bd9a2f..c3513a8279d 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 @@ -16,7 +16,11 @@ */ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import jakarta.ws.rs.core.Response.Status; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.BadRequestException; @@ -65,6 +69,7 @@ import org.keycloak.services.clientregistration.policy.RegistrationAuth; import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.ResourceAdminManager; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; import org.keycloak.services.resources.admin.permissions.AdminPermissions; @@ -99,6 +104,7 @@ import static java.lang.Boolean.TRUE; * @author Bill Burke * @version $Revision: 1 $ */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class ClientResource { protected static final Logger logger = Logger.getLogger(ClientResource.class); protected RealmModel realm; @@ -132,6 +138,8 @@ public class ClientResource { */ @PUT @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Update the client") public Response update(final ClientRepresentation rep) { auth.clients().requireConfigure(client); @@ -174,6 +182,8 @@ public class ClientResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Get representation of the client") public ClientRepresentation getClient() { try { session.clientPolicy().triggerOnEvent(new AdminClientViewContext(client, auth.adminAuth())); @@ -204,6 +214,8 @@ public class ClientResource { @GET @NoCache @Path("installation/providers/{providerId}") + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation() public Response getInstallationProvider(@PathParam("providerId") String providerId) { auth.clients().requireView(client); @@ -218,6 +230,8 @@ public class ClientResource { */ @DELETE @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Delete the client") public void deleteClient() { auth.clients().requireManage(client); @@ -250,6 +264,8 @@ public class ClientResource { @POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Generate a new secret for the client") public CredentialRepresentation regenerateSecret() { try{ auth.clients().requireConfigure(client); @@ -293,6 +309,8 @@ public class ClientResource { @POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Generate a new registration access token for the client") public ClientRepresentation regenerateRegistrationAccessToken() { auth.clients().requireManage(client); @@ -314,6 +332,8 @@ public class ClientResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Get the client secret") public CredentialRepresentation getClientSecret() { auth.clients().requireView(client); @@ -350,6 +370,8 @@ public class ClientResource { @NoCache @Produces(MediaType.APPLICATION_JSON) @Path("default-client-scopes") + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Get default client scopes. Only name and ids are returned.") public Stream getDefaultClientScopes() { return getDefaultClientScopes(true); } @@ -364,6 +386,8 @@ public class ClientResource { @PUT @NoCache @Path("default-client-scopes/{clientScopeId}") + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation() public void addDefaultClientScope(@PathParam("clientScopeId") String clientScopeId) { addDefaultClientScope(clientScopeId,true); } @@ -387,6 +411,8 @@ public class ClientResource { @DELETE @NoCache @Path("default-client-scopes/{clientScopeId}") + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation() public void removeDefaultClientScope(@PathParam("clientScopeId") String clientScopeId) { auth.clients().requireManage(client); @@ -409,6 +435,8 @@ public class ClientResource { @NoCache @Produces(MediaType.APPLICATION_JSON) @Path("optional-client-scopes") + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Get optional client scopes. Only name and ids are returned.") public Stream getOptionalClientScopes() { return getDefaultClientScopes(false); } @@ -416,6 +444,8 @@ public class ClientResource { @PUT @NoCache @Path("optional-client-scopes/{clientScopeId}") + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation() public void addOptionalClientScope(@PathParam("clientScopeId") String clientScopeId) { addDefaultClientScope(clientScopeId, false); } @@ -423,6 +453,8 @@ public class ClientResource { @DELETE @NoCache @Path("optional-client-scopes/{clientScopeId}") + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation() public void removeOptionalClientScope(@PathParam("clientScopeId") String clientScopeId) { removeDefaultClientScope(clientScopeId); } @@ -441,6 +473,8 @@ public class ClientResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Get a user dedicated to the service account") public UserRepresentation getServiceAccountUser() { auth.clients().requireView(client); @@ -465,6 +499,8 @@ public class ClientResource { @Path("push-revocation") @POST @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Push the client's revocation policy to its admin URL If the client has an admin URL, push revocation policy to it.") public GlobalRequestResult pushRevocation() { auth.clients().requireConfigure(client); @@ -488,6 +524,8 @@ public class ClientResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Get application session count Returns a number of user sessions associated with this client { \"count\": number }") public Map getApplicationSessionCount() { auth.clients().requireView(client); @@ -509,7 +547,9 @@ public class ClientResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public Stream getUserSessions(@QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults) { + @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) { auth.clients().requireView(client); firstResult = firstResult != null ? firstResult : -1; @@ -533,6 +573,8 @@ public class ClientResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Get application offline session count Returns a number of offline user sessions associated with this client { \"count\": number }") public Map getOfflineSessionCount() { auth.clients().requireView(client); @@ -554,7 +596,9 @@ public class ClientResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public Stream getOfflineUserSessions(@QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults) { + @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) { auth.clients().requireView(client); firstResult = firstResult != null ? firstResult : -1; @@ -575,6 +619,8 @@ public class ClientResource { @Path("nodes") @POST @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Register a cluster node with the client Manually register cluster node to this client - usually it’s not needed to call this directly as adapter should handle by sending registration request to Keycloak") public void registerNode(Map formParams) { auth.clients().requireConfigure(client); @@ -598,6 +644,8 @@ public class ClientResource { @Path("nodes/{node}") @DELETE @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Unregister a cluster node from the client") public void unregisterNode(final @PathParam("node") String node) { auth.clients().requireConfigure(client); @@ -622,6 +670,8 @@ public class ClientResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Test if registered cluster nodes are available Tests availability by sending 'ping' request to all cluster nodes.") public GlobalRequestResult testNodesAvailable() { auth.clients().requireConfigure(client); @@ -647,6 +697,8 @@ public class ClientResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Return object stating whether client Authorization permissions have been initialized or not and a reference") public ManagementPermissionReference getManagementPermissions() { auth.roles().requireView(client); @@ -677,6 +729,8 @@ public class ClientResource { @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Return object stating whether client Authorization permissions have been initialized or not and a reference") public ManagementPermissionReference setManagementPermissionsEnabled(ManagementPermissionReference ref) { auth.clients().requireManage(client); AdminPermissionManagement permissions = AdminPermissions.management(session, realm); @@ -697,6 +751,8 @@ public class ClientResource { @DELETE @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Invalidate the rotated secret for the client") public Response invalidateRotatedSecret() { try{ auth.clients().requireConfigure(client); @@ -729,6 +785,8 @@ public class ClientResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Get the rotated client secret") public CredentialRepresentation getClientRotatedSecret() { auth.clients().requireView(client); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientRoleMappingsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientRoleMappingsResource.java index ae5b9aee84e..dd6f5fa2edb 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientRoleMappingsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientRoleMappingsResource.java @@ -16,10 +16,15 @@ */ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import jakarta.ws.rs.NotFoundException; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; @@ -56,6 +61,7 @@ import java.util.stream.Stream; * @author Bill Burke * @version $Revision: 1 $ */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class ClientRoleMappingsResource { protected static final Logger logger = Logger.getLogger(ClientRoleMappingsResource.class); @@ -92,6 +98,8 @@ public class ClientRoleMappingsResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_ROLE_MAPPINGS) + @Operation( summary = "Get client-level role mappings for the user, and the app") public Stream getClientRoleMappings() { viewPermission.require(); @@ -111,7 +119,9 @@ public class ClientRoleMappingsResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache - public Stream getCompositeClientRoleMappings(@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_ROLE_MAPPINGS) + @Operation( summary = "Get effective client-level role mappings This recurses any composite roles") + public Stream getCompositeClientRoleMappings(@Parameter(description = "if false, return roles with their attributes") @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) { viewPermission.require(); Stream roles = client.getRolesStream(); @@ -129,6 +139,8 @@ public class ClientRoleMappingsResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_ROLE_MAPPINGS) + @Operation( summary = "Get available client-level roles that can be mapped to the user") public Stream getAvailableClientRoleMappings() { viewPermission.require(); @@ -145,6 +157,8 @@ public class ClientRoleMappingsResource { */ @POST @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_ROLE_MAPPINGS) + @Operation( summary = "Add client-level roles to the user role mapping") public void addClientRoleMapping(List roles) { managePermission.require(); @@ -167,12 +181,14 @@ public class ClientRoleMappingsResource { } /** - * Delete client-level roles from user role mapping - * - * @param roles - */ + * Delete client-level roles from user role mapping + * + * @param roles + */ @DELETE @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_ROLE_MAPPINGS) + @Operation( summary = "Delete client-level roles from user role mapping") public void deleteClientRoleMapping(List roles) { managePermission.require(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeEvaluateResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeEvaluateResource.java index 63e7c6de394..42aa623102a 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeEvaluateResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeEvaluateResource.java @@ -24,6 +24,9 @@ import java.util.Objects; import java.util.function.BiFunction; import java.util.stream.Stream; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.Path; @@ -34,6 +37,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.UriInfo; import com.fasterxml.jackson.annotation.JsonProperty; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.common.ClientConnection; @@ -54,6 +58,7 @@ import org.keycloak.services.Urls; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.UserSessionManager; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.RootAuthenticationSessionModel; @@ -61,6 +66,7 @@ import org.keycloak.sessions.RootAuthenticationSessionModel; /** * @author Marek Posolda */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class ClientScopeEvaluateResource { protected static final Logger logger = Logger.getLogger(ClientScopeEvaluateResource.class); @@ -91,7 +97,7 @@ public class ClientScopeEvaluateResource { * @return */ @Path("scope-mappings/{roleContainerId}") - public ClientScopeEvaluateScopeMappingsResource scopeMappings(@QueryParam("scope") String scopeParam, @PathParam("roleContainerId") String roleContainerId) { + public ClientScopeEvaluateScopeMappingsResource scopeMappings(@QueryParam("scope") String scopeParam, @Parameter(description = "either realm name OR client UUID") @PathParam("roleContainerId") String roleContainerId) { auth.clients().requireView(client); if (roleContainerId == null) { @@ -117,6 +123,9 @@ public class ClientScopeEvaluateResource { @Path("protocol-mappers") @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Return list of all protocol mappers, which will be used when generating tokens issued for particular client.", + description = "This means protocol mappers assigned to this client directly and protocol mappers assigned to all client scopes of this client.") public Stream getGrantedProtocolMappers(@QueryParam("scope") String scopeParam) { auth.clients().requireView(client); @@ -157,6 +166,8 @@ public class ClientScopeEvaluateResource { @Path("generate-example-userinfo") @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Create JSON with payload of example user info") public Map generateExampleUserinfo(@QueryParam("scope") String scopeParam, @QueryParam("userId") String userId) { auth.clients().requireView(client); @@ -182,6 +193,8 @@ public class ClientScopeEvaluateResource { @Path("generate-example-id-token") @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Create JSON with payload of example id token") public IDToken generateExampleIdToken(@QueryParam("scope") String scopeParam, @QueryParam("userId") String userId) { auth.clients().requireView(client); @@ -206,6 +219,8 @@ public class ClientScopeEvaluateResource { @Path("generate-example-access-token") @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Create JSON with payload of example access token") public AccessToken generateExampleAccessToken(@QueryParam("scope") String scopeParam, @QueryParam("userId") String userId) { auth.clients().requireView(client); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeEvaluateScopeMappingsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeEvaluateScopeMappingsResource.java index d804bb1b724..f0e572a2b66 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeEvaluateScopeMappingsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeEvaluateScopeMappingsResource.java @@ -22,11 +22,14 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.eclipse.microprofile.openapi.annotations.Operation; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientScopeModel; @@ -35,11 +38,13 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; /** * @author Marek Posolda */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class ClientScopeEvaluateScopeMappingsResource { private final RoleContainerModel roleContainer; @@ -68,6 +73,9 @@ public class ClientScopeEvaluateScopeMappingsResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Get effective scope mapping of all roles of particular role container, which this client is defacto allowed to have in the accessToken issued for him.", + description = "This contains scope mappings, which this client has directly, as well as scope mappings, which are granted to all client scopes, which are linked with this client.") public Stream getGrantedScopeMappings() { return getGrantedRoles().map(ModelToRepresentation::toBriefRepresentation); } @@ -83,6 +91,8 @@ public class ClientScopeEvaluateScopeMappingsResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Get roles, which this client doesn't have scope for and can't have them in the accessToken issued for him.", description = "Defacto all the other roles of particular role container, which are not in {@link #getGrantedScopeMappings()}") public Stream getNotGrantedScopeMappings() { Set grantedRoles = getGrantedRoles().collect(Collectors.toSet()); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeResource.java index ebbbe6d096e..927e43f8126 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopeResource.java @@ -16,6 +16,9 @@ */ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.common.Profile; @@ -32,6 +35,7 @@ import org.keycloak.representations.idm.ClientScopeRepresentation; import org.keycloak.saml.common.util.StringUtil; import org.keycloak.services.ErrorResponse; import org.keycloak.services.ErrorResponseException; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import jakarta.ws.rs.Consumes; @@ -54,6 +58,7 @@ import java.util.regex.Pattern; * @author Bill Burke * @version $Revision: 1 $ */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class ClientScopeResource { protected static final Logger logger = Logger.getLogger(ClientScopeResource.class); protected RealmModel realm; @@ -99,6 +104,8 @@ public class ClientScopeResource { */ @PUT @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_SCOPES) + @Operation(summary = "Update the client scope") public Response update(final ClientScopeRepresentation rep) { auth.clients().requireManageClientScopes(); validateDynamicScopeUpdate(rep); @@ -124,6 +131,8 @@ public class ClientScopeResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_SCOPES) + @Operation(summary = "Get representation of the client scope") public ClientScopeRepresentation getClientScope() { auth.clients().requireView(clientScope); @@ -136,6 +145,8 @@ public class ClientScopeResource { */ @DELETE @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_SCOPES) + @Operation(summary = "Delete the client scope") public Response deleteClientScope() { auth.clients().requireManage(clientScope); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopesResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopesResource.java index 5bbbea2aabe..5c378ab02c9 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientScopesResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientScopesResource.java @@ -16,6 +16,9 @@ */ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.events.admin.OperationType; @@ -28,6 +31,7 @@ import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.representations.idm.ClientScopeRepresentation; import org.keycloak.services.ErrorResponse; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import jakarta.ws.rs.Consumes; @@ -48,6 +52,7 @@ import java.util.stream.Stream; * @author Bill Burke * @version $Revision: 1 $ */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class ClientScopesResource { protected static final Logger logger = Logger.getLogger(ClientScopesResource.class); protected final RealmModel realm; @@ -71,6 +76,8 @@ public class ClientScopesResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_SCOPES) + @Operation( summary = "Get client scopes belonging to the realm Returns a list of client scopes belonging to the realm") public Stream getClientScopes() { auth.clients().requireListClientScopes(); @@ -90,6 +97,8 @@ public class ClientScopesResource { @POST @Consumes(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENT_SCOPES) + @Operation( summary = "Create a new client scope Client Scope’s name must be unique!") public Response createClientScope(ClientScopeRepresentation rep) { auth.clients().requireManageClientScopes(); ClientScopeResource.validateClientScopeName(rep.getName()); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java index d9ef850cbb0..1e0b9dbaba4 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java @@ -16,6 +16,10 @@ */ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.authorization.admin.AuthorizationService; @@ -39,6 +43,7 @@ import org.keycloak.services.clientpolicy.context.AdminClientRegisterContext; import org.keycloak.services.clientpolicy.context.AdminClientRegisteredContext; import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.RealmManager; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.utils.SearchQueryUtils; import org.keycloak.validation.ValidationUtil; @@ -67,6 +72,7 @@ import static org.keycloak.utils.StreamsUtil.paginatedStream; * @author Bill Burke * @version $Revision: 1 $ */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class ClientsResource { protected static final Logger logger = Logger.getLogger(ClientsResource.class); protected final RealmModel realm; @@ -99,12 +105,15 @@ public class ClientsResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache - public Stream getClients(@QueryParam("clientId") String clientId, - @QueryParam("viewableOnly") @DefaultValue("false") boolean viewableOnly, - @QueryParam("search") @DefaultValue("false") boolean search, + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Get clients belonging to the realm.", + description = "If a client can’t be retrieved from the storage due to a problem with the underlying storage, it is silently removed from the returned list. This ensures that concurrent modifications to the list don’t prevent callers from retrieving this list.") + public Stream getClients(@Parameter(description = "filter by clientId") @QueryParam("clientId") String clientId, + @Parameter(description = "filter clients that cannot be viewed in full by admin") @QueryParam("viewableOnly") @DefaultValue("false") boolean viewableOnly, + @Parameter(description = "whether this is a search query or a getClientById query") @QueryParam("search") @DefaultValue("false") boolean search, @QueryParam("q") String searchQuery, - @QueryParam("first") Integer firstResult, - @QueryParam("max") Integer maxResults) { + @Parameter(description = "the first result") @QueryParam("first") Integer firstResult, + @Parameter(description = "the max results to return") @QueryParam("max") Integer maxResults) { auth.clients().requireList(); boolean canView = auth.clients().canView(); @@ -167,6 +176,8 @@ public class ClientsResource { */ @POST @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.CLIENTS) + @Operation( summary = "Create a new client Client’s client_id must be unique!") public Response createClient(final ClientRepresentation rep) { auth.clients().requireManage(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java index fcc35ec0e92..95de2151f5a 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ComponentResource.java @@ -16,6 +16,9 @@ */ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import jakarta.ws.rs.NotFoundException; @@ -39,6 +42,7 @@ import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.ComponentTypeRepresentation; import org.keycloak.representations.idm.ConfigPropertyRepresentation; import org.keycloak.services.ErrorResponse; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.utils.LockObjectsForModification; @@ -68,6 +72,7 @@ import java.util.stream.Stream; * @author Bill Burke * @version $Revision: 1 $ */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class ComponentResource { protected static final Logger logger = Logger.getLogger(ComponentResource.class); @@ -95,6 +100,8 @@ public class ComponentResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.COMPONENT) + @Operation() public Stream getComponents(@QueryParam("parent") String parent, @QueryParam("type") String type, @QueryParam("name") String name) { @@ -125,6 +132,8 @@ public class ComponentResource { @POST @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.COMPONENT) + @Operation() public Response create(ComponentRepresentation rep) { auth.realm().requireManageRealm(); return KeycloakModelUtils.runJobInRetriableTransaction(session.getKeycloakSessionFactory(), kcSession -> { @@ -149,6 +158,8 @@ public class ComponentResource { @Path("{id}") @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.COMPONENT) + @Operation() public ComponentRepresentation getComponent(@PathParam("id") String id) { auth.realm().requireViewRealm(); ComponentModel model = realm.getComponent(id); @@ -162,6 +173,8 @@ public class ComponentResource { @PUT @Path("{id}") @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.COMPONENT) + @Operation() public Response updateComponent(@PathParam("id") String id, ComponentRepresentation rep) { auth.realm().requireManageRealm(); return KeycloakModelUtils.runJobInRetriableTransaction(session.getKeycloakSessionFactory(), kcSession -> { @@ -184,6 +197,8 @@ public class ComponentResource { } @DELETE @Path("{id}") + @Tag(name = KeycloakOpenAPI.Admin.Tags.COMPONENT) + @Operation() public void removeComponent(@PathParam("id") String id) { auth.realm().requireManageRealm(); KeycloakModelUtils.runJobInRetriableTransaction(session.getKeycloakSessionFactory(), kcSession -> { @@ -228,6 +243,8 @@ public class ComponentResource { @Path("{id}/sub-component-types") @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.COMPONENT) + @Operation( summary = "List of subcomponent types that are available to configure for a particular parent component.") public Stream getSubcomponentConfig(@PathParam("id") String parentId, @QueryParam("type") String subtype) { auth.realm().requireViewRealm(); ComponentModel parent = realm.getComponent(parentId); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java b/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java index 1659c7a7df7..4420a3978cd 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java @@ -16,6 +16,10 @@ */ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.resteasy.annotations.cache.NoCache; import jakarta.ws.rs.NotFoundException; import org.keycloak.common.util.ObjectUtil; @@ -32,6 +36,8 @@ import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.ManagementPermissionReference; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.ErrorResponse; +import org.keycloak.services.Urls; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; import org.keycloak.services.resources.admin.permissions.AdminPermissions; @@ -58,6 +64,7 @@ import java.util.stream.Stream; * @resource Groups * @author Bill Burke */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class GroupResource { private final RealmModel realm; @@ -82,6 +89,8 @@ public class GroupResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.GROUPS) + @Operation() public GroupRepresentation getGroup() { this.auth.groups().requireView(group); @@ -99,6 +108,8 @@ public class GroupResource { */ @PUT @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.GROUPS) + @Operation( summary = "Update group, ignores subgroups.") public Response updateGroup(GroupRepresentation rep) { this.auth.groups().requireManage(group); @@ -130,6 +141,8 @@ public class GroupResource { } @DELETE + @Tag(name = KeycloakOpenAPI.Admin.Tags.GROUPS) + @Operation() public void deleteGroup() { this.auth.groups().requireManage(group); @@ -149,6 +162,8 @@ public class GroupResource { @NoCache @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.GROUPS) + @Operation( summary = "Set or create child.", description = "This will just set the parent if it exists. Create it and set the parent if the group doesn’t exist.") public Response addChild(GroupRepresentation rep) { this.auth.groups().requireManage(group); @@ -267,9 +282,12 @@ public class GroupResource { @NoCache @Path("members") @Produces(MediaType.APPLICATION_JSON) - public Stream getMembers(@QueryParam("first") Integer firstResult, - @QueryParam("max") Integer maxResults, - @QueryParam("briefRepresentation") Boolean briefRepresentation) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.GROUPS) + @Operation( summary = "Get users Returns a stream of users, filtered according to query parameters") + public Stream getMembers(@Parameter(description = "Pagination offset") @QueryParam("first") Integer firstResult, + @Parameter(description = "Maximum results size (defaults to 100)") @QueryParam("max") Integer maxResults, + @Parameter(description = "Only return basic information (only guaranteed to return id, username, created, first and last name, email, enabled state, email verification state, federation link, and access. Note that it means that namely user attributes, required actions, and not before are not returned.)") + @QueryParam("briefRepresentation") Boolean briefRepresentation) { this.auth.groups().requireViewMembers(group); firstResult = firstResult != null ? firstResult : 0; @@ -291,6 +309,8 @@ public class GroupResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.GROUPS) + @Operation( summary = "Return object stating whether client Authorization permissions have been initialized or not and a reference") public ManagementPermissionReference getManagementPermissions() { auth.groups().requireView(group); @@ -321,6 +341,8 @@ public class GroupResource { @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.GROUPS) + @Operation( summary = "Return object stating whether client Authorization permissions have been initialized or not and a reference") public ManagementPermissionReference setManagementPermissionsEnabled(ManagementPermissionReference ref) { auth.groups().requireManage(group); AdminPermissionManagement permissions = AdminPermissions.management(session, realm); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java index f191791b4e0..34e51330666 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java @@ -16,6 +16,9 @@ */ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.resteasy.annotations.cache.NoCache; import jakarta.ws.rs.NotFoundException; import org.keycloak.common.util.ObjectUtil; @@ -28,6 +31,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.services.ErrorResponse; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.utils.SearchQueryUtils; @@ -51,6 +55,7 @@ import java.util.stream.Stream; * @resource Groups * @author Bill Burke */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class GroupsResource { private final RealmModel realm; @@ -74,6 +79,8 @@ public class GroupsResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.GROUPS) + @Operation( summary = "Get group hierarchy. Only name and ids are returned.") public Stream getGroups(@QueryParam("search") String search, @QueryParam("q") String searchQuery, @QueryParam("exact") @DefaultValue("false") Boolean exact, @@ -119,6 +126,8 @@ public class GroupsResource { @NoCache @Path("count") @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.GROUPS) + @Operation( summary = "Returns the groups counts.") public Map getGroupCount(@QueryParam("search") String search, @QueryParam("top") @DefaultValue("false") boolean onlyTopGroups) { Long results; @@ -140,6 +149,9 @@ public class GroupsResource { */ @POST @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.GROUPS) + @Operation( summary = "create or add a top level realm groupSet or create child.", + description = "This will update the group and set the parent if it exists. Create it and set the parent if the group doesn’t exist.") public Response addTopLevelGroup(GroupRepresentation rep) { auth.groups().requireManage(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java index c818d93c6ac..e6d6081ced0 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java @@ -19,6 +19,10 @@ package org.keycloak.services.resources.admin; import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST; import com.google.common.collect.Streams; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import jakarta.ws.rs.NotFoundException; @@ -45,6 +49,7 @@ import org.keycloak.representations.idm.IdentityProviderMapperTypeRepresentation import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.ManagementPermissionReference; import org.keycloak.services.ErrorResponse; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; import org.keycloak.services.resources.admin.permissions.AdminPermissions; @@ -73,6 +78,7 @@ import java.util.stream.Stream; * @resource Identity Providers * @author Pedro Igor */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class IdentityProviderResource { protected static final Logger logger = Logger.getLogger(IdentityProviderResource.class); @@ -99,6 +105,8 @@ public class IdentityProviderResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS) + @Operation( summary = "Get the identity provider") public IdentityProviderRepresentation getIdentityProvider() { this.auth.realm().requireViewIdentityProviders(); @@ -117,6 +125,8 @@ public class IdentityProviderResource { */ @DELETE @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS) + @Operation( summary = "Delete the identity provider") public Response delete() { this.auth.realm().requireManageIdentityProviders(); @@ -145,6 +155,8 @@ public class IdentityProviderResource { @PUT @Consumes(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS) + @Operation( summary = "Update the identity provider") public Response update(IdentityProviderRepresentation providerRep) { this.auth.realm().requireManageIdentityProviders(); @@ -250,7 +262,9 @@ public class IdentityProviderResource { @GET @Path("export") @NoCache - public Response export(@QueryParam("format") String format) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS) + @Operation( summary = "Export public broker configuration for identity provider") + public Response export(@Parameter(description = "Format to use") @QueryParam("format") String format) { this.auth.realm().requireViewIdentityProviders(); if (identityProviderModel == null) { @@ -271,6 +285,8 @@ public class IdentityProviderResource { @GET @Path("mapper-types") @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS) + @Operation( summary = "Get mapper types for identity provider") public Map getMapperTypes() { this.auth.realm().requireViewIdentityProviders(); @@ -308,6 +324,8 @@ public class IdentityProviderResource { @Path("mappers") @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS) + @Operation( summary = "Get mappers for identity provider") public Stream getMappers() { this.auth.realm().requireViewIdentityProviders(); @@ -328,6 +346,8 @@ public class IdentityProviderResource { @POST @Path("mappers") @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS) + @Operation( summary = "Add a mapper to identity provider") public Response addMapper(IdentityProviderMapperRepresentation mapper) { this.auth.realm().requireManageIdentityProviders(); @@ -359,6 +379,8 @@ public class IdentityProviderResource { @NoCache @Path("mappers/{id}") @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS) + @Operation( summary = "Get mapper by id for the identity provider") public IdentityProviderMapperRepresentation getMapperById(@PathParam("id") String id) { this.auth.realm().requireViewIdentityProviders(); @@ -381,7 +403,9 @@ public class IdentityProviderResource { @NoCache @Path("mappers/{id}") @Consumes(MediaType.APPLICATION_JSON) - public void update(@PathParam("id") String id, IdentityProviderMapperRepresentation rep) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS) + @Operation( summary = "Update a mapper for the identity provider") + public void update(@Parameter(description = "Mapper id") @PathParam("id") String id, IdentityProviderMapperRepresentation rep) { this.auth.realm().requireManageIdentityProviders(); if (identityProviderModel == null) { @@ -404,7 +428,9 @@ public class IdentityProviderResource { @DELETE @NoCache @Path("mappers/{id}") - public void delete(@PathParam("id") String id) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS) + @Operation( summary = "Delete a mapper for the identity provider") + public void delete(@Parameter(description = "Mapper id") @PathParam("id") String id) { this.auth.realm().requireManageIdentityProviders(); if (identityProviderModel == null) { @@ -427,6 +453,8 @@ public class IdentityProviderResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS) + @Operation( summary = "Return object stating whether client Authorization permissions have been initialized or not and a reference") public ManagementPermissionReference getManagementPermissions() { this.auth.realm().requireViewIdentityProviders(); @@ -457,6 +485,8 @@ public class IdentityProviderResource { @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS) + @Operation( summary = "Return object stating whether client Authorization permissions have been initialized or not and a reference") public ManagementPermissionReference setManagementPermissionsEnabled(ManagementPermissionReference ref) { this.auth.realm().requireManageIdentityProviders(); AdminPermissionManagement permissions = AdminPermissions.management(session, realm); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java index 17cbd36441a..bdf47ede5fa 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java @@ -18,6 +18,10 @@ package org.keycloak.services.resources.admin; import com.google.common.collect.Streams; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderFactory; @@ -36,6 +40,7 @@ import org.keycloak.models.utils.StripSecretsUtils; import org.keycloak.provider.ProviderFactory; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.services.ErrorResponse; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import jakarta.ws.rs.BadRequestException; @@ -61,6 +66,7 @@ import org.keycloak.utils.ReservedCharValidator; * @resource Identity Providers * @author Pedro Igor */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class IdentityProvidersResource { private final RealmModel realm; @@ -85,7 +91,9 @@ public class IdentityProvidersResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public Response getIdentityProviders(@PathParam("provider_id") String providerId) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS) + @Operation( summary = "Get identity providers") + public Response getIdentityProviders(@Parameter(description = "Provider id") @PathParam("provider_id") String providerId) { this.auth.realm().requireViewIdentityProviders(); IdentityProviderFactory providerFactory = getProviderFactoryById(providerId); if (providerFactory != null) { @@ -105,6 +113,8 @@ public class IdentityProvidersResource { @Path("import-config") @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS) + @Operation( description = "Import identity provider from uploaded JSON file") public Map importFrom() throws IOException { this.auth.realm().requireManageIdentityProviders(); MultivaluedMap formDataMap = session.getContext().getHttpRequest().getMultiPartFormParameters(); @@ -129,7 +139,9 @@ public class IdentityProvidersResource { @Path("import-config") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - public Map importFrom(Map data) throws IOException { + @Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS) + @Operation( summary = "Import identity provider from JSON body") + public Map importFrom(@Parameter(description = "JSON body") Map data) throws IOException { this.auth.realm().requireManageIdentityProviders(); if (data == null || !(data.containsKey("providerId") && data.containsKey("fromUrl"))) { throw new BadRequestException(); @@ -162,6 +174,8 @@ public class IdentityProvidersResource { @Path("instances") @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS) + @Operation( summary = "Get identity providers") public Stream getIdentityProviders() { this.auth.realm().requireViewIdentityProviders(); @@ -178,7 +192,9 @@ public class IdentityProvidersResource { @POST @Path("instances") @Consumes(MediaType.APPLICATION_JSON) - public Response create(IdentityProviderRepresentation representation) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS) + @Operation( summary = "Create a new identity provider") + public Response create(@Parameter(description = "JSON body") IdentityProviderRepresentation representation) { this.auth.realm().requireManageIdentityProviders(); ReservedCharValidator.validate(representation.getAlias()); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/KeyResource.java b/services/src/main/java/org/keycloak/services/resources/admin/KeyResource.java index b2fa8e012aa..bed96312c39 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/KeyResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/KeyResource.java @@ -17,12 +17,16 @@ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.common.util.PemUtils; import org.keycloak.crypto.KeyWrapper; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.representations.idm.KeysMetadataRepresentation; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import jakarta.ws.rs.GET; @@ -36,6 +40,7 @@ import java.util.stream.Collectors; * @resource Key * @author Stian Thorgersen */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class KeyResource { private RealmModel realm; @@ -51,6 +56,8 @@ public class KeyResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.KEY) + @Operation() public KeysMetadataRepresentation getKeyMetadata() { auth.realm().requireViewRealm(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java index 9cc37f0fbea..390e5ecb525 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ProtocolMappersResource.java @@ -18,6 +18,10 @@ package org.keycloak.services.resources.admin; import static org.keycloak.protocol.ProtocolMapperUtils.isEnabled; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import jakarta.ws.rs.NotFoundException; @@ -35,6 +39,7 @@ import org.keycloak.protocol.ProtocolMapperConfigException; import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.services.ErrorResponse; import org.keycloak.services.ErrorResponseException; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import jakarta.ws.rs.Consumes; @@ -60,6 +65,7 @@ import java.util.stream.Stream; * @author Bill Burke * @version $Revision: 1 $ */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class ProtocolMappersResource { protected static final Logger logger = Logger.getLogger(ProtocolMappersResource.class); @@ -99,6 +105,8 @@ public class ProtocolMappersResource { @NoCache @Path("protocol/{protocol}") @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.PROTOCOL_MAPPERS) + @Operation(summary = "Get mappers by name for a specific protocol") public Stream getMappersPerProtocol(@PathParam("protocol") String protocol) { viewPermission.require(); @@ -116,6 +124,8 @@ public class ProtocolMappersResource { @POST @NoCache @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.PROTOCOL_MAPPERS) + @Operation(summary = "Create a mapper") public Response createMapper(ProtocolMapperRepresentation rep) { managePermission.require(); @@ -140,6 +150,8 @@ public class ProtocolMappersResource { @POST @NoCache @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.PROTOCOL_MAPPERS) + @Operation(summary = "Create multiple mappers") public void createMapper(List reps) { managePermission.require(); @@ -161,6 +173,8 @@ public class ProtocolMappersResource { @NoCache @Path("models") @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.PROTOCOL_MAPPERS) + @Operation(summary = "Get mappers") public Stream getMappers() { viewPermission.require(); @@ -179,7 +193,9 @@ public class ProtocolMappersResource { @NoCache @Path("models/{id}") @Produces(MediaType.APPLICATION_JSON) - public ProtocolMapperRepresentation getMapperById(@PathParam("id") String id) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.PROTOCOL_MAPPERS) + @Operation(summary = "Get mapper by id") + public ProtocolMapperRepresentation getMapperById(@Parameter(description = "Mapper id") @PathParam("id") String id) { viewPermission.require(); ProtocolMapperModel model = client.getProtocolMapperById(id); @@ -197,7 +213,9 @@ public class ProtocolMappersResource { @NoCache @Path("models/{id}") @Consumes(MediaType.APPLICATION_JSON) - public void update(@PathParam("id") String id, ProtocolMapperRepresentation rep) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.PROTOCOL_MAPPERS) + @Operation(summary = "Update the mapper") + public void update(@Parameter(description = "Mapper id") @PathParam("id") String id, ProtocolMapperRepresentation rep) { managePermission.require(); ProtocolMapperModel model = client.getProtocolMapperById(id); @@ -218,7 +236,9 @@ public class ProtocolMappersResource { @DELETE @NoCache @Path("models/{id}") - public void delete(@PathParam("id") String id) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.PROTOCOL_MAPPERS) + @Operation(summary = "Delete the mapper") + public void delete(@Parameter(description = "Mapper id") @PathParam("id") String id) { managePermission.require(); ProtocolMapperModel model = client.getProtocolMapperById(id); 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 36d693f766f..b47446605f1 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 @@ -31,6 +31,9 @@ import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; @@ -51,6 +54,7 @@ import jakarta.ws.rs.core.StreamingOutput; import com.fasterxml.jackson.core.type.TypeReference; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.ResteasyProviderFactory; @@ -106,6 +110,7 @@ import org.keycloak.services.ErrorResponse; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.ResourceAdminManager; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.ext.AdminRealmResourceProvider; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; @@ -123,6 +128,7 @@ import org.keycloak.utils.ReservedCharValidator; * @author Bill Burke * @version $Revision: 1 $ */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class RealmAdminResource { protected static final Logger logger = Logger.getLogger(RealmAdminResource.class); protected final AdminPermissionEvaluator auth; @@ -153,6 +159,8 @@ public class RealmAdminResource { @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN }) @POST @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation( summary = "Base path for importing clients under this realm.") public ClientRepresentation convertClientDescription(String description) { auth.clients().requireManage(); @@ -226,6 +234,8 @@ public class RealmAdminResource { @NoCache @Produces(MediaType.APPLICATION_JSON) @Path("default-default-client-scopes") + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation( summary = "Get realm default client scopes. Only name and ids are returned.") public Stream getDefaultDefaultClientScopes() { return getDefaultClientScopes(true); } @@ -246,6 +256,8 @@ public class RealmAdminResource { @PUT @NoCache @Path("default-default-client-scopes/{clientScopeId}") + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public void addDefaultDefaultClientScope(@PathParam("clientScopeId") String clientScopeId) { addDefaultClientScope(clientScopeId,true); } @@ -266,6 +278,8 @@ public class RealmAdminResource { @DELETE @NoCache @Path("default-default-client-scopes/{clientScopeId}") + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public void removeDefaultDefaultClientScope(@PathParam("clientScopeId") String clientScopeId) { auth.clients().requireManageClientScopes(); @@ -288,6 +302,8 @@ public class RealmAdminResource { @NoCache @Produces(MediaType.APPLICATION_JSON) @Path("default-optional-client-scopes") + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation( summary = "Get realm optional client scopes. Only name and ids are returned.") public Stream getDefaultOptionalClientScopes() { return getDefaultClientScopes(false); } @@ -295,6 +311,8 @@ public class RealmAdminResource { @PUT @NoCache @Path("default-optional-client-scopes/{clientScopeId}") + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public void addDefaultOptionalClientScope(@PathParam("clientScopeId") String clientScopeId) { addDefaultClientScope(clientScopeId, false); } @@ -302,6 +320,8 @@ public class RealmAdminResource { @DELETE @NoCache @Path("default-optional-client-scopes/{clientScopeId}") + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public void removeDefaultOptionalClientScope(@PathParam("clientScopeId") String clientScopeId) { removeDefaultDefaultClientScope(clientScopeId); } @@ -351,6 +371,8 @@ public class RealmAdminResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation( summary = "Get the top-level representation of the realm It will not include nested information like User and Client representations.") public RealmRepresentation getRealm() { if (auth.realm().canViewRealm()) { return ModelToRepresentation.toRepresentation(session, realm, false); @@ -385,6 +407,9 @@ public class RealmAdminResource { */ @PUT @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation( summary = "Update the top-level information of the realm Any user, roles or client information in the representation will be ignored.", + description = "This will only update top-level attributes of the realm.") public Response updateRealm(final RealmRepresentation rep) { auth.realm().requireManageRealm(); @@ -448,6 +473,8 @@ public class RealmAdminResource { * */ @DELETE + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation( summary = "Delete the realm") public void deleteRealm() { auth.realm().requireManageRealm(); @@ -476,6 +503,8 @@ public class RealmAdminResource { @GET @Produces(MediaType.APPLICATION_JSON) @Path("users-management-permissions") + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public ManagementPermissionReference getUserMgmtPermissions() { auth.realm().requireViewRealm(); @@ -493,6 +522,8 @@ public class RealmAdminResource { @Consumes(MediaType.APPLICATION_JSON) @NoCache @Path("users-management-permissions") + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public ManagementPermissionReference setUsersManagementPermissionsEnabled(ManagementPermissionReference ref) { auth.realm().requireManageRealm(); @@ -552,6 +583,8 @@ public class RealmAdminResource { @Path("push-revocation") @Produces(MediaType.APPLICATION_JSON) @POST + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation( summary = "Push the realm's revocation policy to any client that has an admin url associated with it.") public GlobalRequestResult pushRevocation() { auth.realm().requireManageRealm(); @@ -568,6 +601,8 @@ public class RealmAdminResource { @Path("logout-all") @POST @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation( summary = "Removes all user sessions.", description = "Any client that has an admin url will also be told to invalidate any sessions they have.") public GlobalRequestResult logoutAll() { auth.users().requireManage(); @@ -585,6 +620,8 @@ public class RealmAdminResource { */ @Path("sessions/{session}") @DELETE + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation( summary = "Remove a specific user session.", description = "Any client that has an admin url will also be told to invalidate this particular session.") public void deleteSession(@PathParam("session") String sessionId) { auth.users().requireManage(); @@ -607,6 +644,9 @@ public class RealmAdminResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation( summary = "Get client session stats Returns a JSON map.", + description = "The key is the client id, the value is the number of sessions that currently are active with that client. Only clients that actually have a session associated with them will be in this map.") public Stream> getClientSessionStats() { auth.realm().requireViewRealm(); @@ -657,6 +697,8 @@ public class RealmAdminResource { @NoCache @Path("events/config") @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation( summary = "Get the events provider configuration Returns JSON object with events provider configuration") public RealmEventsConfigRepresentation getRealmEventsConfig() { auth.realm().requireViewEvents(); @@ -681,6 +723,8 @@ public class RealmAdminResource { @PUT @Path("events/config") @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation( description = "Update the events provider Change the events provider and/or its configuration") public void updateRealmEventsConfig(final RealmEventsConfigRepresentation rep) { auth.realm().requireManageEvents(); @@ -712,10 +756,16 @@ public class RealmAdminResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public Stream getEvents(@QueryParam("type") List types, @QueryParam("client") String client, - @QueryParam("user") String user, @QueryParam("dateFrom") String dateFrom, @QueryParam("dateTo") String dateTo, - @QueryParam("ipAddress") String ipAddress, @QueryParam("first") Integer firstResult, - @QueryParam("max") Integer maxResults) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation( summary = "Get events Returns all events, or filters them based on URL query parameters listed here") + public Stream getEvents(@Parameter(description = "The types of events to return") @QueryParam("type") List types, + @Parameter(description = "App or oauth client name") @QueryParam("client") String client, + @Parameter(description = "User id") @QueryParam("user") String user, + @Parameter(description = "From date") @QueryParam("dateFrom") String dateFrom, + @Parameter(description = "To date") @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) { auth.realm().requireViewEvents(); EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class); @@ -795,11 +845,13 @@ public class RealmAdminResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation( summary = "Get admin events Returns all admin events, or filters events based on URL query parameters listed here") public Stream getEvents(@QueryParam("operationTypes") List operationTypes, @QueryParam("authRealm") String authRealm, @QueryParam("authClient") String authClient, - @QueryParam("authUser") String authUser, @QueryParam("authIpAddress") String authIpAddress, + @Parameter(description = "user id") @QueryParam("authUser") String authUser, @QueryParam("authIpAddress") String authIpAddress, @QueryParam("resourcePath") String resourcePath, @QueryParam("dateFrom") String dateFrom, @QueryParam("dateTo") String dateTo, @QueryParam("first") Integer firstResult, - @QueryParam("max") Integer maxResults, + @Parameter(description = "Maximum results size (defaults to 100)") @QueryParam("max") Integer maxResults, @QueryParam("resourceTypes") List resourceTypes) { auth.realm().requireViewEvents(); @@ -884,6 +936,8 @@ public class RealmAdminResource { */ @Path("events") @DELETE + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation( summary = "Delete all events") public void clearEvents() { auth.realm().requireManageEvents(); @@ -897,6 +951,8 @@ public class RealmAdminResource { */ @Path("admin-events") @DELETE + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation( summary = "Delete all admin events") public void clearAdminEvents() { auth.realm().requireManageEvents(); @@ -916,7 +972,9 @@ public class RealmAdminResource { @NoCache @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Deprecated - public Response testSMTPConnection(final @FormParam("config") String config) throws Exception { + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation( summary = "Test SMTP connection with current logged in user") + public Response testSMTPConnection(final @Parameter(description = "SMTP server configuration") @FormParam("config") String config) throws Exception { Map settings = readValue(config, new TypeReference>() { }); return testSMTPConnection(settings); @@ -926,6 +984,8 @@ public class RealmAdminResource { @POST @NoCache @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public Response testSMTPConnection(Map settings) throws Exception { try { UserModel user = auth.adminAuth().getUser(); @@ -959,6 +1019,8 @@ public class RealmAdminResource { @NoCache @Produces(MediaType.APPLICATION_JSON) @Path("default-groups") + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation( summary = "Get group hierarchy. Only name and ids are returned.") public Stream getDefaultGroups() { auth.realm().requireViewRealm(); @@ -967,6 +1029,8 @@ public class RealmAdminResource { @PUT @NoCache @Path("default-groups/{groupId}") + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public void addDefaultGroup(@PathParam("groupId") String groupId) { auth.realm().requireManageRealm(); @@ -982,6 +1046,8 @@ public class RealmAdminResource { @DELETE @NoCache @Path("default-groups/{groupId}") + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public void removeDefaultGroup(@PathParam("groupId") String groupId) { auth.realm().requireManageRealm(); @@ -1005,6 +1071,8 @@ public class RealmAdminResource { @Path("group-by-path/{path: .*}") @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public GroupRepresentation getGroupByPath(@PathParam("path") String path) { GroupModel found = KeycloakModelUtils.findGroupByPath(realm, path); if (found == null) { @@ -1023,6 +1091,8 @@ public class RealmAdminResource { @POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation( summary = "Partial import from a JSON file to an existing realm.") public Response partialImport(InputStream requestBody) { auth.realm().requireManageRealm(); try { @@ -1079,6 +1149,8 @@ public class RealmAdminResource { @Path("partial-export") @Produces(MediaType.APPLICATION_JSON) @POST + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation( summary = "Partial export of existing realm into a JSON file.") public Response partialExport(@QueryParam("exportGroupsAndRoles") Boolean exportGroupsAndRoles, @QueryParam("exportClients") Boolean exportClients) { auth.realm().requireViewRealm(); @@ -1124,6 +1196,8 @@ public class RealmAdminResource { @Path("credential-registrators") @NoCache @Produces(jakarta.ws.rs.core.MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public Stream getCredentialRegistrators(){ auth.realm().requireViewRealm(); return session.getContext().getRealm().getRequiredActionProvidersStream() diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmLocalizationResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmLocalizationResource.java index bf688f62500..8c2279e55fe 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmLocalizationResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmLocalizationResource.java @@ -19,10 +19,14 @@ package org.keycloak.services.resources.admin; import com.fasterxml.jackson.core.type.TypeReference; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.keycloak.http.FormPartValue; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import java.io.IOException; @@ -47,6 +51,7 @@ import jakarta.ws.rs.core.MultivaluedMap; import org.keycloak.util.JsonSerialization; import org.keycloak.utils.StringUtil; +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class RealmLocalizationResource { private final RealmModel realm; private final AdminPermissionEvaluator auth; @@ -62,6 +67,8 @@ public class RealmLocalizationResource { @Path("{locale}/{key}") @PUT @Consumes(MediaType.TEXT_PLAIN) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public void saveRealmLocalizationText(@PathParam("locale") String locale, @PathParam("key") String key, String text) { this.auth.realm().requireManageRealm(); @@ -81,6 +88,8 @@ public class RealmLocalizationResource { @POST @Path("{locale}") @Consumes(MediaType.MULTIPART_FORM_DATA) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation( summary = "Import localization from uploaded JSON file") public void createOrUpdateRealmLocalizationTextsFromFile(@PathParam("locale") String locale) { this.auth.realm().requireManageRealm(); @@ -101,6 +110,8 @@ public class RealmLocalizationResource { @POST @Path("{locale}") @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public void createOrUpdateRealmLocalizationTexts(@PathParam("locale") String locale, Map localizationTexts) { this.auth.realm().requireManageRealm(); @@ -109,6 +120,8 @@ public class RealmLocalizationResource { @Path("{locale}") @DELETE + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public void deleteRealmLocalizationTexts(@PathParam("locale") String locale) { this.auth.realm().requireManageRealm(); if(!realm.removeRealmLocalizationTexts(locale)) { @@ -118,6 +131,8 @@ public class RealmLocalizationResource { @Path("{locale}/{key}") @DELETE + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public void deleteRealmLocalizationText(@PathParam("locale") String locale, @PathParam("key") String key) { this.auth.realm().requireManageRealm(); if (!session.realms().deleteLocalizationText(realm, locale, key)) { @@ -127,6 +142,8 @@ public class RealmLocalizationResource { @GET @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public Stream getRealmLocalizationLocales() { auth.requireAnyAdminRole(); @@ -136,6 +153,8 @@ public class RealmLocalizationResource { @Path("{locale}") @GET @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public Map getRealmLocalizationTexts(@PathParam("locale") String locale, @Deprecated @QueryParam("useRealmDefaultLocaleFallback") Boolean useFallback) { auth.requireAnyAdminRole(); @@ -158,6 +177,8 @@ public class RealmLocalizationResource { @Path("{locale}/{key}") @GET @Produces(MediaType.TEXT_PLAIN) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation() public String getRealmLocalizationText(@PathParam("locale") String locale, @PathParam("key") String key) { auth.requireAnyAdminRole(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java index 18dba93b362..a1d5d9827bc 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java @@ -16,6 +16,10 @@ */ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.common.ClientConnection; @@ -34,6 +38,7 @@ import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.ErrorResponse; import org.keycloak.services.ForbiddenException; import org.keycloak.services.managers.RealmManager; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissions; import org.keycloak.storage.DatastoreProvider; @@ -66,6 +71,7 @@ import static org.keycloak.utils.StreamsUtil.throwIfEmpty; * @author Bill Burke * @version $Revision: 1 $ */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class RealmsAdminResource { protected static final Logger logger = Logger.getLogger(RealmsAdminResource.class); protected final AdminAuth auth; @@ -98,6 +104,8 @@ public class RealmsAdminResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation(summary = "Get accessible realms Returns a list of accessible realms. The list is filtered based on what realms the caller is allowed to view.") public Stream getRealms(@DefaultValue("false") @QueryParam("briefRepresentation") boolean briefRepresentation) { Stream realms = session.realms().getRealmsStream() .map(realm -> toRealmRep(realm, briefRepresentation)) @@ -124,6 +132,8 @@ public class RealmsAdminResource { */ @POST @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) + @Operation(summary = "Import a realm. Imports a realm from a full representation of that realm.", description = "Realm name must be unique.") public Response importRealm(InputStream requestBody) { AdminPermissions.realms(session, auth).requireCreateRealm(); @@ -177,7 +187,7 @@ public class RealmsAdminResource { * @return */ @Path("{realm}") - public RealmAdminResource getRealmAdmin(@PathParam("realm") final String name) { + public RealmAdminResource getRealmAdmin(@PathParam("realm") @Parameter(description = "realm name (not id!)") final String name) { RealmManager realmManager = new RealmManager(session); RealmModel realm = realmManager.getRealmByName(name); if (realm == null) throw new NotFoundException("Realm not found."); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResourcePreflight.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResourcePreflight.java new file mode 100644 index 00000000000..e1e068057f1 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResourcePreflight.java @@ -0,0 +1,47 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.services.resources.admin; + +import jakarta.ws.rs.OPTIONS; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; +import org.keycloak.http.HttpRequest; +import org.keycloak.models.KeycloakSession; +import org.keycloak.protocol.oidc.TokenManager; +import org.keycloak.services.resources.Cors; + +public class RealmsAdminResourcePreflight extends RealmsAdminResource { + + private HttpRequest request; + + public RealmsAdminResourcePreflight(KeycloakSession session, AdminAuth auth, TokenManager tokenManager) { + super(session, auth, tokenManager); + } + + public RealmsAdminResourcePreflight(KeycloakSession session, AdminAuth auth, TokenManager tokenManager, HttpRequest request) { + super(session, auth, tokenManager); + this.request = request; + } + + @Path("{any:.*}") + @OPTIONS + public Response preFlight() { + return Cors.add(request, Response.ok()).preflight().allowedMethods("GET", "PUT", "POST", "DELETE").auth().build(); + } + +} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleByIdResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleByIdResource.java index 000b90ea1a3..f120a4b1fb2 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RoleByIdResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleByIdResource.java @@ -16,6 +16,10 @@ */ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import jakarta.ws.rs.NotFoundException; @@ -30,6 +34,7 @@ import org.keycloak.representations.idm.ManagementPermissionReference; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.services.ErrorResponse; import org.keycloak.services.ErrorResponseException; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; import org.keycloak.services.resources.admin.permissions.AdminPermissions; @@ -55,6 +60,7 @@ import java.util.stream.Stream; * @author Bill Burke * @version $Revision: 1 $ */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class RoleByIdResource extends RoleResource { protected static final Logger logger = Logger.getLogger(RoleByIdResource.class); private final RealmModel realm; @@ -81,7 +87,9 @@ public class RoleByIdResource extends RoleResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public RoleRepresentation getRole(final @PathParam("role-id") String id) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES_BY_ID) + @Operation( summary = "Get a specific role's representation") + public RoleRepresentation getRole(final @Parameter(description = "id of role") @PathParam("role-id") String id) { RoleModel roleModel = getRoleModel(id); auth.roles().requireView(roleModel); @@ -104,7 +112,9 @@ public class RoleByIdResource extends RoleResource { @Path("{role-id}") @DELETE @NoCache - public void deleteRole(final @PathParam("role-id") String id) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES_BY_ID) + @Operation( summary = "Delete the role") + public void deleteRole(final @Parameter(description = "id of role") @PathParam("role-id") String id) { if (realm.getDefaultRole() == null) { logger.warnf("Default role for realm with id '%s' doesn't exist.", realm.getId()); } else if (realm.getDefaultRole().getId().equals(id)) { @@ -134,7 +144,9 @@ public class RoleByIdResource extends RoleResource { @Path("{role-id}") @PUT @Consumes(MediaType.APPLICATION_JSON) - public void updateRole(final @PathParam("role-id") String id, final RoleRepresentation rep) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES_BY_ID) + @Operation( summary = "Update the role") + public void updateRole(final @Parameter(description = "id of role") @PathParam("role-id") String id, final RoleRepresentation rep) { RoleModel role = getRoleModel(id); auth.roles().requireManage(role); updateRole(rep, role, realm, session); @@ -157,6 +169,8 @@ public class RoleByIdResource extends RoleResource { @Path("{role-id}/composites") @POST @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES_BY_ID) + @Operation( summary = "Make the role a composite role by associating some child roles") public void addComposites(final @PathParam("role-id") String id, List roles) { RoleModel role = getRoleModel(id); auth.roles().requireManage(role); @@ -175,6 +189,8 @@ public class RoleByIdResource extends RoleResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES_BY_ID) + @Operation( summary = "Get role's children Returns a set of role's children provided the role is a composite.") public Stream getRoleComposites(final @PathParam("role-id") String id, final @QueryParam("search") String search, final @QueryParam("first") Integer first, @@ -202,6 +218,8 @@ public class RoleByIdResource extends RoleResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES_BY_ID) + @Operation( summary = "Get realm-level roles that are in the role's composite") public Stream getRealmRoleComposites(final @PathParam("role-id") String id) { RoleModel role = getRoleModel(id); auth.roles().requireView(role); @@ -219,6 +237,8 @@ public class RoleByIdResource extends RoleResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES_BY_ID) + @Operation( summary = "Get client-level roles for the client that are in the role's composite") public Stream getClientRoleComposites(final @PathParam("role-id") String id, final @PathParam("clientUuid") String clientUuid) { @@ -240,14 +260,17 @@ public class RoleByIdResource extends RoleResource { @Path("{role-id}/composites") @DELETE @Consumes(MediaType.APPLICATION_JSON) - public void deleteComposites(final @PathParam("role-id") String id, List roles) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES_BY_ID) + @Operation( summary = "Remove a set of roles from the role's composite") + public void deleteComposites(final @Parameter(description = "Role id") @PathParam("role-id") String id, + @Parameter(description = "A set of roles to be removed") List roles) { RoleModel role = getRoleModel(id); auth.roles().requireManage(role); deleteComposites(adminEvent, session.getContext().getUri(), roles, role); } /** - * Return object stating whether role Authoirzation permissions have been initialized or not and a reference + * Return object stating whether role Authorization permissions have been initialized or not and a reference * * * @param id @@ -257,6 +280,8 @@ public class RoleByIdResource extends RoleResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES_BY_ID) + @Operation( summary = "Return object stating whether role Authorization permissions have been initialized or not and a reference") public ManagementPermissionReference getManagementPermissions(final @PathParam("role-id") String id) { RoleModel role = getRoleModel(id); auth.roles().requireView(role); @@ -277,7 +302,7 @@ public class RoleByIdResource extends RoleResource { } /** - * Return object stating whether role Authoirzation permissions have been initialized or not and a reference + * Return object stating whether role Authorization permissions have been initialized or not and a reference * * * @param id @@ -288,6 +313,8 @@ public class RoleByIdResource extends RoleResource { @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES_BY_ID) + @Operation( summary = "Return object stating whether role Authorization permissions have been initialized or not and a reference") public ManagementPermissionReference setManagementPermissionsEnabled(final @PathParam("role-id") String id, ManagementPermissionReference ref) { RoleModel role = getRoleModel(id); auth.roles().requireManage(role); 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 97cb3450f72..4be73ab5140 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 @@ -17,6 +17,10 @@ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.resteasy.annotations.cache.NoCache; import jakarta.ws.rs.NotFoundException; import org.keycloak.events.admin.OperationType; @@ -37,6 +41,7 @@ import org.keycloak.representations.idm.ManagementPermissionReference; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.ErrorResponse; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; import org.keycloak.services.resources.admin.permissions.AdminPermissions; @@ -70,6 +75,7 @@ import org.keycloak.services.ErrorResponseException; * @author Bill Burke * @version $Revision: 1 $ */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class RoleContainerResource extends RoleResource { private final RealmModel realm; protected AdminPermissionEvaluator auth; @@ -98,6 +104,8 @@ public class RoleContainerResource extends RoleResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES) + @Operation( summary = "Get all roles for the realm or client") public Stream getRoles(@QueryParam("search") @DefaultValue("") String search, @QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults, @@ -128,6 +136,8 @@ public class RoleContainerResource extends RoleResource { */ @POST @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES) + @Operation( summary = "Create a new role for the realm or client") public Response createRole(final RoleRepresentation rep) { auth.roles().requireManage(roleContainer); @@ -212,7 +222,9 @@ public class RoleContainerResource extends RoleResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public RoleRepresentation getRole(final @PathParam("role-name") String roleName) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES) + @Operation( summary = "Get a role by name") + public RoleRepresentation getRole(final @Parameter(description = "role's name (not id!)") @PathParam("role-name") String roleName) { auth.roles().requireView(roleContainer); RoleModel roleModel = roleContainer.getRole(roleName); @@ -231,7 +243,9 @@ public class RoleContainerResource extends RoleResource { @Path("{role-name}") @DELETE @NoCache - public void deleteRole(final @PathParam("role-name") String roleName) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES) + @Operation( summary = "Delete a role by name") + public void deleteRole(final @Parameter(description = "role's name (not id!)") @PathParam("role-name") String roleName) { auth.roles().requireManage(roleContainer); RoleModel role = roleContainer.getRole(roleName); if (role == null) { @@ -262,7 +276,9 @@ public class RoleContainerResource extends RoleResource { @Path("{role-name}") @PUT @Consumes(MediaType.APPLICATION_JSON) - public Response updateRole(final @PathParam("role-name") String roleName, final RoleRepresentation rep) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES) + @Operation( summary = "Update a role by name") + public Response updateRole(final @Parameter(description = "role's name (not id!)") @PathParam("role-name") String roleName, final RoleRepresentation rep) { auth.roles().requireManage(roleContainer); RoleModel role = roleContainer.getRole(roleName); if (role == null) { @@ -294,7 +310,9 @@ public class RoleContainerResource extends RoleResource { @Path("{role-name}/composites") @POST @Consumes(MediaType.APPLICATION_JSON) - public void addComposites(final @PathParam("role-name") String roleName, List roles) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES) + @Operation( summary = "Add a composite to the role") + public void addComposites(final @Parameter(description = "role's name (not id!)") @PathParam("role-name") String roleName, List roles) { auth.roles().requireManage(roleContainer); RoleModel role = roleContainer.getRole(roleName); if (role == null) { @@ -313,7 +331,9 @@ public class RoleContainerResource extends RoleResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public Stream getRoleComposites(final @PathParam("role-name") String roleName) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES) + @Operation( summary = "Get composites of the role") + public Stream getRoleComposites(final @Parameter(description = "role's name (not id!)") @PathParam("role-name") String roleName) { auth.roles().requireView(roleContainer); RoleModel role = roleContainer.getRole(roleName); if (role == null) { @@ -332,7 +352,9 @@ public class RoleContainerResource extends RoleResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public Stream getRealmRoleComposites(final @PathParam("role-name") String roleName) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES) + @Operation( summary = "Get realm-level roles of the role's composite") + public Stream getRealmRoleComposites(final @Parameter(description = "role's name (not id!)") @PathParam("role-name") String roleName) { auth.roles().requireView(roleContainer); RoleModel role = roleContainer.getRole(roleName); if (role == null) { @@ -352,7 +374,9 @@ public class RoleContainerResource extends RoleResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public Stream getClientRoleComposites(final @PathParam("role-name") String roleName, + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES) + @Operation( summary = "Get client-level roles for the client that are in the role's composite") + public Stream getClientRoleComposites(final @Parameter(description = "role's name (not id!)") @PathParam("role-name") String roleName, final @PathParam("clientUuid") String clientUuid) { auth.roles().requireView(roleContainer); RoleModel role = roleContainer.getRole(roleName); @@ -377,9 +401,11 @@ public class RoleContainerResource extends RoleResource { @Path("{role-name}/composites") @DELETE @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES) + @Operation( summary = "Remove roles from the role's composite") public void deleteComposites( - final @PathParam("role-name") String roleName, - List roles) { + final @Parameter(description = "role's name (not id!)") @PathParam("role-name") String roleName, + @Parameter(description = "roles to remove") List roles) { auth.roles().requireManage(roleContainer); RoleModel role = roleContainer.getRole(roleName); @@ -400,6 +426,8 @@ public class RoleContainerResource extends RoleResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES) + @Operation( summary = "Return object stating whether role Authorization permissions have been initialized or not and a reference") public ManagementPermissionReference getManagementPermissions(final @PathParam("role-name") String roleName) { auth.roles().requireView(roleContainer); RoleModel role = roleContainer.getRole(roleName); @@ -426,6 +454,8 @@ public class RoleContainerResource extends RoleResource { @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES) + @Operation( summary = "Return object stating whether role Authorization permissions have been initialized or not and a reference") public ManagementPermissionReference setManagementPermissionsEnabled(final @PathParam("role-name") String roleName, ManagementPermissionReference ref) { auth.roles().requireManage(roleContainer); RoleModel role = roleContainer.getRole(roleName); @@ -455,9 +485,11 @@ public class RoleContainerResource extends RoleResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache - public Stream getUsersInRole(final @PathParam("role-name") String roleName, - @QueryParam("first") Integer firstResult, - @QueryParam("max") Integer maxResults) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES) + @Operation( summary = "Returns a stream of users that have the specified role name.") + public Stream getUsersInRole(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) { auth.roles().requireView(roleContainer); firstResult = firstResult != null ? firstResult : 0; @@ -486,10 +518,12 @@ public class RoleContainerResource extends RoleResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache - public Stream getGroupsInRole(final @PathParam("role-name") String roleName, - @QueryParam("first") Integer firstResult, - @QueryParam("max") Integer maxResults, - @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES) + @Operation( summary = "Returns a stream of groups that have the specified role name") + 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) { auth.roles().requireView(roleContainer); firstResult = firstResult != null ? firstResult : 0; diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleMapperResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleMapperResource.java index d9bca8cc392..6d2c9ffa2a2 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/RoleMapperResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleMapperResource.java @@ -16,6 +16,10 @@ */ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; @@ -35,6 +39,7 @@ import org.keycloak.representations.idm.ClientMappingsRepresentation; import org.keycloak.representations.idm.MappingsRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.services.ErrorResponseException; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.storage.ReadOnlyException; @@ -68,6 +73,7 @@ import java.util.stream.Stream; * @author Miguel P. Nunes * @version $Revision: 1 $ */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class RoleMapperResource { protected static final Logger logger = Logger.getLogger(RoleMapperResource.class); @@ -114,6 +120,8 @@ public class RoleMapperResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLE_MAPPER) + @Operation( summary = "Get role mappings") public MappingsRepresentation getRoleMappings() { viewPermission.require(); @@ -156,6 +164,8 @@ public class RoleMapperResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLE_MAPPER) + @Operation( summary = "Get realm-level role mappings") public Stream getRealmRoleMappings() { viewPermission.require(); @@ -175,7 +185,9 @@ public class RoleMapperResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache - public Stream getCompositeRealmRoleMappings(@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLE_MAPPER) + @Operation( summary = "Get effective realm-level role mappings This will recurse all composite roles to get the result.") + public Stream getCompositeRealmRoleMappings(@Parameter(description = "if false, return roles with their attributes") @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) { viewPermission.require(); Function toBriefRepresentation = briefRepresentation ? @@ -194,6 +206,8 @@ public class RoleMapperResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLE_MAPPER) + @Operation( summary = "Get realm-level roles that can be mapped") public Stream getAvailableRealmRoleMappings() { viewPermission.require(); @@ -211,7 +225,9 @@ public class RoleMapperResource { @Path("realm") @POST @Consumes(MediaType.APPLICATION_JSON) - public void addRealmRoleMappings(List roles) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLE_MAPPER) + @Operation( summary = "Add realm-level role mappings to the user") + public void addRealmRoleMappings(@Parameter(description = "Roles to add") List roles) { managePermission.require(); logger.debugv("** addRealmRoleMappings: {0}", roles); @@ -241,6 +257,8 @@ public class RoleMapperResource { @Path("realm") @DELETE @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROLE_MAPPER) + @Operation( summary = "Delete realm-level role mappings") public void deleteRealmRoleMappings(List roles) { managePermission.require(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedClientResource.java index 7fe0f285d1a..91a4fe01d8c 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedClientResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedClientResource.java @@ -17,6 +17,10 @@ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.resteasy.annotations.cache.NoCache; import jakarta.ws.rs.NotFoundException; import org.keycloak.events.admin.OperationType; @@ -29,6 +33,7 @@ import org.keycloak.models.ScopeContainerModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import jakarta.ws.rs.Consumes; @@ -51,6 +56,7 @@ import java.util.stream.Stream; * @author Bill Burke * @version $Revision: 1 $ */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class ScopeMappedClientResource { protected RealmModel realm; protected AdminPermissionEvaluator auth; @@ -84,6 +90,8 @@ public class ScopeMappedClientResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.SCOPE_MAPPINGS) + @Operation(summary = "Get the roles associated with a client's scope Returns roles for the client.") public Stream getClientScopeMappings() { viewPermission.require(); @@ -102,6 +110,8 @@ public class ScopeMappedClientResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.SCOPE_MAPPINGS) + @Operation(summary = "The available client-level roles Returns the roles for the client that can be associated with the client's scope") public Stream getAvailableClientScopeMappings() { viewPermission.require(); @@ -124,7 +134,9 @@ public class ScopeMappedClientResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache - public Stream getCompositeClientScopeMappings(@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.SCOPE_MAPPINGS) + @Operation(summary = "Get effective client roles Returns the roles for the client that are associated with the client's scope.") + public Stream getCompositeClientScopeMappings(@Parameter(description = "if false, return roles with their attributes") @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) { viewPermission.require(); Function toBriefRepresentation = briefRepresentation ? @@ -141,6 +153,8 @@ public class ScopeMappedClientResource { */ @POST @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.SCOPE_MAPPINGS) + @Operation(summary = "Add client-level roles to the client's scope") public void addClientScopeMapping(List roles) { managePermission.require(); @@ -162,6 +176,8 @@ public class ScopeMappedClientResource { */ @DELETE @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.SCOPE_MAPPINGS) + @Operation(summary = "Remove client-level roles from the client's scope.") public void deleteClientScopeMapping(List roles) { managePermission.require(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedResource.java index 7dc8a8dec00..5d7e948ad9b 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ScopeMappedResource.java @@ -17,6 +17,10 @@ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.resteasy.annotations.cache.NoCache; import jakarta.ws.rs.NotFoundException; import org.keycloak.events.admin.OperationType; @@ -30,6 +34,7 @@ import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.representations.idm.ClientMappingsRepresentation; import org.keycloak.representations.idm.MappingsRepresentation; import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.util.ScopeMappedUtil; @@ -58,6 +63,7 @@ import java.util.stream.Stream; * @author Bill Burke * @version $Revision: 1 $ */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class ScopeMappedResource { protected RealmModel realm; protected AdminPermissionEvaluator auth; @@ -91,6 +97,8 @@ public class ScopeMappedResource { @Produces(MediaType.APPLICATION_JSON) @NoCache @Deprecated + @Tag(name= KeycloakOpenAPI.Admin.Tags.SCOPE_MAPPINGS) + @Operation(summary = "Get all scope mappings for the client", deprecated = true) public MappingsRepresentation getScopeMappings() { viewPermission.require(); @@ -127,6 +135,8 @@ public class ScopeMappedResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.SCOPE_MAPPINGS) + @Operation(summary = "Get realm-level roles associated with the client's scope") public Stream getRealmScopeMappings() { viewPermission.require(); @@ -147,6 +157,8 @@ public class ScopeMappedResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.SCOPE_MAPPINGS) + @Operation(summary = "Get realm-level roles that are available to attach to this client's scope") public Stream getAvailableRealmScopeMappings() { viewPermission.require(); @@ -175,7 +187,10 @@ public class ScopeMappedResource { @GET @Produces(MediaType.APPLICATION_JSON) @NoCache - public Stream getCompositeRealmScopeMappings(@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.SCOPE_MAPPINGS) + @Operation(summary = "Get effective realm-level roles associated with the client’s scope What this does is recurse any composite roles associated with the client’s scope and adds the roles to this lists.", + description = "The method is really to show a comprehensive total view of realm-level roles associated with the client.") + public Stream getCompositeRealmScopeMappings(@Parameter(description = "if false, return roles with their attributes") @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) { viewPermission.require(); if (scopeContainer == null) { @@ -197,6 +212,8 @@ public class ScopeMappedResource { @Path("realm") @POST @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.SCOPE_MAPPINGS) + @Operation(summary = "Add a set of realm-level roles to the client's scope") public void addRealmScopeMappings(List roles) { managePermission.require(); @@ -223,6 +240,8 @@ public class ScopeMappedResource { @Path("realm") @DELETE @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.SCOPE_MAPPINGS) + @Operation(summary = "Remove a set of realm-level roles from the client's scope") public void deleteRealmScopeMappings(List roles) { managePermission.require(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserProfileResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserProfileResource.java index ac92cf94abd..83e64e39b49 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserProfileResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserProfileResource.java @@ -16,6 +16,7 @@ */ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.PUT; @@ -23,16 +24,20 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.keycloak.component.ComponentValidationException; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.services.ErrorResponse; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.userprofile.UserProfileProvider; /** * @author Vlastimil Elias */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class UserProfileResource { protected final KeycloakSession session; @@ -48,6 +53,8 @@ public class UserProfileResource { @GET @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation() public String getConfiguration() { auth.requireAnyAdminRole(); return session.getProvider(UserProfileProvider.class).getConfiguration(); @@ -55,6 +62,8 @@ public class UserProfileResource { @PUT @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation() public Response update(String text) { auth.realm().requireManageRealm(); UserProfileProvider t = session.getProvider(UserProfileProvider.class); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java index 4286888313a..45db1641531 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java @@ -16,6 +16,10 @@ */ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.authentication.RequiredActionProvider; @@ -71,6 +75,7 @@ import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.BruteForceProtector; import org.keycloak.services.managers.UserConsentManager; import org.keycloak.services.managers.UserSessionManager; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.validation.Validation; @@ -125,6 +130,7 @@ import static org.keycloak.utils.LockObjectsForModification.lockUserSessionsForM * @author Bill Burke * @version $Revision: 1 $ */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class UserResource { private static final Logger logger = Logger.getLogger(UserResource.class); @@ -159,6 +165,8 @@ public class UserResource { */ @PUT @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( summary = "Update the user") public Response updateUser(final UserRepresentation rep) { auth.users().requireManage(user); @@ -292,6 +300,8 @@ public class UserResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( summary = "Get representation of the user") public UserRepresentation getUser() { auth.users().requireView(user); @@ -327,6 +337,8 @@ public class UserResource { @POST @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( summary = "Impersonate the user") public Map impersonate() { ProfileHelper.requireFeature(Profile.Feature.IMPERSONATION); @@ -385,6 +397,8 @@ public class UserResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( summary = "Get sessions associated with the user") public Stream getSessions() { auth.users().requireView(user); return session.sessions().getUserSessionsStream(realm, user).map(ModelToRepresentation::toRepresentation); @@ -399,6 +413,8 @@ public class UserResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( summary = "Get offline sessions associated with the user and client") public Stream getOfflineSessions(final @PathParam("clientUuid") String clientUuid) { auth.users().requireView(user); ClientModel client = realm.getClientById(clientUuid); @@ -419,6 +435,8 @@ public class UserResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( summary = "Get social logins associated with the user") public Stream getFederatedIdentity() { auth.users().requireView(user); return getFederatedIdentities(user); @@ -441,7 +459,9 @@ public class UserResource { @Path("federated-identity/{provider}") @POST @NoCache - public Response addFederatedIdentity(final @PathParam("provider") String provider, FederatedIdentityRepresentation rep) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( summary = "Add a social login provider to the user") + public Response addFederatedIdentity(final @Parameter(description = "Social login provider id") @PathParam("provider") String provider, FederatedIdentityRepresentation rep) { auth.users().requireManage(user); if (session.users().getFederatedIdentity(realm, user, provider) != null) { throw ErrorResponse.exists("User is already linked with provider"); @@ -461,7 +481,9 @@ public class UserResource { @Path("federated-identity/{provider}") @DELETE @NoCache - public void removeFederatedIdentity(final @PathParam("provider") String provider) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( summary = "Remove a social login provider from user") + public void removeFederatedIdentity(final @Parameter(description = "Social login provider id") @PathParam("provider") String provider) { auth.users().requireManage(user); if (!session.users().removeFederatedIdentity(realm, user, provider)) { throw new NotFoundException("Link not found"); @@ -478,6 +500,8 @@ public class UserResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( summary = "Get consents granted by the user") public Stream> getConsents() { auth.users().requireView(user); @@ -546,7 +570,9 @@ public class UserResource { @Path("consents/{client}") @DELETE @NoCache - public void revokeConsent(final @PathParam("client") String clientId) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( summary = "Revoke consent and offline tokens for particular client from user") + public void revokeConsent(final @Parameter(description = "Client id") @PathParam("client") String clientId) { auth.users().requireManage(user); ClientModel client = realm.getClientByClientId(clientId); @@ -569,6 +595,8 @@ public class UserResource { */ @Path("logout") @POST + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( summary = "Remove all user sessions associated with the user Also send notification to all clients that have an admin URL to invalidate the sessions for the particular user.") public void logout() { auth.users().requireManage(user); @@ -586,6 +614,8 @@ public class UserResource { */ @DELETE @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( summary = "Delete the user") public Response deleteUser() { auth.users().requireManage(user); @@ -613,6 +643,8 @@ public class UserResource { @Path("disable-credential-types") @PUT @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( summary = "Disable all credentials for a user of a specific type") public void disableCredentialType(List credentialTypes) { auth.users().requireManage(user); if (credentialTypes == null) return; @@ -630,7 +662,9 @@ public class UserResource { @Path("reset-password") @PUT @Consumes(MediaType.APPLICATION_JSON) - public void resetPassword(CredentialRepresentation cred) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( summary = "Set up a new password for the user.") + public void resetPassword(@Parameter(description = "The representation must contain a rawPassword with the plain-text password") CredentialRepresentation cred) { auth.users().requireManage(user); if (cred == null || cred.getValue() == null) { throw new BadRequestException("No password provided"); @@ -666,6 +700,8 @@ public class UserResource { @Path("credentials") @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation() public Stream credentials(){ auth.users().requireView(user); return user.credentialManager().getStoredCredentialsStream() @@ -684,6 +720,8 @@ public class UserResource { @Path("configured-user-storage-credential-types") @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( summary = "Return credential types, which are provided by the user storage where user is stored.", description = "Returned values can contain for example \"password\", \"otp\" etc. This will always return empty list for \"local\" users, which are not backed by any user storage") public Stream getConfiguredUserStorageCredentialTypes() { // changed to "requireView" as per issue #20783 auth.users().requireView(user); @@ -698,6 +736,8 @@ public class UserResource { @Path("credentials/{credentialId}") @DELETE @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( summary = "Remove a credential for a user") public void removeCredential(final @PathParam("credentialId") String credentialId) { auth.users().requireManage(user); CredentialModel credential = user.credentialManager().getStoredCredentialById(credentialId); @@ -716,6 +756,8 @@ public class UserResource { @PUT @Consumes(MediaType.TEXT_PLAIN) @Path("credentials/{credentialId}/userLabel") + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( summary = "Update a credential label for a user") public void setCredentialUserLabel(final @PathParam("credentialId") String credentialId, String userLabel) { auth.users().requireManage(user); CredentialModel credential = user.credentialManager().getStoredCredentialById(credentialId); @@ -733,7 +775,9 @@ public class UserResource { */ @Path("credentials/{credentialId}/moveToFirst") @POST - public void moveCredentialToFirst(final @PathParam("credentialId") String credentialId){ + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( summary = "Move a credential to a first position in the credentials list of the user") + public void moveCredentialToFirst(final @Parameter(description = "The credential to move") @PathParam("credentialId") String credentialId){ moveCredentialAfter(credentialId, null); } @@ -744,7 +788,10 @@ public class UserResource { */ @Path("credentials/{credentialId}/moveAfter/{newPreviousCredentialId}") @POST - public void moveCredentialAfter(final @PathParam("credentialId") String credentialId, final @PathParam("newPreviousCredentialId") String newPreviousCredentialId){ + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( summary = "Move a credential to a position behind another credential") + public void moveCredentialAfter(final @Parameter(description = "The credential to move") @PathParam("credentialId") String credentialId, + final @Parameter(description = "The credential that will be the previous element in the list. If set to null, the moved credential will be the first element in the list.") @PathParam("newPreviousCredentialId") String newPreviousCredentialId){ auth.users().requireManage(user); CredentialModel credential = user.credentialManager().getStoredCredentialById(credentialId); if (credential == null) { @@ -771,8 +818,13 @@ public class UserResource { @Path("reset-password-email") @PUT @Consumes(MediaType.APPLICATION_JSON) - public Response resetPasswordEmail(@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri, - @QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( + summary = "Send an email to the user with a link they can click to reset their password.", + description = "The redirectUri and clientId parameters are optional. The default for the redirect is the account client. This endpoint has been deprecated. Please use the execute-actions-email passing a list with UPDATE_PASSWORD within it.", + deprecated = true) + public Response resetPasswordEmail(@Parameter(description = "redirect uri") @QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri, + @Parameter(description = "client id") @QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId) { List actions = new LinkedList<>(); actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name()); return executeActionsEmail(redirectUri, clientId, null, actions); @@ -796,10 +848,15 @@ public class UserResource { @Path("execute-actions-email") @PUT @Consumes(MediaType.APPLICATION_JSON) - public Response executeActionsEmail(@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri, - @QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId, - @QueryParam("lifespan") Integer lifespan, - List actions) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( + summary = "Send an email to the user with a link they can click to execute particular actions.", + description = "An email contains a link the user can click to perform a set of required actions. The redirectUri and clientId parameters are optional. If no redirect is given, then there will be no link back to click after actions have completed. Redirect uri must be a valid uri for the particular clientId." + ) + public Response executeActionsEmail(@Parameter(description = "Redirect uri") @QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri, + @Parameter(description = "Client id") @QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId, + @Parameter(description = "Number of seconds after which the generated token expires") @QueryParam("lifespan") Integer lifespan, + @Parameter(description = "Required actions the user needs to complete") List actions) { auth.users().requireManage(user); if (user.getEmail() == null) { @@ -883,7 +940,14 @@ public class UserResource { @Path("send-verify-email") @PUT @Consumes(MediaType.APPLICATION_JSON) - public Response sendVerifyEmail(@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri, @QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( + summary = "Send an email-verification email to the user An email contains a link the user can click to verify their email address.", + description = "The redirectUri and clientId parameters are optional. The default for the redirect is the account client." + ) + public Response sendVerifyEmail( + @Parameter(description = "Redirect uri") @QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri, + @Parameter(description = "Client id") @QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId) { List actions = new LinkedList<>(); actions.add(UserModel.RequiredAction.VERIFY_EMAIL.name()); return executeActionsEmail(redirectUri, clientId, null, actions); @@ -893,6 +957,8 @@ public class UserResource { @Path("groups") @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation() public Stream groupMembership(@QueryParam("search") String search, @QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults, @@ -910,6 +976,8 @@ public class UserResource { @NoCache @Path("groups/count") @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation() public Map getGroupMembershipCount(@QueryParam("search") String search) { auth.users().requireView(user); Long results; @@ -927,6 +995,8 @@ public class UserResource { @DELETE @Path("groups/{groupId}") @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation() public void removeMembership(@PathParam("groupId") String groupId) { auth.users().requireManageGroupMembership(user); @@ -951,6 +1021,8 @@ public class UserResource { @PUT @Path("groups/{groupId}") @NoCache + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation() public void joinGroup(@PathParam("groupId") String groupId) { auth.users().requireManageGroupMembership(user); GroupModel group = session.groups().getGroupById(realm, groupId); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index 0b634865482..a2f1b90aa6e 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -16,6 +16,10 @@ */ package org.keycloak.services.resources.admin; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.common.ClientConnection; @@ -37,6 +41,7 @@ import org.keycloak.policy.PasswordPolicyNotMetException; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.ErrorResponse; import org.keycloak.services.ForbiddenException; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.UserPermissionEvaluator; import org.keycloak.userprofile.UserProfile; @@ -74,6 +79,7 @@ import static org.keycloak.userprofile.UserProfileContext.USER_API; * @author Bill Burke * @version $Revision: 1 $ */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "") public class UsersResource { private static final Logger logger = Logger.getLogger(UsersResource.class); @@ -110,6 +116,8 @@ public class UsersResource { */ @POST @Consumes(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( summary = "Create a new user Username must be unique.") public Response createUser(final UserRepresentation rep) { // first check if user has manage rights try { @@ -252,20 +260,23 @@ public class UsersResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public Stream getUsers(@QueryParam("search") String search, - @QueryParam("lastName") String last, - @QueryParam("firstName") String first, - @QueryParam("email") String email, - @QueryParam("username") String username, - @QueryParam("emailVerified") Boolean emailVerified, - @QueryParam("idpAlias") String idpAlias, - @QueryParam("idpUserId") String idpUserId, - @QueryParam("first") Integer firstResult, - @QueryParam("max") Integer maxResults, - @QueryParam("enabled") Boolean enabled, - @QueryParam("briefRepresentation") Boolean briefRepresentation, - @QueryParam("exact") Boolean exact, - @QueryParam("q") String searchQuery) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( summary = "Get users Returns a stream of users, filtered according to query parameters.") + public Stream getUsers( + @Parameter(description = "A String contained in username, first or last name, or email") @QueryParam("search") String search, + @Parameter(description = "A String contained in lastName, or the complete lastName, if param \"exact\" is true") @QueryParam("lastName") String last, + @Parameter(description = "A String contained in firstName, or the complete firstName, if param \"exact\" is true") @QueryParam("firstName") String first, + @Parameter(description = "A String contained in email, or the complete email, if param \"exact\" is true") @QueryParam("email") String email, + @Parameter(description = "A String contained in username, or the complete username, if param \"exact\" is true") @QueryParam("username") String username, + @Parameter(description = "whether the email has been verified") @QueryParam("emailVerified") Boolean emailVerified, + @Parameter(description = "The alias of an Identity Provider linked to the user") @QueryParam("idpAlias") String idpAlias, + @Parameter(description = "The userId at an Identity Provider linked to the user") @QueryParam("idpUserId") String idpUserId, + @Parameter(description = "Pagination offset") @QueryParam("first") Integer firstResult, + @Parameter(description = "Maximum results size (defaults to 100)") @QueryParam("max") Integer maxResults, + @Parameter(description = "Boolean representing if user is enabled or not") @QueryParam("enabled") Boolean enabled, + @Parameter(description = "Boolean which defines whether brief representations are returned (default: false)") @QueryParam("briefRepresentation") Boolean briefRepresentation, + @Parameter(description = "Boolean which defines whether the params \"last\", \"first\", \"email\" and \"username\" must match exactly") @QueryParam("exact") Boolean exact, + @Parameter(description = "A query to search for custom attributes, in the format 'key1:value2 key2:value2'") @QueryParam("q") String searchQuery) { UserPermissionEvaluator userPermissionEvaluator = auth.users(); userPermissionEvaluator.requireQuery(); @@ -364,14 +375,22 @@ public class UsersResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public Integer getUsersCount(@QueryParam("search") String search, - @QueryParam("lastName") String last, - @QueryParam("firstName") String first, - @QueryParam("email") String email, - @QueryParam("emailVerified") Boolean emailVerified, - @QueryParam("username") String username, - @QueryParam("enabled") Boolean enabled, - @QueryParam("q") String searchQuery) { + @Tag(name = KeycloakOpenAPI.Admin.Tags.USERS) + @Operation( + summary = "Returns the number of users that match the given criteria.", + description = "It can be called in three different ways. " + + "1. Don’t specify any criteria and pass {@code null}. The number of all users within that realm will be returned.

" + + "2. If {@code search} is specified other criteria such as {@code last} will be ignored even though you set them. The {@code search} string will be matched against the first and last name, the username and the email of a user.

" + + "3. If {@code search} is unspecified but any of {@code last}, {@code first}, {@code email} or {@code username} those criteria are matched against their respective fields on a user entity. Combined with a logical and.") + public Integer getUsersCount( + @Parameter(description = "arbitrary search string for all the fields below") @QueryParam("search") String search, + @Parameter(description = "last name filter") @QueryParam("lastName") String last, + @Parameter(description = "first name filter") @QueryParam("firstName") String first, + @Parameter(description = "email filter") @QueryParam("email") String email, + @QueryParam("emailVerified") Boolean emailVerified, + @Parameter(description = "username filter") @QueryParam("username") String username, + @Parameter(description = "Boolean representing if user is enabled or not") @QueryParam("enabled") Boolean enabled, + @QueryParam("q") String searchQuery) { UserPermissionEvaluator userPermissionEvaluator = auth.users(); userPermissionEvaluator.requireQuery(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java index c01cb5b058c..9278e886172 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java @@ -17,6 +17,9 @@ package org.keycloak.services.resources.admin.info; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.extensions.Extension; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderFactory; @@ -53,6 +56,7 @@ import org.keycloak.representations.info.ServerInfoRepresentation; import org.keycloak.representations.info.SpiInfoRepresentation; import org.keycloak.representations.info.SystemInfoRepresentation; import org.keycloak.representations.info.ThemeInfoRepresentation; +import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.theme.Theme; import jakarta.ws.rs.GET; @@ -74,6 +78,7 @@ import java.util.stream.Stream; /** * @author Stian Thorgersen */ +@Extension(name = KeycloakOpenAPI.Profiles.ADMIN , value = "") public class ServerInfoAdminResource { private static final Map> ENUMS = createEnumsMap(EventType.class, OperationType.class, ResourceType.class); @@ -92,6 +97,8 @@ public class ServerInfoAdminResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) + @Tag(name = KeycloakOpenAPI.Admin.Tags.ROOT) + @Operation( summary = "Get themes, social providers, auth providers, and event listeners available on this server") public ServerInfoRepresentation getInfo() { ServerInfoRepresentation info = new ServerInfoRepresentation(); info.setSystemInfo(SystemInfoRepresentation.create(session.getKeycloakSessionFactory().getServerStartupTimestamp()));