Refactor Admin REST API Documentation to use OpenAPI annotations.

Removes dependencies on swagger-doclet
Adds dependencies on microprofile-openapi-api
Plugins for smallrye-open-api-maven-plugin, openapi-generator-maven-plugin

Customized ascii doc template for openapi-generator-maven-plugin, to give similar feel to previous documentation.

OpenAPI annotations added to Admin REST API resources.

Closes keycloak/keycloak#20433
This commit is contained in:
Joshua Sorah 2023-05-30 13:41:25 -04:00 committed by Marek Posolda
parent 73076a37f9
commit f695eeaa44
47 changed files with 1286 additions and 213 deletions

19
pom.xml
View file

@ -188,6 +188,7 @@
https://issues.redhat.com/browse/KEYCLOAK-17585?focusedCommentId=16002705&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-16002705
-->
<microprofile-metrics-api.version>3.0.1</microprofile-metrics-api.version>
<microprofile-openapi-api.version>3.1</microprofile-openapi-api.version>
<testcontainers.version>1.17.5</testcontainers.version>
<!-- Maven Plugins -->
@ -202,6 +203,8 @@
<docker.maven.plugin.version>0.40.3</docker.maven.plugin.version>
<verifier.plugin.version>1.1</verifier.plugin.version>
<shade.plugin.version>3.4.1</shade.plugin.version>
<smallrye.openapi.generator.plugin.version>3.1.2</smallrye.openapi.generator.plugin.version>
<openapi.generator.plugin.version>6.3.0</openapi.generator.plugin.version>
<!-- Surefire Settings -->
<surefire.memory.Xms>512m</surefire.memory.Xms>
@ -1825,6 +1828,12 @@
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.microprofile.openapi</groupId>
<artifactId>microprofile-openapi-api</artifactId>
<version>${microprofile-openapi-api.version}</version>
</dependency>
<!-- used in server-dist build while provisioning the distribution -->
<dependency>
<groupId>org.keycloak</groupId>
@ -2007,6 +2016,16 @@
<artifactId>wildfly-server-provisioning-maven-plugin</artifactId>
<version>${wildfly.build-tools.version}</version>
</plugin>
<plugin>
<groupId>io.smallrye</groupId>
<artifactId>smallrye-open-api-maven-plugin</artifactId>
<version>${smallrye.openapi.generator.plugin.version}</version>
</plugin>
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>${openapi.generator.plugin.version}</version>
</plugin>
<plugin>
<groupId>org.wildfly.galleon-plugins</groupId>
<artifactId>wildfly-galleon-maven-plugin</artifactId>

View file

@ -45,7 +45,6 @@
<dependency>
<groupId>org.eclipse.microprofile.openapi</groupId>
<artifactId>microprofile-openapi-api</artifactId>
<version>3.1</version>
</dependency>
</dependencies>
@ -54,7 +53,6 @@
<plugin>
<artifactId>smallrye-open-api-maven-plugin</artifactId>
<groupId>io.smallrye</groupId>
<version>3.1.2</version>
<configuration>
<scanPackages>org.keycloak.admin.ui.rest</scanPackages>
</configuration>

View file

@ -30,10 +30,6 @@
<name>Keycloak REST Services</name>
<description />
<properties>
<version.swagger.doclet>1.1.2</version.swagger.doclet>
</properties>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
@ -201,6 +197,10 @@
<groupId>com.github.ua-parser</groupId>
<artifactId>uap-java</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.microprofile.openapi</groupId>
<artifactId>microprofile-openapi-api</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
@ -244,83 +244,57 @@
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<!-- Reverted version as the new version of maven-javadoc-plugin is not compatible with swagger doclet -->
<version>2.10.3</version>
<groupId>io.smallrye</groupId>
<artifactId>smallrye-open-api-maven-plugin</artifactId>
<configuration>
<scanProfiles>admin</scanProfiles>
<outputDirectory>${project.build.directory}/apidocs-rest/swagger/apidocs</outputDirectory>
<infoTitle>Keycloak Admin REST API</infoTitle>
<infoDescription>This is a REST API reference for the Keycloak Admin REST API.</infoDescription>
</configuration>
<executions>
<execution>
<id>generate-service-docs</id>
<phase>generate-resources</phase>
<configuration>
<doclet>com.carma.swagger.doclet.ServiceDoclet</doclet>
<docletArtifact>
<groupId>com.carma</groupId>
<artifactId>swagger-doclet</artifactId>
<version>${version.swagger.doclet}</version>
</docletArtifact>
<subpackages>org.keycloak.services.resources.admin:org.keycloak.protocol.oidc</subpackages>
<detectOfflineLinks>false</detectOfflineLinks>
<offlineLinks>
<offlineLink>
<url>../javadocs</url>
<location>${project.basedir}/../target/site/apidocs</location>
</offlineLink>
</offlineLinks>
<reportOutputDirectory>${project.basedir}/target/apidocs-rest/swagger</reportOutputDirectory>
<useStandardDocletOptions>false</useStandardDocletOptions>
<additionalparam> -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</additionalparam>
</configuration>
<goals>
<goal>javadoc</goal>
<goal>generate-schema</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>io.github.swagger2markup</groupId>
<artifactId>swagger2markup-maven-plugin</artifactId>
<version>1.1.0</version>
<!-- Replace the dependencies that aren't in Maven Central -->
<dependencies>
<dependency>
<groupId>ca.szc.thirdparty.nl.jworks.markdown_to_asciidoc</groupId>
<artifactId>markdown_to_asciidoc</artifactId>
<!-- Keep in sync with markup-document-builder's dependency -->
<version>1.0</version>
</dependency>
<dependency>
<groupId>io.github.swagger2markup</groupId>
<artifactId>swagger2markup</artifactId>
<!-- Keep in sync with swagger2markup-maven-plugin's dependency -->
<version>1.1.0</version>
<exclusions>
<exclusion>
<groupId>nl.jworks.markdown_to_asciidoc</groupId>
<artifactId>markdown_to_asciidoc</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<executions>
<execution>
<id>gen-asciidoc</id>
<phase>process-resources</phase>
<id>generate-ascii-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>convertSwagger2markup</goal>
<goal>generate</goal>
</goals>
<configuration>
<swaggerInput>${project.build.directory}/apidocs-rest/swagger/apidocs/service.json</swaggerInput>
<outputDir>${project.build.directory}/apidocs-rest/asciidoc/</outputDir>
<config>
<swagger2markup.markupLanguage>ASCIIDOC</swagger2markup.markupLanguage>
<swagger2markup.pathsGroupedBy>TAGS</swagger2markup.pathsGroupedBy>
</config>
<globalProperties>
<apiDocs>true</apiDocs>
<apiModels>true</apiModels>
<apiTests>false</apiTests>
<modelTests>false</modelTests>
<verbose>false</verbose>
</globalProperties>
<inputSpec>${project.build.directory}/apidocs-rest/swagger/apidocs/openapi.yaml</inputSpec>
<output>${project.basedir}/target/docs/asciidoc/</output>
<generatorName>asciidoc</generatorName>
<generateApiDocumentation>true</generateApiDocumentation>
<generateModelDocumentation>true</generateModelDocumentation>
<generateSupportingFiles>true</generateSupportingFiles>
<groupId>${project.groupId}</groupId>
<artifactId>${project.artifactId}</artifactId>
<templateDirectory>src/docs/openapi-generator-templates/keycloak-admin-api</templateDirectory>
<configOptions>
<useIntroduction>true</useIntroduction>
<delegatePattern>false</delegatePattern>
<useMethodAndPath>true</useMethodAndPath>
<prependFormOrBodyParameters>false</prependFormOrBodyParameters>
<skipExamples>true</skipExamples>
</configOptions>
<skipValidateSpec>true</skipValidateSpec>
</configuration>
</execution>
</executions>

View file

@ -1,3 +0,0 @@
include::overview.adoc[]
include::{generated}/paths.adoc[]
include::{generated}/definitions.adoc[]

View file

@ -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`

View file

@ -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}}

View file

@ -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}}

View file

@ -0,0 +1,5 @@
| *{{baseName}}* +
{{^required}}_optional_{{/required}}{{#required}}_required_{{/required}}
| {{description}} {{#baseType}}<<{{.}}>>{{/baseType}}
| {{defaultValue}}
| {{{pattern}}}

View file

@ -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}}

View file

@ -0,0 +1 @@
// openapi generator built documentation, see https://github.com/OpenAPITools/openapi-generator

View file

@ -1,4 +1,8 @@
{
"openapi": "3.0.1",
"info": {
"title": "Keycloak Admin REST API",
"description": "This is a REST API reference for the Keycloak Admin"
"description": "This is a REST API reference for the Keycloak Admin REST API",
"version": 1
}
}

View file

@ -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() { }
}
}
}

View file

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

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<String, Object> 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();

View file

@ -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<Map<String, Object>> 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<Map<String, Object>> 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<Map<String, Object>> 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<Map<String, Object>> 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<AuthenticationFlowRepresentation> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<Map<String, String>> getUnregisteredRequiredActions() {
auth.realm().requireViewRealm();
@ -970,7 +1021,9 @@ public class AuthenticationManagementResource {
@POST
@Consumes(MediaType.APPLICATION_JSON)
@NoCache
public void registerRequiredAction(Map<String, String> 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<String, String> 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<RequiredActionProviderRepresentation> 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<String, List<ConfigPropertyRepresentation>> 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());

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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);

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@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<ClientInitialAccessPresentation> 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();

View file

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

View file

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

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@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<ComponentTypeRepresentation> getProviders() {
return session.getKeycloakSessionFactory().getProviderFactoriesStream(ClientRegistrationPolicy.class)
.map((ProviderFactory factory) -> {

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<ClientScopeRepresentation> 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<ClientScopeRepresentation> 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<String, Long> getApplicationSessionCount() {
auth.clients().requireView(client);
@ -509,7 +547,9 @@ public class ClientResource {
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public Stream<UserSessionRepresentation> 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<UserSessionRepresentation> 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<String, Long> getOfflineSessionCount() {
auth.clients().requireView(client);
@ -554,7 +596,9 @@ public class ClientResource {
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public Stream<UserSessionRepresentation> 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<UserSessionRepresentation> 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 its not needed to call this directly as adapter should handle by sending registration request to Keycloak")
public void registerNode(Map<String, String> 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);

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<RoleRepresentation> getClientRoleMappings() {
viewPermission.require();
@ -111,7 +119,9 @@ public class ClientRoleMappingsResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public Stream<RoleRepresentation> 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<RoleRepresentation> getCompositeClientRoleMappings(@Parameter(description = "if false, return roles with their attributes") @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) {
viewPermission.require();
Stream<RoleModel> 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<RoleRepresentation> 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<RoleRepresentation> roles) {
managePermission.require();
@ -173,6 +187,8 @@ public class ClientRoleMappingsResource {
*/
@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<RoleRepresentation> roles) {
managePermission.require();

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@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<ProtocolMapperEvaluationRepresentation> 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<String, Object> 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);

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@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<RoleRepresentation> 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<RoleRepresentation> getNotGrantedScopeMappings() {
Set<RoleModel> grantedRoles = getGrantedRoles().collect(Collectors.toSet());

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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);

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<ClientScopeRepresentation> 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 Scopes name must be unique!")
public Response createClientScope(ClientScopeRepresentation rep) {
auth.clients().requireManageClientScopes();
ClientScopeResource.validateClientScopeName(rep.getName());

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<ClientRepresentation> 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 cant 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 dont prevent callers from retrieving this list.")
public Stream<ClientRepresentation> 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 Clients client_id must be unique!")
public Response createClient(final ClientRepresentation rep) {
auth.clients().requireManage();

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<ComponentRepresentation> 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<ComponentTypeRepresentation> getSubcomponentConfig(@PathParam("id") String parentId, @QueryParam("type") String subtype) {
auth.realm().requireViewRealm();
ComponentModel parent = realm.getComponent(parentId);

View file

@ -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 doesnt exist.")
public Response addChild(GroupRepresentation rep) {
this.auth.groups().requireManage(group);
@ -267,8 +282,11 @@ public class GroupResource {
@NoCache
@Path("members")
@Produces(MediaType.APPLICATION_JSON)
public Stream<UserRepresentation> getMembers(@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults,
@Tag(name = KeycloakOpenAPI.Admin.Tags.GROUPS)
@Operation( summary = "Get users Returns a stream of users, filtered according to query parameters")
public Stream<UserRepresentation> 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);
@ -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);

View file

@ -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<GroupRepresentation> 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<String, Long> 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 doesnt exist.")
public Response addTopLevelGroup(GroupRepresentation rep) {
auth.groups().requireManage();

View file

@ -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<String, IdentityProviderMapperTypeRepresentation> 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<IdentityProviderMapperRepresentation> 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);

View file

@ -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<String, String> importFrom() throws IOException {
this.auth.realm().requireManageIdentityProviders();
MultivaluedMap<String, FormPartValue> 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<String, String> importFrom(Map<String, Object> data) throws IOException {
@Tag(name = KeycloakOpenAPI.Admin.Tags.IDENTITY_PROVIDERS)
@Operation( summary = "Import identity provider from JSON body")
public Map<String, String> importFrom(@Parameter(description = "JSON body") Map<String, Object> 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<IdentityProviderRepresentation> 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());

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@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();

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<ProtocolMapperRepresentation> 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<ProtocolMapperRepresentation> 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<ProtocolMapperRepresentation> 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);

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<ClientScopeRepresentation> 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<ClientScopeRepresentation> 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<Map<String, String>> 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<EventRepresentation> getEvents(@QueryParam("type") List<String> 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<EventRepresentation> getEvents(@Parameter(description = "The types of events to return") @QueryParam("type") List<String> 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<AdminEventRepresentation> getEvents(@QueryParam("operationTypes") List<String> 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<String> 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<String, String> settings = readValue(config, new TypeReference<Map<String, String>>() {
});
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<String, String> 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<GroupRepresentation> 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<String> getCredentialRegistrators(){
auth.realm().requireViewRealm();
return session.getContext().getRealm().getRequiredActionProvidersStream()

View file

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

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<RealmRepresentation> getRealms(@DefaultValue("false") @QueryParam("briefRepresentation") boolean briefRepresentation) {
Stream<RealmRepresentation> 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.");

View file

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

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<RoleRepresentation> 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<RoleRepresentation> 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<RoleRepresentation> 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<RoleRepresentation> 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<RoleRepresentation> 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<RoleRepresentation> 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);

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<RoleRepresentation> 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<RoleRepresentation> 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<RoleRepresentation> 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<RoleRepresentation> getRoleComposites(final @PathParam("role-name") String roleName) {
@Tag(name = KeycloakOpenAPI.Admin.Tags.ROLES)
@Operation( summary = "Get composites of the role")
public Stream<RoleRepresentation> 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<RoleRepresentation> 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<RoleRepresentation> 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<RoleRepresentation> 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<RoleRepresentation> 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<RoleRepresentation> roles) {
final @Parameter(description = "role's name (not id!)") @PathParam("role-name") String roleName,
@Parameter(description = "roles to remove") List<RoleRepresentation> 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<UserRepresentation> 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<UserRepresentation> 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<GroupRepresentation> 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<GroupRepresentation> 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;

View file

@ -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 <a href="mailto:mpaulosnunes@gmail.com">Miguel P. Nunes</a>
* @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<RoleRepresentation> getRealmRoleMappings() {
viewPermission.require();
@ -175,7 +185,9 @@ public class RoleMapperResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public Stream<RoleRepresentation> 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<RoleRepresentation> getCompositeRealmRoleMappings(@Parameter(description = "if false, return roles with their attributes") @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) {
viewPermission.require();
Function<RoleModel, RoleRepresentation> 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<RoleRepresentation> getAvailableRealmRoleMappings() {
viewPermission.require();
@ -211,7 +225,9 @@ public class RoleMapperResource {
@Path("realm")
@POST
@Consumes(MediaType.APPLICATION_JSON)
public void addRealmRoleMappings(List<RoleRepresentation> 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<RoleRepresentation> 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<RoleRepresentation> roles) {
managePermission.require();

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<RoleRepresentation> 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<RoleRepresentation> getAvailableClientScopeMappings() {
viewPermission.require();
@ -124,7 +134,9 @@ public class ScopeMappedClientResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public Stream<RoleRepresentation> 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<RoleRepresentation> getCompositeClientScopeMappings(@Parameter(description = "if false, return roles with their attributes") @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) {
viewPermission.require();
Function<RoleModel, RoleRepresentation> 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<RoleRepresentation> 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<RoleRepresentation> roles) {
managePermission.require();

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<RoleRepresentation> 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<RoleRepresentation> getAvailableRealmScopeMappings() {
viewPermission.require();
@ -175,7 +187,10 @@ public class ScopeMappedResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public Stream<RoleRepresentation> getCompositeRealmScopeMappings(@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) {
@Tag(name = KeycloakOpenAPI.Admin.Tags.SCOPE_MAPPINGS)
@Operation(summary = "Get effective realm-level roles associated with the clients scope What this does is recurse any composite roles associated with the clients 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<RoleRepresentation> 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<RoleRepresentation> 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<RoleRepresentation> roles) {
managePermission.require();

View file

@ -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 <velias@redhat.com>
*/
@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);

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<String, Object> 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<UserSessionRepresentation> 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<UserSessionRepresentation> 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<FederatedIdentityRepresentation> 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<Map<String, Object>> 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<String> 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<CredentialRepresentation> 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<String> 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<String> 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<String> 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<String> 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<String> 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<GroupRepresentation> 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<String, Long> 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);

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<UserRepresentation> 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<UserRepresentation> 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,13 +375,21 @@ 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,
@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. Dont specify any criteria and pass {@code null}. The number of all users within that realm will be returned. <p> " +
"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. <p> " +
"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,
@QueryParam("username") String username,
@QueryParam("enabled") Boolean enabled,
@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();

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@Extension(name = KeycloakOpenAPI.Profiles.ADMIN , value = "")
public class ServerInfoAdminResource {
private static final Map<String, List<String>> 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()));