From 18224eb2848b5db7ead272c9788cb4549c34e388 Mon Sep 17 00:00:00 2001 From: ch4mpy Date: Thu, 13 Jun 2024 16:49:03 -1000 Subject: [PATCH] Add development infra Replace Feign with spring-addons-starter-rest & RestClient Use MapStruct --- .env | 2 + .gitignore | 3 + angular-ui.app.config.ts | 12 + api/.gitignore | 1 + api/bff/pom.xml | 8 + .../java/com/c4soft/quiz/BffApplication.java | 6 +- .../java/com/c4soft/quiz/BffController.java | 47 +- ...itional-spring-configuration-metadata.json | 5 + api/bff/src/main/resources/application.yml | 47 +- api/keycloak-admin-api.openapi.json | 18358 ++++++++++++++++ api/pom.xml | 62 +- api/quiz-api/pom.xml | 66 +- .../com/c4soft/quiz/ExceptionHandlers.java | 104 + .../com/c4soft/quiz/PersistenceConfig.java | 2 +- .../com/c4soft/quiz/QuizApiApplication.java | 7 +- .../main/java/com/c4soft/quiz/RestConfig.java | 15 + .../java/com/c4soft/quiz/SecurityConfig.java | 25 +- .../java/com/c4soft/quiz/domain/Choice.java | 52 +- .../com/c4soft/quiz/domain/ChoiceService.java | 19 + .../domain/DraftAlreadyExistsException.java | 15 - .../quiz/domain/NotADraftException.java | 15 - .../java/com/c4soft/quiz/domain/Question.java | 162 +- .../c4soft/quiz/domain/QuestionService.java | 25 + .../java/com/c4soft/quiz/domain/Quiz.java | 221 +- .../quiz/domain/QuizAuthentication.java | 35 +- .../c4soft/quiz/domain/QuizRejectionDto.java | 5 +- .../c4soft/quiz/domain/QuizRepository.java | 47 - .../com/c4soft/quiz/domain/SkillTest.java | 90 +- .../quiz/domain/SkillTestRepository.java | 46 - .../DraftAlreadyExistsException.java | 10 + .../exception/InvalidQuizException.java | 9 + .../domain/exception/NotADraftException.java | 14 + .../QuizAlreadyHasAnAnswerException.java | 12 + .../domain/{ => jpa}/ChoiceRepository.java | 4 +- .../domain/{ => jpa}/QuestionRepository.java | 4 +- .../quiz/domain/jpa/QuizRepository.java | 51 + .../quiz/domain/jpa/SkillTestRepository.java | 49 + .../com/c4soft/quiz/feign/FeignConfig.java | 10 - .../quiz/feign/KeycloakAdminApiClient.java | 41 - .../com/c4soft/quiz/web/ChoiceController.java | 146 + .../c4soft/quiz/web/ExceptionHandlers.java | 100 - .../c4soft/quiz/web/QuestionController.java | 154 + .../com/c4soft/quiz/web/QuizController.java | 645 +- .../c4soft/quiz/web/SkillTestController.java | 369 +- .../com/c4soft/quiz/web/UsersController.java | 23 +- .../com/c4soft/quiz/web/dto/ChoiceDto.java | 7 +- .../c4soft/quiz/web/dto/ChoiceUpdateDto.java | 3 +- .../com/c4soft/quiz/web/dto/QuestionDto.java | 13 +- .../quiz/web/dto/QuestionUpdateDto.java | 9 +- .../java/com/c4soft/quiz/web/dto/QuizDto.java | 59 +- .../c4soft/quiz/web/dto/QuizUpdateDto.java | 27 +- .../com/c4soft/quiz/web/dto/SkillTestDto.java | 11 +- .../quiz/web/dto/SkillTestQuestionDto.java | 7 +- .../web/dto/SkillTestResultDetailsDto.java | 13 +- .../web/dto/SkillTestResultPreviewDto.java | 4 +- .../com/c4soft/quiz/web/dto/UserInfoDto.java | 11 +- .../quiz/web/dto/mapping/ChoiceMapper.java | 21 + .../quiz/web/dto/mapping/QuestionMapper.java | 24 + .../quiz/web/dto/mapping/QuizMapper.java | 32 + ...itional-spring-configuration-metadata.json | 10 + .../src/main/resources/application.yml | 31 +- .../EnableSpringDataWebSupportTestConf.java | 54 +- .../c4soft/quiz/QuizApiApplicationTest.java | 400 +- .../java/com/c4soft/quiz/QuizFixtures.java | 31 + .../java/com/c4soft/quiz/SecuredTest.java | 11 +- .../c4soft/quiz/web/QuizControllerTest.java | 132 +- api/quiz-api/src/test/resources/ch4mp.json | 3 +- .../{tonton-pirate.json => trainee.json} | 3 +- .../test/resources/unprivieldged-trainer.json | 11 + build.sh | 85 + compose-desktop-ch4mp.yml | 104 + compose.yml | 106 + delete-eclipse-project-files.sh | 3 + keycloak/.env | 1 + keycloak/compose.yml | 42 + keycloak/import/quiz-realm.json | 1941 ++ nginx-reverse-proxy/404.html | 10 + nginx-reverse-proxy/Dockerfile | 2 + nginx-reverse-proxy/nginx.conf | 67 + nginx.conf | 66 + quiz-realm.json | 2051 ++ 81 files changed, 24923 insertions(+), 1625 deletions(-) create mode 100644 .env create mode 100644 angular-ui.app.config.ts create mode 100644 api/.gitignore create mode 100644 api/keycloak-admin-api.openapi.json create mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/ExceptionHandlers.java create mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/RestConfig.java create mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/domain/ChoiceService.java delete mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/domain/DraftAlreadyExistsException.java delete mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/domain/NotADraftException.java create mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/domain/QuestionService.java delete mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/domain/QuizRepository.java delete mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/domain/SkillTestRepository.java create mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/domain/exception/DraftAlreadyExistsException.java create mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/domain/exception/InvalidQuizException.java create mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/domain/exception/NotADraftException.java create mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/domain/exception/QuizAlreadyHasAnAnswerException.java rename api/quiz-api/src/main/java/com/c4soft/quiz/domain/{ => jpa}/ChoiceRepository.java (61%) rename api/quiz-api/src/main/java/com/c4soft/quiz/domain/{ => jpa}/QuestionRepository.java (61%) create mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/domain/jpa/QuizRepository.java create mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/domain/jpa/SkillTestRepository.java delete mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/feign/FeignConfig.java delete mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/feign/KeycloakAdminApiClient.java create mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/web/ChoiceController.java delete mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/web/ExceptionHandlers.java create mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/web/QuestionController.java create mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/mapping/ChoiceMapper.java create mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/mapping/QuestionMapper.java create mode 100644 api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/mapping/QuizMapper.java create mode 100644 api/quiz-api/src/test/java/com/c4soft/quiz/QuizFixtures.java rename api/quiz-api/src/test/resources/{tonton-pirate.json => trainee.json} (66%) create mode 100644 api/quiz-api/src/test/resources/unprivieldged-trainer.json create mode 100644 build.sh create mode 100644 compose-desktop-ch4mp.yml create mode 100644 compose.yml create mode 100644 delete-eclipse-project-files.sh create mode 100644 keycloak/.env create mode 100644 keycloak/compose.yml create mode 100644 keycloak/import/quiz-realm.json create mode 100644 nginx-reverse-proxy/404.html create mode 100644 nginx-reverse-proxy/Dockerfile create mode 100644 nginx-reverse-proxy/nginx.conf create mode 100644 nginx.conf create mode 100644 quiz-realm.json diff --git a/.env b/.env new file mode 100644 index 0000000..b5fd578 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +KEYCLOAK_ADMIN_PASSWORD=admin +POSTGRES_QUIZ_PASSWORD=secret \ No newline at end of file diff --git a/.gitignore b/.gitignore index 80830fa..5f45265 100644 --- a/.gitignore +++ b/.gitignore @@ -114,3 +114,6 @@ resource-server-spring/** # OpenAPI specs **/*.openapi.json + +# Generated infra +compose-*.yml diff --git a/angular-ui.app.config.ts b/angular-ui.app.config.ts new file mode 100644 index 0000000..24f2355 --- /dev/null +++ b/angular-ui.app.config.ts @@ -0,0 +1,12 @@ +import { ApplicationConfig } from '@angular/core'; +import { provideRouter } from '@angular/router'; + +import { provideHttpClient } from '@angular/common/http'; +import { routes } from './app.routes'; + +export const appConfig: ApplicationConfig = { + providers: [provideRouter(routes), provideHttpClient()], +}; + +export const reverseProxyUri = 'http://LOCALHOST_NAME:7080'; +export const baseUri = `${reverseProxyUri}/angular-ui/`; diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 0000000..d7d1e8c --- /dev/null +++ b/api/.gitignore @@ -0,0 +1 @@ +!keycloak-admin-api.openapi.json \ No newline at end of file diff --git a/api/bff/pom.xml b/api/bff/pom.xml index bc85d51..6531914 100644 --- a/api/bff/pom.xml +++ b/api/bff/pom.xml @@ -95,6 +95,9 @@ openapi + + 7080 + org.springdoc @@ -124,6 +127,11 @@ start + + + https://oidc.c4-soft.com/auth + + post-integration-test diff --git a/api/bff/src/main/java/com/c4soft/quiz/BffApplication.java b/api/bff/src/main/java/com/c4soft/quiz/BffApplication.java index 033febb..a44327a 100644 --- a/api/bff/src/main/java/com/c4soft/quiz/BffApplication.java +++ b/api/bff/src/main/java/com/c4soft/quiz/BffApplication.java @@ -6,7 +6,7 @@ @SpringBootApplication public class BffApplication { - public static void main(String[] args) { - SpringApplication.run(BffApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(BffApplication.class, args); + } } diff --git a/api/bff/src/main/java/com/c4soft/quiz/BffController.java b/api/bff/src/main/java/com/c4soft/quiz/BffController.java index ed61ddc..57c5ead 100644 --- a/api/bff/src/main/java/com/c4soft/quiz/BffController.java +++ b/api/bff/src/main/java/com/c4soft/quiz/BffController.java @@ -2,7 +2,6 @@ import java.net.URISyntaxException; import java.util.List; - import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; @@ -10,9 +9,7 @@ import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; - import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties; - import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.constraints.NotEmpty; @@ -21,24 +18,28 @@ @RestController @Tag(name = "BFF") public class BffController { - private final List loginOptions; - - public BffController( - OAuth2ClientProperties clientProps, - SpringAddonsOidcProperties addonsProperties, - ReactiveClientRegistrationRepository clientRegistrationRepository) { - this.loginOptions = clientProps.getRegistration().entrySet().stream().filter(e -> "authorization_code".equals(e.getValue().getAuthorizationGrantType())) - .map(e -> new LoginOptionDto(e.getValue().getProvider(), "%s/oauth2/authorization/%s".formatted(addonsProperties.getClient().getClientUri(), e.getKey()))) - .toList(); - } - - @GetMapping(path = "/login-options", produces = MediaType.APPLICATION_JSON_VALUE) - @Operation(operationId = "getLoginOptions") - public Mono> getLoginOptions(Authentication auth) throws URISyntaxException { - final boolean isAuthenticated = auth instanceof OAuth2AuthenticationToken; - return Mono.just(isAuthenticated ? List.of() : this.loginOptions); - } - - public static record LoginOptionDto(@NotEmpty String label, @NotEmpty String loginUri) { - } + private final List loginOptions; + + public BffController(OAuth2ClientProperties clientProps, + SpringAddonsOidcProperties addonsProperties, + ReactiveClientRegistrationRepository clientRegistrationRepository) { + this.loginOptions = + clientProps.getRegistration().entrySet().stream() + .filter(e -> "authorization_code".equals(e.getValue().getAuthorizationGrantType())) + .map( + e -> new LoginOptionDto(e.getValue().getProvider(), + "%s/oauth2/authorization/%s" + .formatted(addonsProperties.getClient().getClientUri(), e.getKey()))) + .toList(); + } + + @GetMapping(path = "/login-options", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(operationId = "getLoginOptions") + public Mono> getLoginOptions(Authentication auth) throws URISyntaxException { + final boolean isAuthenticated = auth instanceof OAuth2AuthenticationToken; + return Mono.just(isAuthenticated ? List.of() : this.loginOptions); + } + + public static record LoginOptionDto(@NotEmpty String label, @NotEmpty String loginUri) { + } } diff --git a/api/bff/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/api/bff/src/main/resources/META-INF/additional-spring-configuration-metadata.json index a82412c..8896f65 100644 --- a/api/bff/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/api/bff/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -4,6 +4,11 @@ "type": "java.lang.String", "description": "'http' or 'https', used to build URIs involved in authentication and can be used in 'gateway-uri', 'bao-loc-api-uri' or 'ui-uri'" }, + { + "name": "hostname", + "type": "java.lang.String", + "description": "the host name" + }, { "name": "oauth2-issuer", "type": "java.lang.String", diff --git a/api/bff/src/main/resources/application.yml b/api/bff/src/main/resources/application.yml index 759f0d8..634de92 100644 --- a/api/bff/src/main/resources/application.yml +++ b/api/bff/src/main/resources/application.yml @@ -1,17 +1,16 @@ scheme: http -keycloak-host: https://oidc.c4-soft.com/auth +hostname: localhost +keycloak-host: http://${hostname}/auth keycloak-realm: quiz oauth2-issuer: ${keycloak-host}/realms/${keycloak-realm} oauth2-client-id: quiz-bff -oauth2-client-secret: change-me +oauth2-client-secret: secret -gateway-uri: ${scheme}://localhost:${server.port} -quiz-api-uri: ${scheme}://localhost:7084 -ui-host: https://localhost:4200 +gateway-uri: ${scheme}://${hostname}:${server.port} +quiz-api-uri: ${scheme}://${hostname}:7084 server: - port: 8080 - shutdown: graceful + port: 7080 ssl: enabled: false @@ -35,29 +34,12 @@ spring: client-id: ${oauth2-client-id} client-secret: ${oauth2-client-secret} authorization-grant-type: authorization_code - scope: - - openid - - profile - - email - - offline_access + scope: openid, profile, email, offline_access cloud: gateway: default-filters: - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin Access-Control-Request-Method Access-Control-Request-Headers routes: - # Redirection from / to /ui/ - - id: home - uri: ${gateway-uri} - predicates: - - Path=/ - filters: - - RedirectTo=301,${gateway-uri}/ui/ - # Serve the Angular app through the gateway - - id: ui - uri: ${ui-host} - predicates: - - Path=/ui/** - # Access the quiz API with BFF pattern - id: quiz-bff uri: ${quiz-api-uri} predicates: @@ -66,14 +48,6 @@ spring: - TokenRelay= - SaveSession - StripPrefix=2 - # Access the quiz API with OAuth2 clients like Postman - - id: quiz-resource-server - uri: ${quiz-api-uri} - predicates: - - Path=/resource-server/v1/** - filters: - - SaveSession - - StripPrefix=2 # Cert-manager http01 challenge for SSL certificates on K8s - id: letsencrypt uri: https://cert-manager-webhook @@ -95,11 +69,12 @@ com: security-matchers: - /login/** - /oauth2/** - - /logout + - /logout/** - /bff/** permit-all: - /login/** - /oauth2/** + - /logout/connect/back-channel/quiz-bff - /bff/** csrf: cookie-accessible-from-js post-login-redirect-path: /ui/ @@ -107,12 +82,14 @@ com: oauth2-redirections: rp-initiated-logout: ACCEPTED pkce-forced: true + back-channel-logout: + enabled: true + internal-logout-uri: ${scheme}://${hostname}:${serve.port}/logout # OAuth2 resource server configuration resourceserver: permit-all: - /login-options - /ui/** - - /resource-server/** - /v3/api-docs/** - /actuator/health/readiness - /actuator/health/liveness diff --git a/api/keycloak-admin-api.openapi.json b/api/keycloak-admin-api.openapi.json new file mode 100644 index 0000000..3a6f0d5 --- /dev/null +++ b/api/keycloak-admin-api.openapi.json @@ -0,0 +1,18358 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Keycloak Admin REST API", + "description": "This is a REST API reference for the Keycloak Admin REST API.", + "version": "1.0" + }, + "tags": [ + { + "name": "Attack Detection" + }, + { + "name": "Authentication Management" + }, + { + "name": "Client Attribute Certificate" + }, + { + "name": "Client Initial Access" + }, + { + "name": "Client Registration Policy" + }, + { + "name": "Client Role Mappings" + }, + { + "name": "Client Scopes" + }, + { + "name": "Clients" + }, + { + "name": "Component" + }, + { + "name": "Groups" + }, + { + "name": "Identity Providers" + }, + { + "name": "Key" + }, + { + "name": "Protocol Mappers" + }, + { + "name": "Realms Admin" + }, + { + "name": "Role Mapper" + }, + { + "name": "Roles" + }, + { + "name": "Roles (by ID)" + }, + { + "name": "Scope Mappings" + }, + { + "name": "Users" + } + ], + "paths": { + "/admin/realms": { + "get": { + "tags": [ + "Realms Admin" + ], + "summary": "Get accessible realms Returns a list of accessible realms. The list is filtered based on what realms the caller is allowed to view.", + "parameters": [ + { + "name": "briefRepresentation", + "in": "query", + "schema": { + "default": false, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RealmRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Realms Admin" + ], + "summary": "Import a realm. Imports a realm from a full representation of that realm.", + "description": "Realm name must be unique.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "format": "binary", + "type": "string" + } + } + } + }, + "responses": { + "201": { + "description": "Created" + } + } + } + }, + "/admin/realms/{realm}": { + "get": { + "tags": [ + "Realms Admin" + ], + "summary": "Get the top-level representation of the realm It will not include nested information like User and Client representations.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RealmRepresentation" + } + } + } + } + } + }, + "put": { + "tags": [ + "Realms Admin" + ], + "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.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RealmRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "Realms Admin" + ], + "summary": "Delete the realm", + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/admin-events": { + "get": { + "tags": [ + "Realms Admin" + ], + "summary": "Get admin events Returns all admin events, or filters events based on URL query parameters listed here", + "parameters": [ + { + "name": "authClient", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "authIpAddress", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "authRealm", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "authUser", + "in": "query", + "description": "user id", + "schema": { + "type": "string" + } + }, + { + "name": "dateFrom", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "dateTo", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "first", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "max", + "in": "query", + "description": "Maximum results size (defaults to 100)", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "operationTypes", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "resourcePath", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "resourceTypes", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdminEventRepresentation" + } + } + } + } + } + } + }, + "delete": { + "tags": [ + "Realms Admin" + ], + "summary": "Delete all admin events", + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/attack-detection/brute-force/users": { + "delete": { + "tags": [ + "Attack Detection" + ], + "summary": "Clear any user login failures for all users This can release temporary disabled users", + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/attack-detection/brute-force/users/{userId}": { + "get": { + "tags": [ + "Attack Detection" + ], + "summary": "Get status of a username in brute force detection", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + + } + } + } + } + } + } + }, + "delete": { + "tags": [ + "Attack Detection" + ], + "summary": "Clear any user login failures for the user This can release temporary disabled user", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/authenticator-providers": { + "get": { + "tags": [ + "Authentication Management" + ], + "summary": "Get authenticator providers Returns a stream of authenticator providers.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + + } + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/client-authenticator-providers": { + "get": { + "tags": [ + "Authentication Management" + ], + "summary": "Get client authenticator providers Returns a stream of client authenticator providers.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + + } + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/config": { + "post": { + "tags": [ + "Authentication Management" + ], + "summary": "Create new authenticator configuration", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticatorConfigRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + }, + "deprecated": true + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/config-description/{providerId}": { + "get": { + "tags": [ + "Authentication Management" + ], + "summary": "Get authenticator provider's configuration description", + "parameters": [ + { + "name": "providerId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticatorConfigInfoRepresentation" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/config/{id}": { + "get": { + "tags": [ + "Authentication Management" + ], + "summary": "Get authenticator configuration", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Configuration id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticatorConfigRepresentation" + } + } + } + } + } + }, + "put": { + "tags": [ + "Authentication Management" + ], + "summary": "Update authenticator configuration", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Configuration id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticatorConfigRepresentation" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Authentication Management" + ], + "summary": "Delete authenticator configuration", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Configuration id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/executions": { + "post": { + "tags": [ + "Authentication Management" + ], + "summary": "Add new authentication execution", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationExecutionRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/executions/{executionId}": { + "get": { + "tags": [ + "Authentication Management" + ], + "summary": "Get Single Execution", + "parameters": [ + { + "name": "executionId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "Authentication Management" + ], + "summary": "Delete execution", + "parameters": [ + { + "name": "executionId", + "in": "path", + "description": "Execution id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/executions/{executionId}/config": { + "post": { + "tags": [ + "Authentication Management" + ], + "summary": "Update execution with new configuration", + "parameters": [ + { + "name": "executionId", + "in": "path", + "description": "Execution id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticatorConfigRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/executions/{executionId}/config/{id}": { + "get": { + "tags": [ + "Authentication Management" + ], + "summary": "Get execution's configuration", + "parameters": [ + { + "name": "executionId", + "in": "path", + "description": "Execution id", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "path", + "description": "Configuration id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticatorConfigRepresentation" + } + } + } + } + }, + "deprecated": true + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/executions/{executionId}/lower-priority": { + "post": { + "tags": [ + "Authentication Management" + ], + "summary": "Lower execution's priority", + "parameters": [ + { + "name": "executionId", + "in": "path", + "description": "Execution id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/executions/{executionId}/raise-priority": { + "post": { + "tags": [ + "Authentication Management" + ], + "summary": "Raise execution's priority", + "parameters": [ + { + "name": "executionId", + "in": "path", + "description": "Execution id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/flows": { + "get": { + "tags": [ + "Authentication Management" + ], + "summary": "Get authentication flows Returns a stream of authentication flows.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AuthenticationFlowRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Authentication Management" + ], + "summary": "Create a new authentication flow", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationFlowRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/flows/{flowAlias}/copy": { + "post": { + "tags": [ + "Authentication Management" + ], + "summary": "Copy existing authentication flow under a new name The new name is given as 'newName' attribute of the passed JSON object", + "parameters": [ + { + "name": "flowAlias", + "in": "path", + "description": "name of the existing authentication flow", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/flows/{flowAlias}/executions": { + "get": { + "tags": [ + "Authentication Management" + ], + "summary": "Get authentication executions for a flow", + "parameters": [ + { + "name": "flowAlias", + "in": "path", + "description": "Flow alias", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "put": { + "tags": [ + "Authentication Management" + ], + "summary": "Update authentication executions of a Flow", + "parameters": [ + { + "name": "flowAlias", + "in": "path", + "description": "Flow alias", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationExecutionInfoRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/flows/{flowAlias}/executions/execution": { + "post": { + "tags": [ + "Authentication Management" + ], + "summary": "Add new authentication execution to a flow", + "parameters": [ + { + "name": "flowAlias", + "in": "path", + "description": "Alias of parent flow", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/flows/{flowAlias}/executions/flow": { + "post": { + "tags": [ + "Authentication Management" + ], + "summary": "Add new flow with new execution to existing flow", + "parameters": [ + { + "name": "flowAlias", + "in": "path", + "description": "Alias of parent authentication flow", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/flows/{id}": { + "get": { + "tags": [ + "Authentication Management" + ], + "summary": "Get authentication flow for id", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Flow id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationFlowRepresentation" + } + } + } + } + } + }, + "put": { + "tags": [ + "Authentication Management" + ], + "summary": "Update an authentication flow", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationFlowRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "Authentication Management" + ], + "summary": "Delete an authentication flow", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Flow id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/form-action-providers": { + "get": { + "tags": [ + "Authentication Management" + ], + "summary": "Get form action providers Returns a stream of form action providers.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + + } + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/form-providers": { + "get": { + "tags": [ + "Authentication Management" + ], + "summary": "Get form providers Returns a stream of form providers.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + + } + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/per-client-config-description": { + "get": { + "tags": [ + "Authentication Management" + ], + "summary": "Get configuration descriptions for all clients", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConfigPropertyRepresentation" + } + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/register-required-action": { + "post": { + "tags": [ + "Authentication Management" + ], + "summary": "Register a new required actions", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/required-actions": { + "get": { + "tags": [ + "Authentication Management" + ], + "summary": "Get required actions Returns a stream of required actions.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RequiredActionProviderRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/required-actions/{alias}": { + "get": { + "tags": [ + "Authentication Management" + ], + "summary": "Get required action for alias", + "parameters": [ + { + "name": "alias", + "in": "path", + "description": "Alias of required action", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequiredActionProviderRepresentation" + } + } + } + } + } + }, + "put": { + "tags": [ + "Authentication Management" + ], + "summary": "Update required action", + "parameters": [ + { + "name": "alias", + "in": "path", + "description": "Alias of required action", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequiredActionProviderRepresentation" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Authentication Management" + ], + "summary": "Delete required action", + "parameters": [ + { + "name": "alias", + "in": "path", + "description": "Alias of required action", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/required-actions/{alias}/lower-priority": { + "post": { + "tags": [ + "Authentication Management" + ], + "summary": "Lower required action's priority", + "parameters": [ + { + "name": "alias", + "in": "path", + "description": "Alias of required action", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/required-actions/{alias}/raise-priority": { + "post": { + "tags": [ + "Authentication Management" + ], + "summary": "Raise required action's priority", + "parameters": [ + { + "name": "alias", + "in": "path", + "description": "Alias of required action", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/authentication/unregistered-required-actions": { + "get": { + "tags": [ + "Authentication Management" + ], + "summary": "Get unregistered required actions Returns a stream of unregistered required actions.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-description-converter": { + "post": { + "tags": [ + "Realms Admin" + ], + "summary": "Base path for importing clients under this realm.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + }, + "application/xml": { + "schema": { + "type": "string" + } + }, + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientRepresentation" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-policies/policies": { + "get": { + "tags": [ + "Realms Admin" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientPoliciesRepresentation" + } + } + } + } + } + }, + "put": { + "tags": [ + "Realms Admin" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientPoliciesRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-policies/profiles": { + "get": { + "tags": [ + "Realms Admin" + ], + "parameters": [ + { + "name": "include-global-profiles", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientProfilesRepresentation" + } + } + } + } + } + }, + "put": { + "tags": [ + "Realms Admin" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientProfilesRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-registration-policy/providers": { + "get": { + "tags": [ + "Client Registration Policy" + ], + "summary": "Base path for retrieve providers with the configProperties properly filled", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ComponentTypeRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-scopes": { + "get": { + "tags": [ + "Client Scopes" + ], + "summary": "Get client scopes belonging to the realm Returns a list of client scopes belonging to the realm", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ClientScopeRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Client Scopes" + ], + "summary": "Create a new client scope Client Scope’s name must be unique!", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientScopeRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-scopes/{client-scope-id}": { + "get": { + "tags": [ + "Client Scopes" + ], + "summary": "Get representation of the client scope", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientScopeRepresentation" + } + } + } + } + } + }, + "put": { + "tags": [ + "Client Scopes" + ], + "summary": "Update the client scope", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientScopeRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "Client Scopes" + ], + "summary": "Delete the client scope", + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-scopes/{client-scope-id}/protocol-mappers/add-models": { + "post": { + "tags": [ + "Protocol Mappers" + ], + "summary": "Create multiple mappers", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-scopes/{client-scope-id}/protocol-mappers/models": { + "get": { + "tags": [ + "Protocol Mappers" + ], + "summary": "Get mappers", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Protocol Mappers" + ], + "summary": "Create a mapper", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-scopes/{client-scope-id}/protocol-mappers/models/{id}": { + "get": { + "tags": [ + "Protocol Mappers" + ], + "summary": "Get mapper by id", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Mapper id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + } + } + } + } + }, + "put": { + "tags": [ + "Protocol Mappers" + ], + "summary": "Update the mapper", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Mapper id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Protocol Mappers" + ], + "summary": "Delete the mapper", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Mapper id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-scopes/{client-scope-id}/protocol-mappers/protocol/{protocol}": { + "get": { + "tags": [ + "Protocol Mappers" + ], + "summary": "Get mappers by name for a specific protocol", + "parameters": [ + { + "name": "protocol", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-scopes/{client-scope-id}/scope-mappings": { + "get": { + "tags": [ + "Scope Mappings" + ], + "summary": "Get all scope mappings for the client", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MappingsRepresentation" + } + } + } + } + }, + "deprecated": true + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-scopes/{client-scope-id}/scope-mappings/clients/{client}": { + "get": { + "tags": [ + "Scope Mappings" + ], + "summary": "Get the roles associated with a client's scope Returns roles for the client.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Scope Mappings" + ], + "summary": "Add client-level roles to the client's scope", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Scope Mappings" + ], + "summary": "Remove client-level roles from the client's scope.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-scopes/{client-scope-id}/scope-mappings/clients/{client}/available": { + "get": { + "tags": [ + "Scope Mappings" + ], + "summary": "The available client-level roles Returns the roles for the client that can be associated with the client's scope", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-scopes/{client-scope-id}/scope-mappings/clients/{client}/composite": { + "get": { + "tags": [ + "Scope Mappings" + ], + "summary": "Get effective client roles Returns the roles for the client that are associated with the client's scope.", + "parameters": [ + { + "name": "briefRepresentation", + "in": "query", + "description": "if false, return roles with their attributes", + "schema": { + "default": true, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-scopes/{client-scope-id}/scope-mappings/realm": { + "get": { + "tags": [ + "Scope Mappings" + ], + "summary": "Get realm-level roles associated with the client's scope", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Scope Mappings" + ], + "summary": "Add a set of realm-level roles to the client's scope", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Scope Mappings" + ], + "summary": "Remove a set of realm-level roles from the client's scope", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-scopes/{client-scope-id}/scope-mappings/realm/available": { + "get": { + "tags": [ + "Scope Mappings" + ], + "summary": "Get realm-level roles that are available to attach to this client's scope", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-scopes/{client-scope-id}/scope-mappings/realm/composite": { + "get": { + "tags": [ + "Scope Mappings" + ], + "summary": "Get effective realm-level roles associated with the client’s scope What this does is recurse any composite roles associated with the client’s scope and adds the roles to this lists.", + "description": "The method is really to show a comprehensive total view of realm-level roles associated with the client.", + "parameters": [ + { + "name": "briefRepresentation", + "in": "query", + "description": "if false, return roles with their attributes", + "schema": { + "default": true, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-session-stats": { + "get": { + "tags": [ + "Realms Admin" + ], + "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.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-templates": { + "get": { + "tags": [ + "Client Scopes" + ], + "summary": "Get client scopes belonging to the realm Returns a list of client scopes belonging to the realm", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ClientScopeRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Client Scopes" + ], + "summary": "Create a new client scope Client Scope’s name must be unique!", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientScopeRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-templates/{client-scope-id}": { + "get": { + "tags": [ + "Client Scopes" + ], + "summary": "Get representation of the client scope", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientScopeRepresentation" + } + } + } + } + } + }, + "put": { + "tags": [ + "Client Scopes" + ], + "summary": "Update the client scope", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientScopeRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "Client Scopes" + ], + "summary": "Delete the client scope", + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-templates/{client-scope-id}/protocol-mappers/add-models": { + "post": { + "tags": [ + "Protocol Mappers" + ], + "summary": "Create multiple mappers", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-templates/{client-scope-id}/protocol-mappers/models": { + "get": { + "tags": [ + "Protocol Mappers" + ], + "summary": "Get mappers", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Protocol Mappers" + ], + "summary": "Create a mapper", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-templates/{client-scope-id}/protocol-mappers/models/{id}": { + "get": { + "tags": [ + "Protocol Mappers" + ], + "summary": "Get mapper by id", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Mapper id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + } + } + } + } + }, + "put": { + "tags": [ + "Protocol Mappers" + ], + "summary": "Update the mapper", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Mapper id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Protocol Mappers" + ], + "summary": "Delete the mapper", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Mapper id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-templates/{client-scope-id}/protocol-mappers/protocol/{protocol}": { + "get": { + "tags": [ + "Protocol Mappers" + ], + "summary": "Get mappers by name for a specific protocol", + "parameters": [ + { + "name": "protocol", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-templates/{client-scope-id}/scope-mappings": { + "get": { + "tags": [ + "Scope Mappings" + ], + "summary": "Get all scope mappings for the client", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MappingsRepresentation" + } + } + } + } + }, + "deprecated": true + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-templates/{client-scope-id}/scope-mappings/clients/{client}": { + "get": { + "tags": [ + "Scope Mappings" + ], + "summary": "Get the roles associated with a client's scope Returns roles for the client.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Scope Mappings" + ], + "summary": "Add client-level roles to the client's scope", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Scope Mappings" + ], + "summary": "Remove client-level roles from the client's scope.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-templates/{client-scope-id}/scope-mappings/clients/{client}/available": { + "get": { + "tags": [ + "Scope Mappings" + ], + "summary": "The available client-level roles Returns the roles for the client that can be associated with the client's scope", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-templates/{client-scope-id}/scope-mappings/clients/{client}/composite": { + "get": { + "tags": [ + "Scope Mappings" + ], + "summary": "Get effective client roles Returns the roles for the client that are associated with the client's scope.", + "parameters": [ + { + "name": "briefRepresentation", + "in": "query", + "description": "if false, return roles with their attributes", + "schema": { + "default": true, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-templates/{client-scope-id}/scope-mappings/realm": { + "get": { + "tags": [ + "Scope Mappings" + ], + "summary": "Get realm-level roles associated with the client's scope", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Scope Mappings" + ], + "summary": "Add a set of realm-level roles to the client's scope", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Scope Mappings" + ], + "summary": "Remove a set of realm-level roles from the client's scope", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-templates/{client-scope-id}/scope-mappings/realm/available": { + "get": { + "tags": [ + "Scope Mappings" + ], + "summary": "Get realm-level roles that are available to attach to this client's scope", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/client-templates/{client-scope-id}/scope-mappings/realm/composite": { + "get": { + "tags": [ + "Scope Mappings" + ], + "summary": "Get effective realm-level roles associated with the client’s scope What this does is recurse any composite roles associated with the client’s scope and adds the roles to this lists.", + "description": "The method is really to show a comprehensive total view of realm-level roles associated with the client.", + "parameters": [ + { + "name": "briefRepresentation", + "in": "query", + "description": "if false, return roles with their attributes", + "schema": { + "default": true, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients": { + "get": { + "tags": [ + "Clients" + ], + "summary": "Get clients belonging to the realm.", + "description": "If a client can’t be retrieved from the storage due to a problem with the underlying storage, it is silently removed from the returned list. This ensures that concurrent modifications to the list don’t prevent callers from retrieving this list.", + "parameters": [ + { + "name": "clientId", + "in": "query", + "description": "filter by clientId", + "schema": { + "type": "string" + } + }, + { + "name": "first", + "in": "query", + "description": "the first result", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "max", + "in": "query", + "description": "the max results to return", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "q", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "search", + "in": "query", + "description": "whether this is a search query or a getClientById query", + "schema": { + "default": false, + "type": "boolean" + } + }, + { + "name": "viewableOnly", + "in": "query", + "description": "filter clients that cannot be viewed in full by admin", + "schema": { + "default": false, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ClientRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Clients" + ], + "summary": "Create a new client Client’s client_id must be unique!", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientRepresentation" + } + } + } + }, + "responses": { + "201": { + "description": "Created" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients-initial-access": { + "get": { + "tags": [ + "Client Initial Access" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ClientInitialAccessPresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Client Initial Access" + ], + "summary": "Create a new initial access token.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientInitialAccessCreatePresentation" + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientInitialAccessCreatePresentation" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients-initial-access/{id}": { + "delete": { + "tags": [ + "Client Initial Access" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}": { + "get": { + "tags": [ + "Clients" + ], + "summary": "Get representation of the client", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientRepresentation" + } + } + } + } + } + }, + "put": { + "tags": [ + "Clients" + ], + "summary": "Update the client", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "Clients" + ], + "summary": "Delete the client", + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceServerRepresentation" + } + } + } + } + } + }, + "put": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceServerRepresentation" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server/import": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceServerRepresentation" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server/permission": { + "get": { + "parameters": [ + { + "name": "fields", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "first", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "max", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "owner", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "permission", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "policyId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "resource", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "scope", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AbstractPolicyRepresentation" + } + } + } + } + }, + "204": { + "description": "No Content" + } + } + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "responses": { + "201": { + "description": "Created" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server/permission/evaluate": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PolicyEvaluationRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PolicyEvaluationResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server/permission/providers": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PolicyProviderRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server/permission/search": { + "get": { + "parameters": [ + { + "name": "fields", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AbstractPolicyRepresentation" + } + } + } + }, + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server/policy": { + "get": { + "parameters": [ + { + "name": "fields", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "first", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "max", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "owner", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "permission", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "policyId", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "resource", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "scope", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AbstractPolicyRepresentation" + } + } + } + } + }, + "204": { + "description": "No Content" + } + } + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "responses": { + "201": { + "description": "Created" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server/policy/evaluate": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PolicyEvaluationRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PolicyEvaluationResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server/policy/providers": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PolicyProviderRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server/policy/search": { + "get": { + "parameters": [ + { + "name": "fields", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AbstractPolicyRepresentation" + } + } + } + }, + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server/resource": { + "get": { + "parameters": [ + { + "name": "_id", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "deep", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "exactName", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "first", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "matchingUri", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "max", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "owner", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "scope", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "uri", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceRepresentation" + } + } + } + } + } + } + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceRepresentation" + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceRepresentation" + } + } + } + }, + "400": { + "description": "Bad Request" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "_id", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "deep", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "exactName", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "first", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "matchingUri", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "max", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "owner", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "scope", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "uri", + "in": "query", + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server/resource/search": { + "get": { + "parameters": [ + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceRepresentation" + } + } + } + }, + "400": { + "description": "Bad Request" + }, + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "_id", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "deep", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "exactName", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "first", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "matchingUri", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "max", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "owner", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "scope", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "uri", + "in": "query", + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server/resource/{resource-id}": { + "get": { + "parameters": [ + { + "name": "resource-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceRepresentation" + } + } + } + }, + "404": { + "description": "Not found" + } + } + }, + "put": { + "parameters": [ + { + "name": "resource-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceRepresentation" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found" + } + } + }, + "delete": { + "parameters": [ + { + "name": "resource-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "_id", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "deep", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "exactName", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "first", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "matchingUri", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "max", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "owner", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "scope", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "uri", + "in": "query", + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server/resource/{resource-id}/attributes": { + "get": { + "parameters": [ + { + "name": "resource-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "_id", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "deep", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "exactName", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "first", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "matchingUri", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "max", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "owner", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "scope", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "uri", + "in": "query", + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server/resource/{resource-id}/permissions": { + "get": { + "parameters": [ + { + "name": "resource-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PolicyRepresentation" + } + } + } + } + }, + "404": { + "description": "Not found" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "_id", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "deep", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "exactName", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "first", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "matchingUri", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "max", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "owner", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "scope", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "uri", + "in": "query", + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server/resource/{resource-id}/scopes": { + "get": { + "parameters": [ + { + "name": "resource-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScopeRepresentation" + } + } + } + } + }, + "404": { + "description": "Not found" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "_id", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "deep", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "exactName", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "first", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "matchingUri", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "max", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "owner", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "scope", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "uri", + "in": "query", + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server/scope": { + "get": { + "parameters": [ + { + "name": "first", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "max", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "scopeId", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScopeRepresentation" + } + } + } + } + } + } + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScopeRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server/scope/search": { + "get": { + "parameters": [ + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScopeRepresentation" + } + } + } + } + }, + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server/scope/{scope-id}": { + "get": { + "parameters": [ + { + "name": "scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScopeRepresentation" + } + } + } + }, + "404": { + "description": "Not found" + } + } + }, + "put": { + "parameters": [ + { + "name": "scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScopeRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "parameters": [ + { + "name": "scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server/scope/{scope-id}/permissions": { + "get": { + "parameters": [ + { + "name": "scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PolicyRepresentation" + } + } + } + } + }, + "404": { + "description": "Not found" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server/scope/{scope-id}/resources": { + "get": { + "parameters": [ + { + "name": "scope-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceRepresentation" + } + } + } + } + }, + "404": { + "description": "Not found" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/authz/resource-server/settings": { + "get": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceServerRepresentation" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/certificates/{attr}": { + "get": { + "tags": [ + "Client Attribute Certificate" + ], + "summary": "Get key info", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CertificateRepresentation" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "attr", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/certificates/{attr}/download": { + "post": { + "tags": [ + "Client Attribute Certificate" + ], + "summary": "Get a keystore file for the client, containing private key and public certificate", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyStoreConfig" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "attr", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/certificates/{attr}/generate": { + "post": { + "tags": [ + "Client Attribute Certificate" + ], + "summary": "Generate a new certificate with new key pair", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CertificateRepresentation" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "attr", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/certificates/{attr}/generate-and-download": { + "post": { + "tags": [ + "Client Attribute Certificate" + ], + "summary": "Generate a new keypair and certificate, and get the private key file\n\nGenerates a keypair and certificate and serves the private key in a specified keystore format.\nOnly generated public certificate is saved in Keycloak DB - the private key is not.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyStoreConfig" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "attr", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/certificates/{attr}/upload": { + "post": { + "tags": [ + "Client Attribute Certificate" + ], + "summary": "Upload certificate and eventually private key", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CertificateRepresentation" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "attr", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/certificates/{attr}/upload-certificate": { + "post": { + "tags": [ + "Client Attribute Certificate" + ], + "summary": "Upload only certificate, not private key", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CertificateRepresentation" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "attr", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/client-secret": { + "get": { + "tags": [ + "Clients" + ], + "summary": "Get the client secret", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRepresentation" + } + } + } + } + } + }, + "post": { + "tags": [ + "Clients" + ], + "summary": "Generate a new secret for the client", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRepresentation" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/client-secret/rotated": { + "get": { + "tags": [ + "Clients" + ], + "summary": "Get the rotated client secret", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRepresentation" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Clients" + ], + "summary": "Invalidate the rotated secret for the client", + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/default-client-scopes": { + "get": { + "tags": [ + "Clients" + ], + "summary": "Get default client scopes. Only name and ids are returned.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ClientScopeRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/default-client-scopes/{clientScopeId}": { + "put": { + "tags": [ + "Clients" + ], + "parameters": [ + { + "name": "clientScopeId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Clients" + ], + "parameters": [ + { + "name": "clientScopeId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/evaluate-scopes/generate-example-access-token": { + "get": { + "tags": [ + "Clients" + ], + "summary": "Create JSON with payload of example access token", + "parameters": [ + { + "name": "scope", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessToken" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/evaluate-scopes/generate-example-id-token": { + "get": { + "tags": [ + "Clients" + ], + "summary": "Create JSON with payload of example id token", + "parameters": [ + { + "name": "scope", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IDToken" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/evaluate-scopes/generate-example-userinfo": { + "get": { + "tags": [ + "Clients" + ], + "summary": "Create JSON with payload of example user info", + "parameters": [ + { + "name": "scope", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "userId", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/evaluate-scopes/protocol-mappers": { + "get": { + "tags": [ + "Clients" + ], + "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.", + "parameters": [ + { + "name": "scope", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProtocolMapperEvaluationRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/evaluate-scopes/scope-mappings/{roleContainerId}/granted": { + "get": { + "tags": [ + "Clients" + ], + "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.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "roleContainerId", + "in": "path", + "description": "either realm name OR client UUID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "scope", + "in": "query", + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/evaluate-scopes/scope-mappings/{roleContainerId}/not-granted": { + "get": { + "tags": [ + "Clients" + ], + "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()}", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "roleContainerId", + "in": "path", + "description": "either realm name OR client UUID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "scope", + "in": "query", + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/installation/providers/{providerId}": { + "get": { + "tags": [ + "Clients" + ], + "parameters": [ + { + "name": "providerId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/management/permissions": { + "get": { + "tags": [ + "Clients" + ], + "summary": "Return object stating whether client Authorization permissions have been initialized or not and a reference", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementPermissionReference" + } + } + } + } + } + }, + "put": { + "tags": [ + "Clients" + ], + "summary": "Return object stating whether client Authorization permissions have been initialized or not and a reference", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementPermissionReference" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementPermissionReference" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/nodes": { + "post": { + "tags": [ + "Clients" + ], + "summary": "Register a cluster node with the client Manually register cluster node to this client - usually it’s not needed to call this directly as adapter should handle by sending registration request to Keycloak", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/nodes/{node}": { + "delete": { + "tags": [ + "Clients" + ], + "summary": "Unregister a cluster node from the client", + "parameters": [ + { + "name": "node", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/offline-session-count": { + "get": { + "tags": [ + "Clients" + ], + "summary": "Get application offline session count Returns a number of offline user sessions associated with this client { \"count\": number }", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "format": "int64", + "type": "integer" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/offline-sessions": { + "get": { + "tags": [ + "Clients" + ], + "summary": "Get offline sessions for client Returns a list of offline user sessions associated with this client", + "parameters": [ + { + "name": "first", + "in": "query", + "description": "Paging offset", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "max", + "in": "query", + "description": "Maximum results size (defaults to 100)", + "schema": { + "format": "int32", + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserSessionRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/optional-client-scopes": { + "get": { + "tags": [ + "Clients" + ], + "summary": "Get optional client scopes. Only name and ids are returned.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ClientScopeRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/optional-client-scopes/{clientScopeId}": { + "put": { + "tags": [ + "Clients" + ], + "parameters": [ + { + "name": "clientScopeId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Clients" + ], + "parameters": [ + { + "name": "clientScopeId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/protocol-mappers/add-models": { + "post": { + "tags": [ + "Protocol Mappers" + ], + "summary": "Create multiple mappers", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/protocol-mappers/models": { + "get": { + "tags": [ + "Protocol Mappers" + ], + "summary": "Get mappers", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Protocol Mappers" + ], + "summary": "Create a mapper", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/protocol-mappers/models/{id}": { + "get": { + "tags": [ + "Protocol Mappers" + ], + "summary": "Get mapper by id", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Mapper id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + } + } + } + } + }, + "put": { + "tags": [ + "Protocol Mappers" + ], + "summary": "Update the mapper", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Mapper id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Protocol Mappers" + ], + "summary": "Delete the mapper", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Mapper id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/protocol-mappers/protocol/{protocol}": { + "get": { + "tags": [ + "Protocol Mappers" + ], + "summary": "Get mappers by name for a specific protocol", + "parameters": [ + { + "name": "protocol", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/push-revocation": { + "post": { + "tags": [ + "Clients" + ], + "summary": "Push the client's revocation policy to its admin URL If the client has an admin URL, push revocation policy to it.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GlobalRequestResult" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/registration-access-token": { + "post": { + "tags": [ + "Clients" + ], + "summary": "Generate a new registration access token for the client", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientRepresentation" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/roles": { + "get": { + "tags": [ + "Roles" + ], + "summary": "Get all roles for the realm or client", + "parameters": [ + { + "name": "briefRepresentation", + "in": "query", + "schema": { + "default": true, + "type": "boolean" + } + }, + { + "name": "first", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "max", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "search", + "in": "query", + "schema": { + "default": "", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Roles" + ], + "summary": "Create a new role for the realm or client", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/roles/{role-name}": { + "get": { + "tags": [ + "Roles" + ], + "summary": "Get a role by name", + "parameters": [ + { + "name": "role-name", + "in": "path", + "description": "role's name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + }, + "put": { + "tags": [ + "Roles" + ], + "summary": "Update a role by name", + "parameters": [ + { + "name": "role-name", + "in": "path", + "description": "role's name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "Roles" + ], + "summary": "Delete a role by name", + "parameters": [ + { + "name": "role-name", + "in": "path", + "description": "role's name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/roles/{role-name}/composites": { + "get": { + "tags": [ + "Roles" + ], + "summary": "Get composites of the role", + "parameters": [ + { + "name": "role-name", + "in": "path", + "description": "role's name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Roles" + ], + "summary": "Add a composite to the role", + "parameters": [ + { + "name": "role-name", + "in": "path", + "description": "role's name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Roles" + ], + "summary": "Remove roles from the role's composite", + "parameters": [ + { + "name": "role-name", + "in": "path", + "description": "role's name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/roles/{role-name}/composites/clients/{client-uuid}": { + "get": { + "tags": [ + "Roles" + ], + "summary": "Get client-level roles for the client that are in the role's composite", + "parameters": [ + { + "name": "client-uuid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "role-name", + "in": "path", + "description": "role's name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/roles/{role-name}/composites/realm": { + "get": { + "tags": [ + "Roles" + ], + "summary": "Get realm-level roles of the role's composite", + "parameters": [ + { + "name": "role-name", + "in": "path", + "description": "role's name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/roles/{role-name}/groups": { + "get": { + "tags": [ + "Roles" + ], + "summary": "Returns a stream of groups that have the specified role name", + "parameters": [ + { + "name": "role-name", + "in": "path", + "description": "the role name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "briefRepresentation", + "in": "query", + "description": "if false, return a full representation of the {@code GroupRepresentation} objects.", + "schema": { + "default": true, + "type": "boolean" + } + }, + { + "name": "first", + "in": "query", + "description": "first result to return. Ignored if negative or {@code null}.", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "max", + "in": "query", + "description": "maximum number of results to return. Ignored if negative or {@code null}.", + "schema": { + "format": "int32", + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/roles/{role-name}/management/permissions": { + "get": { + "tags": [ + "Roles" + ], + "summary": "Return object stating whether role Authorization permissions have been initialized or not and a reference", + "parameters": [ + { + "name": "role-name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementPermissionReference" + } + } + } + } + } + }, + "put": { + "tags": [ + "Roles" + ], + "summary": "Return object stating whether role Authorization permissions have been initialized or not and a reference", + "parameters": [ + { + "name": "role-name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementPermissionReference" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementPermissionReference" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/roles/{role-name}/users": { + "get": { + "tags": [ + "Roles" + ], + "summary": "Returns a stream of users that have the specified role name.", + "parameters": [ + { + "name": "role-name", + "in": "path", + "description": "the role name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "first", + "in": "query", + "description": "first result to return. Ignored if negative or {@code null}.", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "max", + "in": "query", + "description": "maximum number of results to return. Ignored if negative or {@code null}.", + "schema": { + "format": "int32", + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/scope-mappings": { + "get": { + "tags": [ + "Scope Mappings" + ], + "summary": "Get all scope mappings for the client", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MappingsRepresentation" + } + } + } + } + }, + "deprecated": true + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/scope-mappings/clients/{client}": { + "get": { + "tags": [ + "Scope Mappings" + ], + "summary": "Get the roles associated with a client's scope Returns roles for the client.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Scope Mappings" + ], + "summary": "Add client-level roles to the client's scope", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Scope Mappings" + ], + "summary": "Remove client-level roles from the client's scope.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/scope-mappings/clients/{client}/available": { + "get": { + "tags": [ + "Scope Mappings" + ], + "summary": "The available client-level roles Returns the roles for the client that can be associated with the client's scope", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/scope-mappings/clients/{client}/composite": { + "get": { + "tags": [ + "Scope Mappings" + ], + "summary": "Get effective client roles Returns the roles for the client that are associated with the client's scope.", + "parameters": [ + { + "name": "briefRepresentation", + "in": "query", + "description": "if false, return roles with their attributes", + "schema": { + "default": true, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/scope-mappings/realm": { + "get": { + "tags": [ + "Scope Mappings" + ], + "summary": "Get realm-level roles associated with the client's scope", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Scope Mappings" + ], + "summary": "Add a set of realm-level roles to the client's scope", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Scope Mappings" + ], + "summary": "Remove a set of realm-level roles from the client's scope", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/scope-mappings/realm/available": { + "get": { + "tags": [ + "Scope Mappings" + ], + "summary": "Get realm-level roles that are available to attach to this client's scope", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/scope-mappings/realm/composite": { + "get": { + "tags": [ + "Scope Mappings" + ], + "summary": "Get effective realm-level roles associated with the client’s scope What this does is recurse any composite roles associated with the client’s scope and adds the roles to this lists.", + "description": "The method is really to show a comprehensive total view of realm-level roles associated with the client.", + "parameters": [ + { + "name": "briefRepresentation", + "in": "query", + "description": "if false, return roles with their attributes", + "schema": { + "default": true, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/service-account-user": { + "get": { + "tags": [ + "Clients" + ], + "summary": "Get a user dedicated to the service account", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserRepresentation" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/session-count": { + "get": { + "tags": [ + "Clients" + ], + "summary": "Get application session count Returns a number of user sessions associated with this client { \"count\": number }", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "format": "int64", + "type": "integer" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/test-nodes-available": { + "get": { + "tags": [ + "Clients" + ], + "summary": "Test if registered cluster nodes are available Tests availability by sending 'ping' request to all cluster nodes.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GlobalRequestResult" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/clients/{client-uuid}/user-sessions": { + "get": { + "tags": [ + "Clients" + ], + "summary": "Get user sessions for client Returns a list of user sessions associated with this client\n", + "parameters": [ + { + "name": "first", + "in": "query", + "description": "Paging offset", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "max", + "in": "query", + "description": "Maximum results size (defaults to 100)", + "schema": { + "format": "int32", + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserSessionRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client-uuid", + "in": "path", + "description": "id of client (not client-id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/components": { + "get": { + "tags": [ + "Component" + ], + "parameters": [ + { + "name": "name", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "parent", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ComponentRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Component" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComponentRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/components/{id}": { + "get": { + "tags": [ + "Component" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComponentRepresentation" + } + } + } + } + } + }, + "put": { + "tags": [ + "Component" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ComponentRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "Component" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/components/{id}/sub-component-types": { + "get": { + "tags": [ + "Component" + ], + "summary": "List of subcomponent types that are available to configure for a particular parent component.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "type", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ComponentTypeRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/credential-registrators": { + "get": { + "tags": [ + "Realms Admin" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/default-default-client-scopes": { + "get": { + "tags": [ + "Realms Admin" + ], + "summary": "Get realm default client scopes. Only name and ids are returned.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ClientScopeRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/default-default-client-scopes/{clientScopeId}": { + "put": { + "tags": [ + "Realms Admin" + ], + "parameters": [ + { + "name": "clientScopeId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Realms Admin" + ], + "parameters": [ + { + "name": "clientScopeId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/default-groups": { + "get": { + "tags": [ + "Realms Admin" + ], + "summary": "Get group hierarchy. Only name and ids are returned.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/default-groups/{groupId}": { + "put": { + "tags": [ + "Realms Admin" + ], + "parameters": [ + { + "name": "groupId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Realms Admin" + ], + "parameters": [ + { + "name": "groupId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/default-optional-client-scopes": { + "get": { + "tags": [ + "Realms Admin" + ], + "summary": "Get realm optional client scopes. Only name and ids are returned.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ClientScopeRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/default-optional-client-scopes/{clientScopeId}": { + "put": { + "tags": [ + "Realms Admin" + ], + "parameters": [ + { + "name": "clientScopeId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Realms Admin" + ], + "parameters": [ + { + "name": "clientScopeId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/events": { + "get": { + "tags": [ + "Realms Admin" + ], + "summary": "Get events Returns all events, or filters them based on URL query parameters listed here", + "parameters": [ + { + "name": "client", + "in": "query", + "description": "App or oauth client name", + "schema": { + "type": "string" + } + }, + { + "name": "dateFrom", + "in": "query", + "description": "From date", + "schema": { + "type": "string" + } + }, + { + "name": "dateTo", + "in": "query", + "description": "To date", + "schema": { + "type": "string" + } + }, + { + "name": "first", + "in": "query", + "description": "Paging offset", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "ipAddress", + "in": "query", + "description": "IP Address", + "schema": { + "type": "string" + } + }, + { + "name": "max", + "in": "query", + "description": "Maximum results size (defaults to 100)", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "type", + "in": "query", + "description": "The types of events to return", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "user", + "in": "query", + "description": "User id", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventRepresentation" + } + } + } + } + } + } + }, + "delete": { + "tags": [ + "Realms Admin" + ], + "summary": "Delete all events", + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/events/config": { + "get": { + "tags": [ + "Realms Admin" + ], + "summary": "Get the events provider configuration Returns JSON object with events provider configuration", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RealmEventsConfigRepresentation" + } + } + } + } + } + }, + "put": { + "tags": [ + "Realms Admin" + ], + "description": "Update the events provider Change the events provider and/or its configuration", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RealmEventsConfigRepresentation" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/group-by-path/{path}": { + "get": { + "tags": [ + "Realms Admin" + ], + "parameters": [ + { + "name": "path", + "in": "path", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathSegment" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupRepresentation" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/groups": { + "get": { + "tags": [ + "Groups" + ], + "summary": "Get group hierarchy. Only name and ids are returned.", + "parameters": [ + { + "name": "briefRepresentation", + "in": "query", + "schema": { + "default": true, + "type": "boolean" + } + }, + { + "name": "exact", + "in": "query", + "schema": { + "default": "false", + "type": "boolean" + } + }, + { + "name": "first", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "max", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "populateHierarchy", + "in": "query", + "schema": { + "default": true, + "type": "boolean" + } + }, + { + "name": "q", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Groups" + ], + "summary": "create or add a top level realm groupSet or create child.", + "description": "This will update the group and set the parent if it exists. Create it and set the parent if the group doesn’t exist.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/groups/count": { + "get": { + "tags": [ + "Groups" + ], + "summary": "Returns the groups counts.", + "parameters": [ + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "top", + "in": "query", + "schema": { + "default": false, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "format": "int64", + "type": "integer" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/groups/{group-id}": { + "get": { + "tags": [ + "Groups" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupRepresentation" + } + } + } + } + } + }, + "put": { + "tags": [ + "Groups" + ], + "summary": "Update group, ignores subgroups.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "Groups" + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "group-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/groups/{group-id}/children": { + "get": { + "tags": [ + "Groups" + ], + "summary": "Return a paginated list of subgroups that have a parent group corresponding to the group on the URL", + "parameters": [ + { + "name": "briefRepresentation", + "in": "query", + "schema": { + "default": "false", + "type": "boolean" + } + }, + { + "name": "first", + "in": "query", + "schema": { + "format": "int32", + "default": "0", + "type": "integer" + } + }, + { + "name": "max", + "in": "query", + "schema": { + "format": "int32", + "default": "10", + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Groups" + ], + "summary": "Set or create child.", + "description": "This will just set the parent if it exists. Create it and set the parent if the group doesn’t exist.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "group-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/groups/{group-id}/management/permissions": { + "get": { + "tags": [ + "Groups" + ], + "summary": "Return object stating whether client Authorization permissions have been initialized or not and a reference", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementPermissionReference" + } + } + } + } + } + }, + "put": { + "tags": [ + "Groups" + ], + "summary": "Return object stating whether client Authorization permissions have been initialized or not and a reference", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementPermissionReference" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementPermissionReference" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "group-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/groups/{group-id}/members": { + "get": { + "tags": [ + "Groups" + ], + "summary": "Get users Returns a stream of users, filtered according to query parameters", + "parameters": [ + { + "name": "briefRepresentation", + "in": "query", + "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.)", + "schema": { + "type": "boolean" + } + }, + { + "name": "first", + "in": "query", + "description": "Pagination offset", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "max", + "in": "query", + "description": "Maximum results size (defaults to 100)", + "schema": { + "format": "int32", + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "group-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/groups/{group-id}/role-mappings": { + "get": { + "tags": [ + "Role Mapper" + ], + "summary": "Get role mappings", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MappingsRepresentation" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "group-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/groups/{group-id}/role-mappings/clients/{client}": { + "get": { + "tags": [ + "Client Role Mappings" + ], + "summary": "Get client-level role mappings for the user, and the app", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Client Role Mappings" + ], + "summary": "Add client-level roles to the user role mapping", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Client Role Mappings" + ], + "summary": "Delete client-level roles from user role mapping", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "group-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/groups/{group-id}/role-mappings/clients/{client}/available": { + "get": { + "tags": [ + "Client Role Mappings" + ], + "summary": "Get available client-level roles that can be mapped to the user", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "group-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/groups/{group-id}/role-mappings/clients/{client}/composite": { + "get": { + "tags": [ + "Client Role Mappings" + ], + "summary": "Get effective client-level role mappings This recurses any composite roles", + "parameters": [ + { + "name": "briefRepresentation", + "in": "query", + "description": "if false, return roles with their attributes", + "schema": { + "default": true, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "group-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/groups/{group-id}/role-mappings/realm": { + "get": { + "tags": [ + "Role Mapper" + ], + "summary": "Get realm-level role mappings", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Role Mapper" + ], + "summary": "Add realm-level role mappings to the user", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Role Mapper" + ], + "summary": "Delete realm-level role mappings", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "group-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/groups/{group-id}/role-mappings/realm/available": { + "get": { + "tags": [ + "Role Mapper" + ], + "summary": "Get realm-level roles that can be mapped", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "group-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/groups/{group-id}/role-mappings/realm/composite": { + "get": { + "tags": [ + "Role Mapper" + ], + "summary": "Get effective realm-level role mappings This will recurse all composite roles to get the result.", + "parameters": [ + { + "name": "briefRepresentation", + "in": "query", + "description": "if false, return roles with their attributes", + "schema": { + "default": true, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "group-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/identity-provider/import-config": { + "post": { + "tags": [ + "Identity Providers" + ], + "summary": "Import identity provider from JSON body", + "description": "Import identity provider from uploaded JSON file", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/identity-provider/instances": { + "get": { + "tags": [ + "Identity Providers" + ], + "summary": "List identity providers", + "parameters": [ + { + "name": "briefRepresentation", + "in": "query", + "description": "Boolean which defines whether brief representations are returned (default: false)", + "schema": { + "type": "boolean" + } + }, + { + "name": "first", + "in": "query", + "description": "Pagination offset", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "max", + "in": "query", + "description": "Maximum results size (defaults to 100)", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "search", + "in": "query", + "description": "Filter specific providers by name. Search can be prefix (name*), contains (*name*) or exact (\"name\"). Default prefixed.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IdentityProviderRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Identity Providers" + ], + "summary": "Create a new identity provider", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdentityProviderRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/identity-provider/instances/{alias}": { + "get": { + "tags": [ + "Identity Providers" + ], + "summary": "Get the identity provider", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdentityProviderRepresentation" + } + } + } + } + } + }, + "put": { + "tags": [ + "Identity Providers" + ], + "summary": "Update the identity provider", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdentityProviderRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "Identity Providers" + ], + "summary": "Delete the identity provider", + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "alias", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/identity-provider/instances/{alias}/export": { + "get": { + "tags": [ + "Identity Providers" + ], + "summary": "Export public broker configuration for identity provider", + "parameters": [ + { + "name": "format", + "in": "query", + "description": "Format to use", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "alias", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/identity-provider/instances/{alias}/management/permissions": { + "get": { + "tags": [ + "Identity Providers" + ], + "summary": "Return object stating whether client Authorization permissions have been initialized or not and a reference", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementPermissionReference" + } + } + } + } + } + }, + "put": { + "tags": [ + "Identity Providers" + ], + "summary": "Return object stating whether client Authorization permissions have been initialized or not and a reference", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementPermissionReference" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementPermissionReference" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "alias", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/identity-provider/instances/{alias}/mapper-types": { + "get": { + "tags": [ + "Identity Providers" + ], + "summary": "Get mapper types for identity provider", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/IdentityProviderMapperTypeRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "alias", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/identity-provider/instances/{alias}/mappers": { + "get": { + "tags": [ + "Identity Providers" + ], + "summary": "Get mappers for identity provider", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IdentityProviderMapperRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Identity Providers" + ], + "summary": "Add a mapper to identity provider", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdentityProviderMapperRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "alias", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/identity-provider/instances/{alias}/mappers/{id}": { + "get": { + "tags": [ + "Identity Providers" + ], + "summary": "Get mapper by id for the identity provider", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdentityProviderMapperRepresentation" + } + } + } + } + } + }, + "put": { + "tags": [ + "Identity Providers" + ], + "summary": "Update a mapper for the identity provider", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Mapper id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IdentityProviderMapperRepresentation" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Identity Providers" + ], + "summary": "Delete a mapper for the identity provider", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Mapper id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "alias", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/identity-provider/instances/{alias}/reload-keys": { + "get": { + "tags": [ + "Identity Providers" + ], + "summary": "Reaload keys for the identity provider if the provider supports it, \"true\" is returned if reload was performed, \"false\" if not.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "alias", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/identity-provider/providers/{provider_id}": { + "get": { + "tags": [ + "Identity Providers" + ], + "summary": "Get the identity provider factory for that provider id", + "parameters": [ + { + "name": "provider_id", + "in": "path", + "description": "The provider id to get the factory", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/keys": { + "get": { + "tags": [ + "Key" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeysMetadataRepresentation" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/localization": { + "get": { + "tags": [ + "Realms Admin" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/localization/{locale}": { + "get": { + "tags": [ + "Realms Admin" + ], + "parameters": [ + { + "name": "locale", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "useRealmDefaultLocaleFallback", + "in": "query", + "schema": { + "type": "boolean" + }, + "deprecated": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Realms Admin" + ], + "summary": "Import localization from uploaded JSON file", + "parameters": [ + { + "name": "locale", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Realms Admin" + ], + "parameters": [ + { + "name": "locale", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/localization/{locale}/{key}": { + "get": { + "tags": [ + "Realms Admin" + ], + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "locale", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "put": { + "tags": [ + "Realms Admin" + ], + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "locale", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Realms Admin" + ], + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "locale", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/logout-all": { + "post": { + "tags": [ + "Realms Admin" + ], + "summary": "Removes all user sessions.", + "description": "Any client that has an admin url will also be told to invalidate any sessions they have.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GlobalRequestResult" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/partial-export": { + "post": { + "tags": [ + "Realms Admin" + ], + "summary": "Partial export of existing realm into a JSON file.", + "parameters": [ + { + "name": "exportClients", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "exportGroupsAndRoles", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/partialImport": { + "post": { + "tags": [ + "Realms Admin" + ], + "summary": "Partial import from a JSON file to an existing realm.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "format": "binary", + "type": "string" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/push-revocation": { + "post": { + "tags": [ + "Realms Admin" + ], + "summary": "Push the realm's revocation policy to any client that has an admin url associated with it.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GlobalRequestResult" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/roles": { + "get": { + "tags": [ + "Roles" + ], + "summary": "Get all roles for the realm or client", + "parameters": [ + { + "name": "briefRepresentation", + "in": "query", + "schema": { + "default": true, + "type": "boolean" + } + }, + { + "name": "first", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "max", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "search", + "in": "query", + "schema": { + "default": "", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Roles" + ], + "summary": "Create a new role for the realm or client", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/roles-by-id/{role-id}": { + "get": { + "tags": [ + "Roles (by ID)" + ], + "summary": "Get a specific role's representation", + "parameters": [ + { + "name": "role-id", + "in": "path", + "description": "id of role", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + }, + "put": { + "tags": [ + "Roles (by ID)" + ], + "summary": "Update the role", + "parameters": [ + { + "name": "role-id", + "in": "path", + "description": "id of role", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Roles (by ID)" + ], + "summary": "Delete the role", + "parameters": [ + { + "name": "role-id", + "in": "path", + "description": "id of role", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/roles-by-id/{role-id}/composites": { + "get": { + "tags": [ + "Roles (by ID)" + ], + "summary": "Get role's children Returns a set of role's children provided the role is a composite.", + "parameters": [ + { + "name": "role-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "first", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "max", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Roles (by ID)" + ], + "summary": "Make the role a composite role by associating some child roles", + "parameters": [ + { + "name": "role-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Roles (by ID)" + ], + "summary": "Remove a set of roles from the role's composite", + "parameters": [ + { + "name": "role-id", + "in": "path", + "description": "Role id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/roles-by-id/{role-id}/composites/clients/{clientUuid}": { + "get": { + "tags": [ + "Roles (by ID)" + ], + "summary": "Get client-level roles for the client that are in the role's composite", + "parameters": [ + { + "name": "clientUuid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "role-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/roles-by-id/{role-id}/composites/realm": { + "get": { + "tags": [ + "Roles (by ID)" + ], + "summary": "Get realm-level roles that are in the role's composite", + "parameters": [ + { + "name": "role-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/roles-by-id/{role-id}/management/permissions": { + "get": { + "tags": [ + "Roles (by ID)" + ], + "summary": "Return object stating whether role Authorization permissions have been initialized or not and a reference", + "parameters": [ + { + "name": "role-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementPermissionReference" + } + } + } + } + } + }, + "put": { + "tags": [ + "Roles (by ID)" + ], + "summary": "Return object stating whether role Authorization permissions have been initialized or not and a reference", + "parameters": [ + { + "name": "role-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementPermissionReference" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementPermissionReference" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/roles/{role-name}": { + "get": { + "tags": [ + "Roles" + ], + "summary": "Get a role by name", + "parameters": [ + { + "name": "role-name", + "in": "path", + "description": "role's name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + }, + "put": { + "tags": [ + "Roles" + ], + "summary": "Update a role by name", + "parameters": [ + { + "name": "role-name", + "in": "path", + "description": "role's name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "Roles" + ], + "summary": "Delete a role by name", + "parameters": [ + { + "name": "role-name", + "in": "path", + "description": "role's name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/roles/{role-name}/composites": { + "get": { + "tags": [ + "Roles" + ], + "summary": "Get composites of the role", + "parameters": [ + { + "name": "role-name", + "in": "path", + "description": "role's name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Roles" + ], + "summary": "Add a composite to the role", + "parameters": [ + { + "name": "role-name", + "in": "path", + "description": "role's name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Roles" + ], + "summary": "Remove roles from the role's composite", + "parameters": [ + { + "name": "role-name", + "in": "path", + "description": "role's name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/roles/{role-name}/composites/clients/{client-uuid}": { + "get": { + "tags": [ + "Roles" + ], + "summary": "Get client-level roles for the client that are in the role's composite", + "parameters": [ + { + "name": "client-uuid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "role-name", + "in": "path", + "description": "role's name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/roles/{role-name}/composites/realm": { + "get": { + "tags": [ + "Roles" + ], + "summary": "Get realm-level roles of the role's composite", + "parameters": [ + { + "name": "role-name", + "in": "path", + "description": "role's name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/roles/{role-name}/groups": { + "get": { + "tags": [ + "Roles" + ], + "summary": "Returns a stream of groups that have the specified role name", + "parameters": [ + { + "name": "role-name", + "in": "path", + "description": "the role name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "briefRepresentation", + "in": "query", + "description": "if false, return a full representation of the {@code GroupRepresentation} objects.", + "schema": { + "default": true, + "type": "boolean" + } + }, + { + "name": "first", + "in": "query", + "description": "first result to return. Ignored if negative or {@code null}.", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "max", + "in": "query", + "description": "maximum number of results to return. Ignored if negative or {@code null}.", + "schema": { + "format": "int32", + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/roles/{role-name}/management/permissions": { + "get": { + "tags": [ + "Roles" + ], + "summary": "Return object stating whether role Authorization permissions have been initialized or not and a reference", + "parameters": [ + { + "name": "role-name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementPermissionReference" + } + } + } + } + } + }, + "put": { + "tags": [ + "Roles" + ], + "summary": "Return object stating whether role Authorization permissions have been initialized or not and a reference", + "parameters": [ + { + "name": "role-name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementPermissionReference" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementPermissionReference" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/roles/{role-name}/users": { + "get": { + "tags": [ + "Roles" + ], + "summary": "Returns a stream of users that have the specified role name.", + "parameters": [ + { + "name": "role-name", + "in": "path", + "description": "the role name.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "first", + "in": "query", + "description": "first result to return. Ignored if negative or {@code null}.", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "max", + "in": "query", + "description": "maximum number of results to return. Ignored if negative or {@code null}.", + "schema": { + "format": "int32", + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/sessions/{session}": { + "delete": { + "tags": [ + "Realms Admin" + ], + "summary": "Remove a specific user session.", + "description": "Any client that has an admin url will also be told to invalidate this particular session.", + "parameters": [ + { + "name": "session", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "isOffline", + "in": "query", + "schema": { + "default": false, + "type": "boolean" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/testSMTPConnection": { + "post": { + "tags": [ + "Realms Admin" + ], + "summary": "Test SMTP connection with current logged in user", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "properties": { + "config": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + }, + "deprecated": true + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users": { + "get": { + "tags": [ + "Users" + ], + "summary": "Get users Returns a stream of users, filtered according to query parameters.", + "parameters": [ + { + "name": "briefRepresentation", + "in": "query", + "description": "Boolean which defines whether brief representations are returned (default: false)", + "schema": { + "type": "boolean" + } + }, + { + "name": "email", + "in": "query", + "description": "A String contained in email, or the complete email, if param \"exact\" is true", + "schema": { + "type": "string" + } + }, + { + "name": "emailVerified", + "in": "query", + "description": "whether the email has been verified", + "schema": { + "type": "boolean" + } + }, + { + "name": "enabled", + "in": "query", + "description": "Boolean representing if user is enabled or not", + "schema": { + "type": "boolean" + } + }, + { + "name": "exact", + "in": "query", + "description": "Boolean which defines whether the params \"last\", \"first\", \"email\" and \"username\" must match exactly", + "schema": { + "type": "boolean" + } + }, + { + "name": "first", + "in": "query", + "description": "Pagination offset", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "firstName", + "in": "query", + "description": "A String contained in firstName, or the complete firstName, if param \"exact\" is true", + "schema": { + "type": "string" + } + }, + { + "name": "idpAlias", + "in": "query", + "description": "The alias of an Identity Provider linked to the user", + "schema": { + "type": "string" + } + }, + { + "name": "idpUserId", + "in": "query", + "description": "The userId at an Identity Provider linked to the user", + "schema": { + "type": "string" + } + }, + { + "name": "lastName", + "in": "query", + "description": "A String contained in lastName, or the complete lastName, if param \"exact\" is true", + "schema": { + "type": "string" + } + }, + { + "name": "max", + "in": "query", + "description": "Maximum results size (defaults to 100)", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "q", + "in": "query", + "description": "A query to search for custom attributes, in the format 'key1:value2 key2:value2'", + "schema": { + "type": "string" + } + }, + { + "name": "search", + "in": "query", + "description": "A String contained in username, first or last name, or email. Default search behavior is prefix-based (e.g., foo or foo*). Use *foo* for infix search and \"foo\" for exact search.", + "schema": { + "type": "string" + } + }, + { + "name": "username", + "in": "query", + "description": "A String contained in username, or the complete username, if param \"exact\" is true", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Users" + ], + "summary": "Create a new user Username must be unique.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users-management-permissions": { + "get": { + "tags": [ + "Realms Admin" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementPermissionReference" + } + } + } + } + } + }, + "put": { + "tags": [ + "Realms Admin" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementPermissionReference" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementPermissionReference" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/count": { + "get": { + "tags": [ + "Users" + ], + "summary": "Returns the number of users that match the given criteria.", + "description": "It can be called in three different ways. 1. Don’t specify any criteria and pass {@code null}. The number of all users within that realm will be returned. \u003Cp\u003E 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. \u003Cp\u003E 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.", + "parameters": [ + { + "name": "email", + "in": "query", + "description": "email filter", + "schema": { + "type": "string" + } + }, + { + "name": "emailVerified", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "enabled", + "in": "query", + "description": "Boolean representing if user is enabled or not", + "schema": { + "type": "boolean" + } + }, + { + "name": "firstName", + "in": "query", + "description": "first name filter", + "schema": { + "type": "string" + } + }, + { + "name": "lastName", + "in": "query", + "description": "last name filter", + "schema": { + "type": "string" + } + }, + { + "name": "q", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "search", + "in": "query", + "description": "arbitrary search string for all the fields below. Default search behavior is prefix-based (e.g., foo or foo*). Use *foo* for infix search and \"foo\" for exact search.", + "schema": { + "type": "string" + } + }, + { + "name": "username", + "in": "query", + "description": "username filter", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "format": "int32", + "type": "integer" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/profile": { + "get": { + "tags": [ + "Users" + ], + "description": "Get the configuration for the user profile", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UPConfig" + } + } + } + } + } + }, + "put": { + "tags": [ + "Users" + ], + "description": "Set the configuration for the user profile", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UPConfig" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UPConfig" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/profile/metadata": { + "get": { + "tags": [ + "Users" + ], + "description": "Get the UserProfileMetadata from the configuration", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserProfileMetadata" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}": { + "get": { + "tags": [ + "Users" + ], + "summary": "Get representation of the user", + "parameters": [ + { + "name": "userProfileMetadata", + "in": "query", + "description": "Indicates if the user profile metadata should be added to the response", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserRepresentation" + } + } + } + } + } + }, + "put": { + "tags": [ + "Users" + ], + "summary": "Update the user", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserRepresentation" + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "Users" + ], + "summary": "Delete the user", + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/configured-user-storage-credential-types": { + "get": { + "tags": [ + "Users" + ], + "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", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/consents": { + "get": { + "tags": [ + "Users" + ], + "summary": "Get consents granted by the user", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + + } + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/consents/{client}": { + "delete": { + "tags": [ + "Users" + ], + "summary": "Revoke consent and offline tokens for particular client from user", + "parameters": [ + { + "name": "client", + "in": "path", + "description": "Client id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/credentials": { + "get": { + "tags": [ + "Users" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CredentialRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/credentials/{credentialId}": { + "delete": { + "tags": [ + "Users" + ], + "summary": "Remove a credential for a user", + "parameters": [ + { + "name": "credentialId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/credentials/{credentialId}/moveAfter/{newPreviousCredentialId}": { + "post": { + "tags": [ + "Users" + ], + "summary": "Move a credential to a position behind another credential", + "parameters": [ + { + "name": "credentialId", + "in": "path", + "description": "The credential to move", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "newPreviousCredentialId", + "in": "path", + "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.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/credentials/{credentialId}/moveToFirst": { + "post": { + "tags": [ + "Users" + ], + "summary": "Move a credential to a first position in the credentials list of the user", + "parameters": [ + { + "name": "credentialId", + "in": "path", + "description": "The credential to move", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/credentials/{credentialId}/userLabel": { + "put": { + "tags": [ + "Users" + ], + "summary": "Update a credential label for a user", + "parameters": [ + { + "name": "credentialId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/disable-credential-types": { + "put": { + "tags": [ + "Users" + ], + "summary": "Disable all credentials for a user of a specific type", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/execute-actions-email": { + "put": { + "tags": [ + "Users" + ], + "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.", + "parameters": [ + { + "name": "client_id", + "in": "query", + "description": "Client id", + "schema": { + "type": "string" + } + }, + { + "name": "lifespan", + "in": "query", + "description": "Number of seconds after which the generated token expires", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "redirect_uri", + "in": "query", + "description": "Redirect uri", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/federated-identity": { + "get": { + "tags": [ + "Users" + ], + "summary": "Get social logins associated with the user", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FederatedIdentityRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/federated-identity/{provider}": { + "post": { + "tags": [ + "Users" + ], + "summary": "Add a social login provider to the user", + "parameters": [ + { + "name": "provider", + "in": "path", + "description": "Social login provider id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "tags": [ + "Users" + ], + "summary": "Remove a social login provider from user", + "parameters": [ + { + "name": "provider", + "in": "path", + "description": "Social login provider id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/groups": { + "get": { + "tags": [ + "Users" + ], + "parameters": [ + { + "name": "briefRepresentation", + "in": "query", + "schema": { + "default": true, + "type": "boolean" + } + }, + { + "name": "first", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "max", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/groups/count": { + "get": { + "tags": [ + "Users" + ], + "parameters": [ + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "format": "int64", + "type": "integer" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/groups/{groupId}": { + "put": { + "tags": [ + "Users" + ], + "parameters": [ + { + "name": "groupId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Users" + ], + "parameters": [ + { + "name": "groupId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/impersonation": { + "post": { + "tags": [ + "Users" + ], + "summary": "Impersonate the user", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/logout": { + "post": { + "tags": [ + "Users" + ], + "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.", + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/offline-sessions/{clientUuid}": { + "get": { + "tags": [ + "Users" + ], + "summary": "Get offline sessions associated with the user and client", + "parameters": [ + { + "name": "clientUuid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserSessionRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/reset-password": { + "put": { + "tags": [ + "Users" + ], + "summary": "Set up a new password for the user.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CredentialRepresentation" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/reset-password-email": { + "put": { + "tags": [ + "Users" + ], + "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.", + "parameters": [ + { + "name": "client_id", + "in": "query", + "description": "client id", + "schema": { + "type": "string" + } + }, + { + "name": "redirect_uri", + "in": "query", + "description": "redirect uri", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + }, + "deprecated": true + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/role-mappings": { + "get": { + "tags": [ + "Role Mapper" + ], + "summary": "Get role mappings", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MappingsRepresentation" + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/role-mappings/clients/{client}": { + "get": { + "tags": [ + "Client Role Mappings" + ], + "summary": "Get client-level role mappings for the user, and the app", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Client Role Mappings" + ], + "summary": "Add client-level roles to the user role mapping", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Client Role Mappings" + ], + "summary": "Delete client-level roles from user role mapping", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/role-mappings/clients/{client}/available": { + "get": { + "tags": [ + "Client Role Mappings" + ], + "summary": "Get available client-level roles that can be mapped to the user", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/role-mappings/clients/{client}/composite": { + "get": { + "tags": [ + "Client Role Mappings" + ], + "summary": "Get effective client-level role mappings This recurses any composite roles", + "parameters": [ + { + "name": "briefRepresentation", + "in": "query", + "description": "if false, return roles with their attributes", + "schema": { + "default": true, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "client", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/role-mappings/realm": { + "get": { + "tags": [ + "Role Mapper" + ], + "summary": "Get realm-level role mappings", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Role Mapper" + ], + "summary": "Add realm-level role mappings to the user", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Role Mapper" + ], + "summary": "Delete realm-level role mappings", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/role-mappings/realm/available": { + "get": { + "tags": [ + "Role Mapper" + ], + "summary": "Get realm-level roles that can be mapped", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/role-mappings/realm/composite": { + "get": { + "tags": [ + "Role Mapper" + ], + "summary": "Get effective realm-level role mappings This will recurse all composite roles to get the result.", + "parameters": [ + { + "name": "briefRepresentation", + "in": "query", + "description": "if false, return roles with their attributes", + "schema": { + "default": true, + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/send-verify-email": { + "put": { + "tags": [ + "Users" + ], + "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, clientId and lifespan parameters are optional. The default for the redirect is the account client. The default for the lifespan is 12 hours", + "parameters": [ + { + "name": "client_id", + "in": "query", + "description": "Client id", + "schema": { + "type": "string" + } + }, + { + "name": "lifespan", + "in": "query", + "description": "Number of seconds after which the generated token expires", + "schema": { + "format": "int32", + "type": "integer" + } + }, + { + "name": "redirect_uri", + "in": "query", + "description": "Redirect uri", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/admin/realms/{realm}/users/{user-id}/sessions": { + "get": { + "tags": [ + "Users" + ], + "summary": "Get sessions associated with the user", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserSessionRepresentation" + } + } + } + } + } + } + }, + "parameters": [ + { + "name": "realm", + "in": "path", + "description": "realm name (not id!)", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "user-id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "components": { + "schemas": { + "AbstractPolicyRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string" + }, + "policies": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + }, + "resources": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + }, + "scopes": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + }, + "logic": { + "$ref": "#/components/schemas/Logic" + }, + "decisionStrategy": { + "$ref": "#/components/schemas/DecisionStrategy" + }, + "owner": { + "type": "string" + }, + "resourcesData": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceRepresentation" + } + }, + "scopesData": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/ScopeRepresentation" + } + } + } + }, + "Access": { + "type": "object", + "properties": { + "roles": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + }, + "verify_caller": { + "type": "boolean" + } + } + }, + "AccessToken": { + "type": "object", + "properties": { + "jti": { + "type": "string" + }, + "exp": { + "format": "int64", + "type": "integer" + }, + "nbf": { + "format": "int64", + "type": "integer" + }, + "iat": { + "format": "int64", + "type": "integer" + }, + "iss": { + "type": "string" + }, + "sub": { + "type": "string" + }, + "typ": { + "type": "string" + }, + "azp": { + "type": "string" + }, + "otherClaims": { + "type": "object", + "additionalProperties": { + + } + }, + "nonce": { + "type": "string" + }, + "auth_time": { + "format": "int64", + "type": "integer" + }, + "session_state": { + "type": "string" + }, + "at_hash": { + "type": "string" + }, + "c_hash": { + "type": "string" + }, + "name": { + "type": "string" + }, + "given_name": { + "type": "string" + }, + "family_name": { + "type": "string" + }, + "middle_name": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "preferred_username": { + "type": "string" + }, + "profile": { + "type": "string" + }, + "picture": { + "type": "string" + }, + "website": { + "type": "string" + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "gender": { + "type": "string" + }, + "birthdate": { + "type": "string" + }, + "zoneinfo": { + "type": "string" + }, + "locale": { + "type": "string" + }, + "phone_number": { + "type": "string" + }, + "phone_number_verified": { + "type": "boolean" + }, + "address": { + "$ref": "#/components/schemas/AddressClaimSet" + }, + "updated_at": { + "format": "int64", + "type": "integer" + }, + "claims_locales": { + "type": "string" + }, + "acr": { + "type": "string" + }, + "s_hash": { + "type": "string" + }, + "authTime": { + "format": "int32", + "type": "integer", + "writeOnly": true, + "deprecated": true + }, + "sid": { + "type": "string" + }, + "trusted-certs": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + }, + "allowed-origins": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + }, + "realm_access": { + "$ref": "#/components/schemas/Access" + }, + "resource_access": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Access" + }, + "writeOnly": true + }, + "authorization": { + "$ref": "#/components/schemas/Authorization" + }, + "cnf": { + "$ref": "#/components/schemas/Confirmation" + }, + "scope": { + "type": "string" + } + } + }, + "AddressClaimSet": { + "type": "object", + "properties": { + "formatted": { + "type": "string" + }, + "street_address": { + "type": "string" + }, + "locality": { + "type": "string" + }, + "region": { + "type": "string" + }, + "postal_code": { + "type": "string" + }, + "country": { + "type": "string" + } + } + }, + "AdminEventRepresentation": { + "type": "object", + "properties": { + "time": { + "format": "int64", + "type": "integer" + }, + "realmId": { + "type": "string" + }, + "authDetails": { + "$ref": "#/components/schemas/AuthDetailsRepresentation" + }, + "operationType": { + "type": "string" + }, + "resourceType": { + "type": "string" + }, + "resourcePath": { + "type": "string" + }, + "representation": { + "type": "string" + }, + "error": { + "type": "string" + } + } + }, + "ApplicationRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "description": { + "type": "string" + }, + "rootUrl": { + "type": "string" + }, + "adminUrl": { + "type": "string" + }, + "baseUrl": { + "type": "string" + }, + "surrogateAuthRequired": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "alwaysDisplayInConsole": { + "type": "boolean" + }, + "clientAuthenticatorType": { + "type": "string" + }, + "secret": { + "type": "string" + }, + "registrationAccessToken": { + "type": "string" + }, + "defaultRoles": { + "type": "array", + "items": { + "type": "string" + }, + "deprecated": true + }, + "redirectUris": { + "type": "array", + "items": { + "type": "string" + } + }, + "webOrigins": { + "type": "array", + "items": { + "type": "string" + } + }, + "notBefore": { + "format": "int32", + "type": "integer" + }, + "bearerOnly": { + "type": "boolean" + }, + "consentRequired": { + "type": "boolean" + }, + "standardFlowEnabled": { + "type": "boolean" + }, + "implicitFlowEnabled": { + "type": "boolean" + }, + "directAccessGrantsEnabled": { + "type": "boolean" + }, + "serviceAccountsEnabled": { + "type": "boolean" + }, + "authorizationServicesEnabled": { + "type": "boolean" + }, + "directGrantsOnly": { + "type": "boolean", + "deprecated": true + }, + "publicClient": { + "type": "boolean" + }, + "frontchannelLogout": { + "type": "boolean" + }, + "protocol": { + "type": "string" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "authenticationFlowBindingOverrides": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "fullScopeAllowed": { + "type": "boolean" + }, + "nodeReRegistrationTimeout": { + "format": "int32", + "type": "integer" + }, + "registeredNodes": { + "type": "object", + "additionalProperties": { + "format": "int32", + "type": "integer" + } + }, + "protocolMappers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + }, + "clientTemplate": { + "type": "string", + "deprecated": true + }, + "useTemplateConfig": { + "type": "boolean", + "deprecated": true + }, + "useTemplateScope": { + "type": "boolean", + "deprecated": true + }, + "useTemplateMappers": { + "type": "boolean", + "deprecated": true + }, + "defaultClientScopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "optionalClientScopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "authorizationSettings": { + "$ref": "#/components/schemas/ResourceServerRepresentation" + }, + "access": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + }, + "origin": { + "type": "string" + }, + "name": { + "type": "string" + }, + "claims": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ClaimRepresentation" + } + ], + "deprecated": true + } + }, + "deprecated": true + }, + "AuthDetailsRepresentation": { + "type": "object", + "properties": { + "realmId": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "ipAddress": { + "type": "string" + } + } + }, + "AuthenticationExecutionExportRepresentation": { + "type": "object", + "properties": { + "authenticatorConfig": { + "type": "string" + }, + "authenticator": { + "type": "string" + }, + "authenticatorFlow": { + "type": "boolean" + }, + "requirement": { + "type": "string" + }, + "priority": { + "format": "int32", + "type": "integer" + }, + "autheticatorFlow": { + "type": "boolean", + "deprecated": true + }, + "flowAlias": { + "type": "string" + }, + "userSetupAllowed": { + "type": "boolean" + } + } + }, + "AuthenticationExecutionInfoRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "requirement": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "alias": { + "type": "string" + }, + "description": { + "type": "string" + }, + "requirementChoices": { + "type": "array", + "items": { + "type": "string" + } + }, + "configurable": { + "type": "boolean" + }, + "authenticationFlow": { + "type": "boolean" + }, + "providerId": { + "type": "string" + }, + "authenticationConfig": { + "type": "string" + }, + "flowId": { + "type": "string" + }, + "level": { + "format": "int32", + "type": "integer" + }, + "index": { + "format": "int32", + "type": "integer" + } + } + }, + "AuthenticationExecutionRepresentation": { + "type": "object", + "properties": { + "authenticatorConfig": { + "type": "string" + }, + "authenticator": { + "type": "string" + }, + "authenticatorFlow": { + "type": "boolean" + }, + "requirement": { + "type": "string" + }, + "priority": { + "format": "int32", + "type": "integer" + }, + "autheticatorFlow": { + "type": "boolean", + "deprecated": true + }, + "id": { + "type": "string" + }, + "flowId": { + "type": "string" + }, + "parentFlow": { + "type": "string" + } + } + }, + "AuthenticationFlowRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "alias": { + "type": "string" + }, + "description": { + "type": "string" + }, + "providerId": { + "type": "string" + }, + "topLevel": { + "type": "boolean" + }, + "builtIn": { + "type": "boolean" + }, + "authenticationExecutions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AuthenticationExecutionExportRepresentation" + } + } + } + }, + "AuthenticatorConfigInfoRepresentation": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "providerId": { + "type": "string" + }, + "helpText": { + "type": "string" + }, + "properties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConfigPropertyRepresentation" + } + } + } + }, + "AuthenticatorConfigRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "alias": { + "type": "string" + }, + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "Authorization": { + "type": "object", + "properties": { + "permissions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Permission" + } + } + } + }, + "CertificateRepresentation": { + "type": "object", + "properties": { + "privateKey": { + "type": "string" + }, + "publicKey": { + "type": "string" + }, + "certificate": { + "type": "string" + }, + "kid": { + "type": "string" + } + } + }, + "ClaimRepresentation": { + "type": "object", + "properties": { + "name": { + "type": "boolean" + }, + "username": { + "type": "boolean" + }, + "profile": { + "type": "boolean" + }, + "picture": { + "type": "boolean" + }, + "website": { + "type": "boolean" + }, + "email": { + "type": "boolean" + }, + "gender": { + "type": "boolean" + }, + "locale": { + "type": "boolean" + }, + "address": { + "type": "boolean" + }, + "phone": { + "type": "boolean" + } + } + }, + "ClientInitialAccessCreatePresentation": { + "type": "object", + "properties": { + "expiration": { + "format": "int32", + "type": "integer" + }, + "count": { + "format": "int32", + "type": "integer" + } + } + }, + "ClientInitialAccessPresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "token": { + "type": "string" + }, + "timestamp": { + "format": "int32", + "type": "integer" + }, + "expiration": { + "format": "int32", + "type": "integer" + }, + "count": { + "format": "int32", + "type": "integer" + }, + "remainingCount": { + "format": "int32", + "type": "integer" + } + } + }, + "ClientMappingsRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "client": { + "type": "string" + }, + "mappings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + } + }, + "ClientPoliciesRepresentation": { + "type": "object", + "properties": { + "policies": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ClientPolicyRepresentation" + } + } + } + }, + "ClientPolicyConditionRepresentation": { + "type": "object", + "properties": { + "condition": { + "type": "string" + }, + "configuration": { + "type": "array" + } + } + }, + "ClientPolicyExecutorRepresentation": { + "type": "object", + "properties": { + "executor": { + "type": "string" + }, + "configuration": { + "type": "array" + } + } + }, + "ClientPolicyRepresentation": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ClientPolicyConditionRepresentation" + } + }, + "profiles": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "ClientProfileRepresentation": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "executors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ClientPolicyExecutorRepresentation" + } + } + } + }, + "ClientProfilesRepresentation": { + "type": "object", + "properties": { + "profiles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ClientProfileRepresentation" + } + }, + "globalProfiles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ClientProfileRepresentation" + } + } + } + }, + "ClientRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "rootUrl": { + "type": "string" + }, + "adminUrl": { + "type": "string" + }, + "baseUrl": { + "type": "string" + }, + "surrogateAuthRequired": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "alwaysDisplayInConsole": { + "type": "boolean" + }, + "clientAuthenticatorType": { + "type": "string" + }, + "secret": { + "type": "string" + }, + "registrationAccessToken": { + "type": "string" + }, + "defaultRoles": { + "type": "array", + "items": { + "type": "string" + }, + "deprecated": true + }, + "redirectUris": { + "type": "array", + "items": { + "type": "string" + } + }, + "webOrigins": { + "type": "array", + "items": { + "type": "string" + } + }, + "notBefore": { + "format": "int32", + "type": "integer" + }, + "bearerOnly": { + "type": "boolean" + }, + "consentRequired": { + "type": "boolean" + }, + "standardFlowEnabled": { + "type": "boolean" + }, + "implicitFlowEnabled": { + "type": "boolean" + }, + "directAccessGrantsEnabled": { + "type": "boolean" + }, + "serviceAccountsEnabled": { + "type": "boolean" + }, + "authorizationServicesEnabled": { + "type": "boolean" + }, + "directGrantsOnly": { + "type": "boolean", + "deprecated": true + }, + "publicClient": { + "type": "boolean" + }, + "frontchannelLogout": { + "type": "boolean" + }, + "protocol": { + "type": "string" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "authenticationFlowBindingOverrides": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "fullScopeAllowed": { + "type": "boolean" + }, + "nodeReRegistrationTimeout": { + "format": "int32", + "type": "integer" + }, + "registeredNodes": { + "type": "object", + "additionalProperties": { + "format": "int32", + "type": "integer" + } + }, + "protocolMappers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + }, + "clientTemplate": { + "type": "string", + "deprecated": true + }, + "useTemplateConfig": { + "type": "boolean", + "deprecated": true + }, + "useTemplateScope": { + "type": "boolean", + "deprecated": true + }, + "useTemplateMappers": { + "type": "boolean", + "deprecated": true + }, + "defaultClientScopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "optionalClientScopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "authorizationSettings": { + "$ref": "#/components/schemas/ResourceServerRepresentation" + }, + "access": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + }, + "origin": { + "type": "string" + } + } + }, + "ClientScopeRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "protocolMappers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + } + } + }, + "ClientTemplateRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "fullScopeAllowed": { + "type": "boolean" + }, + "bearerOnly": { + "type": "boolean" + }, + "consentRequired": { + "type": "boolean" + }, + "standardFlowEnabled": { + "type": "boolean" + }, + "implicitFlowEnabled": { + "type": "boolean" + }, + "directAccessGrantsEnabled": { + "type": "boolean" + }, + "serviceAccountsEnabled": { + "type": "boolean" + }, + "publicClient": { + "type": "boolean" + }, + "frontchannelLogout": { + "type": "boolean" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "protocolMappers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + } + }, + "deprecated": true + }, + "ComponentExportRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "providerId": { + "type": "string" + }, + "subType": { + "type": "string" + }, + "subComponents": { + "$ref": "#/components/schemas/MultivaluedHashMapStringComponentExportRepresentation" + }, + "config": { + "$ref": "#/components/schemas/MultivaluedHashMapStringString" + } + } + }, + "ComponentRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "providerId": { + "type": "string" + }, + "providerType": { + "type": "string" + }, + "parentId": { + "type": "string" + }, + "subType": { + "type": "string" + }, + "config": { + "$ref": "#/components/schemas/MultivaluedHashMapStringString" + } + } + }, + "ComponentTypeRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "helpText": { + "type": "string" + }, + "properties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConfigPropertyRepresentation" + } + }, + "metadata": { + "type": "object", + "additionalProperties": { + + } + } + } + }, + "Composites": { + "type": "object", + "properties": { + "realm": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + }, + "client": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "application": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + }, + "deprecated": true + } + } + }, + "ConfigPropertyRepresentation": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "label": { + "type": "string" + }, + "helpText": { + "type": "string" + }, + "type": { + "type": "string" + }, + "defaultValue": { + + }, + "options": { + "type": "array", + "items": { + "type": "string" + } + }, + "secret": { + "type": "boolean" + }, + "required": { + "type": "boolean" + }, + "readOnly": { + "type": "boolean" + } + } + }, + "Confirmation": { + "type": "object", + "properties": { + "x5t#S256": { + "type": "string" + }, + "jkt": { + "type": "string" + } + } + }, + "CredentialRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "userLabel": { + "type": "string" + }, + "createdDate": { + "format": "int64", + "type": "integer" + }, + "secretData": { + "type": "string" + }, + "credentialData": { + "type": "string" + }, + "priority": { + "format": "int32", + "type": "integer" + }, + "value": { + "type": "string" + }, + "temporary": { + "type": "boolean" + }, + "device": { + "type": "string", + "deprecated": true + }, + "hashedSaltedValue": { + "type": "string", + "deprecated": true + }, + "salt": { + "type": "string", + "deprecated": true + }, + "hashIterations": { + "format": "int32", + "type": "integer", + "deprecated": true + }, + "counter": { + "format": "int32", + "type": "integer", + "deprecated": true + }, + "algorithm": { + "type": "string", + "deprecated": true + }, + "digits": { + "format": "int32", + "type": "integer", + "deprecated": true + }, + "period": { + "format": "int32", + "type": "integer", + "deprecated": true + }, + "config": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MultivaluedHashMapStringString" + } + ], + "deprecated": true + } + } + }, + "DecisionEffect": { + "enum": [ + "PERMIT", + "DENY" + ], + "type": "string" + }, + "DecisionStrategy": { + "enum": [ + "AFFIRMATIVE", + "UNANIMOUS", + "CONSENSUS" + ], + "type": "string" + }, + "EnforcementMode": { + "enum": [ + "PERMISSIVE", + "ENFORCING", + "DISABLED" + ], + "type": "string" + }, + "EvaluationResultRepresentation": { + "type": "object", + "properties": { + "resource": { + "$ref": "#/components/schemas/ResourceRepresentation" + }, + "scopes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScopeRepresentation" + } + }, + "policies": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PolicyResultRepresentation" + } + }, + "status": { + "$ref": "#/components/schemas/DecisionEffect" + }, + "allowedScopes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScopeRepresentation" + } + } + } + }, + "EventRepresentation": { + "type": "object", + "properties": { + "time": { + "format": "int64", + "type": "integer" + }, + "type": { + "type": "string" + }, + "realmId": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "sessionId": { + "type": "string" + }, + "ipAddress": { + "type": "string" + }, + "error": { + "type": "string" + }, + "details": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "FederatedIdentityRepresentation": { + "type": "object", + "properties": { + "identityProvider": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "userName": { + "type": "string" + } + } + }, + "GlobalRequestResult": { + "type": "object", + "properties": { + "successRequests": { + "type": "array", + "items": { + "type": "string" + } + }, + "failedRequests": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "GroupRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "parentId": { + "type": "string" + }, + "subGroupCount": { + "format": "int64", + "type": "integer" + }, + "subGroups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupRepresentation" + } + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "realmRoles": { + "type": "array", + "items": { + "type": "string" + } + }, + "clientRoles": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "access": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + } + } + }, + "IDToken": { + "type": "object", + "properties": { + "jti": { + "type": "string" + }, + "exp": { + "format": "int64", + "type": "integer" + }, + "nbf": { + "format": "int64", + "type": "integer" + }, + "iat": { + "format": "int64", + "type": "integer" + }, + "iss": { + "type": "string" + }, + "sub": { + "type": "string" + }, + "typ": { + "type": "string" + }, + "azp": { + "type": "string" + }, + "otherClaims": { + "type": "object", + "additionalProperties": { + + } + }, + "nonce": { + "type": "string" + }, + "auth_time": { + "format": "int64", + "type": "integer" + }, + "session_state": { + "type": "string" + }, + "at_hash": { + "type": "string" + }, + "c_hash": { + "type": "string" + }, + "name": { + "type": "string" + }, + "given_name": { + "type": "string" + }, + "family_name": { + "type": "string" + }, + "middle_name": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "preferred_username": { + "type": "string" + }, + "profile": { + "type": "string" + }, + "picture": { + "type": "string" + }, + "website": { + "type": "string" + }, + "email": { + "type": "string" + }, + "email_verified": { + "type": "boolean" + }, + "gender": { + "type": "string" + }, + "birthdate": { + "type": "string" + }, + "zoneinfo": { + "type": "string" + }, + "locale": { + "type": "string" + }, + "phone_number": { + "type": "string" + }, + "phone_number_verified": { + "type": "boolean" + }, + "address": { + "$ref": "#/components/schemas/AddressClaimSet" + }, + "updated_at": { + "format": "int64", + "type": "integer" + }, + "claims_locales": { + "type": "string" + }, + "acr": { + "type": "string" + }, + "s_hash": { + "type": "string" + }, + "authTime": { + "format": "int32", + "type": "integer", + "writeOnly": true, + "deprecated": true + }, + "sid": { + "type": "string" + } + } + }, + "IdentityProviderMapperRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "identityProviderAlias": { + "type": "string" + }, + "identityProviderMapper": { + "type": "string" + }, + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "IdentityProviderMapperTypeRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "category": { + "type": "string" + }, + "helpText": { + "type": "string" + }, + "properties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConfigPropertyRepresentation" + } + } + } + }, + "IdentityProviderRepresentation": { + "type": "object", + "properties": { + "alias": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "internalId": { + "type": "string" + }, + "providerId": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "updateProfileFirstLoginMode": { + "type": "string", + "deprecated": true + }, + "trustEmail": { + "type": "boolean" + }, + "storeToken": { + "type": "boolean" + }, + "addReadTokenRoleOnCreate": { + "type": "boolean" + }, + "authenticateByDefault": { + "type": "boolean" + }, + "linkOnly": { + "type": "boolean" + }, + "firstBrokerLoginFlowAlias": { + "type": "string" + }, + "postBrokerLoginFlowAlias": { + "type": "string" + }, + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "updateProfileFirstLogin": { + "type": "boolean", + "deprecated": true + } + } + }, + "InstallationAdapterConfig": { + "type": "object", + "properties": { + "realm": { + "type": "string" + }, + "realm-public-key": { + "type": "string" + }, + "auth-server-url": { + "type": "string" + }, + "ssl-required": { + "type": "string" + }, + "bearer-only": { + "type": "boolean" + }, + "resource": { + "type": "string" + }, + "public-client": { + "type": "boolean" + }, + "verify-token-audience": { + "type": "boolean" + }, + "credentials": { + "type": "object", + "additionalProperties": { + + } + }, + "use-resource-role-mappings": { + "type": "boolean" + }, + "confidential-port": { + "format": "int32", + "type": "integer" + }, + "policy-enforcer": { + "$ref": "#/components/schemas/PolicyEnforcerConfig" + } + } + }, + "KeyMetadataRepresentation": { + "type": "object", + "properties": { + "providerId": { + "type": "string" + }, + "providerPriority": { + "format": "int64", + "type": "integer" + }, + "kid": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + }, + "algorithm": { + "type": "string" + }, + "publicKey": { + "type": "string" + }, + "certificate": { + "type": "string" + }, + "use": { + "$ref": "#/components/schemas/KeyUse" + }, + "validTo": { + "format": "int64", + "type": "integer" + } + } + }, + "KeyStoreConfig": { + "type": "object", + "properties": { + "realmCertificate": { + "type": "boolean" + }, + "storePassword": { + "type": "string" + }, + "keyPassword": { + "type": "string" + }, + "keyAlias": { + "type": "string" + }, + "realmAlias": { + "type": "string" + }, + "format": { + "type": "string" + } + } + }, + "KeyUse": { + "enum": [ + "SIG", + "ENC" + ], + "type": "string" + }, + "KeysMetadataRepresentation": { + "type": "object", + "properties": { + "active": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "keys": { + "type": "array", + "items": { + "$ref": "#/components/schemas/KeyMetadataRepresentation" + } + } + } + }, + "Logic": { + "enum": [ + "POSITIVE", + "NEGATIVE" + ], + "type": "string" + }, + "ManagementPermissionReference": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "resource": { + "type": "string" + }, + "scopePermissions": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "MappingsRepresentation": { + "type": "object", + "properties": { + "realmMappings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + }, + "clientMappings": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ClientMappingsRepresentation" + } + } + } + }, + "MethodConfig": { + "type": "object", + "properties": { + "method": { + "type": "string" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "scopes-enforcement-mode": { + "$ref": "#/components/schemas/ScopeEnforcementMode" + } + } + }, + "MultivaluedHashMapStringComponentExportRepresentation": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ComponentExportRepresentation" + } + } + }, + "MultivaluedHashMapStringString": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "MultivaluedMapStringString": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "OAuthClientRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "description": { + "type": "string" + }, + "rootUrl": { + "type": "string" + }, + "adminUrl": { + "type": "string" + }, + "baseUrl": { + "type": "string" + }, + "surrogateAuthRequired": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "alwaysDisplayInConsole": { + "type": "boolean" + }, + "clientAuthenticatorType": { + "type": "string" + }, + "secret": { + "type": "string" + }, + "registrationAccessToken": { + "type": "string" + }, + "defaultRoles": { + "type": "array", + "items": { + "type": "string" + }, + "deprecated": true + }, + "redirectUris": { + "type": "array", + "items": { + "type": "string" + } + }, + "webOrigins": { + "type": "array", + "items": { + "type": "string" + } + }, + "notBefore": { + "format": "int32", + "type": "integer" + }, + "bearerOnly": { + "type": "boolean" + }, + "consentRequired": { + "type": "boolean" + }, + "standardFlowEnabled": { + "type": "boolean" + }, + "implicitFlowEnabled": { + "type": "boolean" + }, + "directAccessGrantsEnabled": { + "type": "boolean" + }, + "serviceAccountsEnabled": { + "type": "boolean" + }, + "authorizationServicesEnabled": { + "type": "boolean" + }, + "directGrantsOnly": { + "type": "boolean", + "deprecated": true + }, + "publicClient": { + "type": "boolean" + }, + "frontchannelLogout": { + "type": "boolean" + }, + "protocol": { + "type": "string" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "authenticationFlowBindingOverrides": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "fullScopeAllowed": { + "type": "boolean" + }, + "nodeReRegistrationTimeout": { + "format": "int32", + "type": "integer" + }, + "registeredNodes": { + "type": "object", + "additionalProperties": { + "format": "int32", + "type": "integer" + } + }, + "protocolMappers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + }, + "clientTemplate": { + "type": "string", + "deprecated": true + }, + "useTemplateConfig": { + "type": "boolean", + "deprecated": true + }, + "useTemplateScope": { + "type": "boolean", + "deprecated": true + }, + "useTemplateMappers": { + "type": "boolean", + "deprecated": true + }, + "defaultClientScopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "optionalClientScopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "authorizationSettings": { + "$ref": "#/components/schemas/ResourceServerRepresentation" + }, + "access": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + }, + "origin": { + "type": "string" + }, + "name": { + "type": "string" + }, + "claims": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ClaimRepresentation" + } + ], + "deprecated": true + } + }, + "deprecated": true + }, + "PathCacheConfig": { + "type": "object", + "properties": { + "max-entries": { + "format": "int32", + "type": "integer" + }, + "lifespan": { + "format": "int64", + "type": "integer" + } + } + }, + "PathConfig": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "path": { + "type": "string" + }, + "methods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MethodConfig" + } + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "enforcement-mode": { + "$ref": "#/components/schemas/EnforcementMode" + }, + "claim-information-point": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + + } + } + }, + "invalidated": { + "type": "boolean" + }, + "staticPath": { + "type": "boolean" + }, + "static": { + "type": "boolean" + } + } + }, + "PathSegment": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "matrixParameters": { + "$ref": "#/components/schemas/MultivaluedMapStringString" + } + } + }, + "Permission": { + "type": "object", + "properties": { + "rsid": { + "type": "string" + }, + "rsname": { + "type": "string" + }, + "scopes": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + }, + "claims": { + "type": "object", + "additionalProperties": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "PolicyEnforcementMode": { + "enum": [ + "ENFORCING", + "PERMISSIVE", + "DISABLED" + ], + "type": "string" + }, + "PolicyEnforcerConfig": { + "type": "object", + "properties": { + "enforcement-mode": { + "$ref": "#/components/schemas/EnforcementMode" + }, + "paths": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PathConfig" + } + }, + "path-cache": { + "$ref": "#/components/schemas/PathCacheConfig" + }, + "lazy-load-paths": { + "type": "boolean" + }, + "on-deny-redirect-to": { + "type": "string" + }, + "user-managed-access": { + "$ref": "#/components/schemas/UserManagedAccessConfig" + }, + "claim-information-point": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + + } + } + }, + "http-method-as-scope": { + "type": "boolean" + }, + "realm": { + "type": "string" + }, + "auth-server-url": { + "type": "string" + }, + "credentials": { + "type": "object", + "additionalProperties": { + + } + }, + "resource": { + "type": "string" + } + } + }, + "PolicyEvaluationRequest": { + "type": "object", + "properties": { + "context": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "resources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceRepresentation" + } + }, + "clientId": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "roleIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "entitlements": { + "type": "boolean" + } + } + }, + "PolicyEvaluationResponse": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EvaluationResultRepresentation" + } + }, + "entitlements": { + "type": "boolean" + }, + "status": { + "$ref": "#/components/schemas/DecisionEffect" + }, + "rpt": { + "$ref": "#/components/schemas/AccessToken" + } + } + }, + "PolicyProviderRepresentation": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "group": { + "type": "string" + } + } + }, + "PolicyRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string" + }, + "policies": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + }, + "resources": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + }, + "scopes": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + }, + "logic": { + "$ref": "#/components/schemas/Logic" + }, + "decisionStrategy": { + "$ref": "#/components/schemas/DecisionStrategy" + }, + "owner": { + "type": "string" + }, + "resourcesData": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceRepresentation" + } + }, + "scopesData": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/ScopeRepresentation" + } + }, + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "PolicyResultRepresentation": { + "type": "object", + "properties": { + "policy": { + "$ref": "#/components/schemas/PolicyRepresentation" + }, + "status": { + "$ref": "#/components/schemas/DecisionEffect" + }, + "associatedPolicies": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PolicyResultRepresentation" + } + }, + "scopes": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "ProtocolMapperEvaluationRepresentation": { + "type": "object", + "properties": { + "mapperId": { + "type": "string" + }, + "mapperName": { + "type": "string" + }, + "containerId": { + "type": "string" + }, + "containerName": { + "type": "string" + }, + "containerType": { + "type": "string" + }, + "protocolMapper": { + "type": "string" + } + } + }, + "ProtocolMapperRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "protocol": { + "type": "string" + }, + "protocolMapper": { + "type": "string" + }, + "consentRequired": { + "type": "boolean", + "deprecated": true + }, + "consentText": { + "type": "string", + "deprecated": true + }, + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "PublishedRealmRepresentation": { + "type": "object", + "properties": { + "realm": { + "type": "string" + }, + "public_key": { + "type": "string" + }, + "token-service": { + "type": "string" + }, + "account-service": { + "type": "string" + }, + "tokens-not-before": { + "format": "int32", + "type": "integer" + } + } + }, + "RealmEventsConfigRepresentation": { + "type": "object", + "properties": { + "eventsEnabled": { + "type": "boolean" + }, + "eventsExpiration": { + "format": "int64", + "type": "integer" + }, + "eventsListeners": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabledEventTypes": { + "type": "array", + "items": { + "type": "string" + } + }, + "adminEventsEnabled": { + "type": "boolean" + }, + "adminEventsDetailsEnabled": { + "type": "boolean" + } + } + }, + "RealmRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "realm": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "displayNameHtml": { + "type": "string" + }, + "notBefore": { + "format": "int32", + "type": "integer" + }, + "defaultSignatureAlgorithm": { + "type": "string" + }, + "revokeRefreshToken": { + "type": "boolean" + }, + "refreshTokenMaxReuse": { + "format": "int32", + "type": "integer" + }, + "accessTokenLifespan": { + "format": "int32", + "type": "integer" + }, + "accessTokenLifespanForImplicitFlow": { + "format": "int32", + "type": "integer" + }, + "ssoSessionIdleTimeout": { + "format": "int32", + "type": "integer" + }, + "ssoSessionMaxLifespan": { + "format": "int32", + "type": "integer" + }, + "ssoSessionIdleTimeoutRememberMe": { + "format": "int32", + "type": "integer" + }, + "ssoSessionMaxLifespanRememberMe": { + "format": "int32", + "type": "integer" + }, + "offlineSessionIdleTimeout": { + "format": "int32", + "type": "integer" + }, + "offlineSessionMaxLifespanEnabled": { + "type": "boolean" + }, + "offlineSessionMaxLifespan": { + "format": "int32", + "type": "integer" + }, + "clientSessionIdleTimeout": { + "format": "int32", + "type": "integer" + }, + "clientSessionMaxLifespan": { + "format": "int32", + "type": "integer" + }, + "clientOfflineSessionIdleTimeout": { + "format": "int32", + "type": "integer" + }, + "clientOfflineSessionMaxLifespan": { + "format": "int32", + "type": "integer" + }, + "accessCodeLifespan": { + "format": "int32", + "type": "integer" + }, + "accessCodeLifespanUserAction": { + "format": "int32", + "type": "integer" + }, + "accessCodeLifespanLogin": { + "format": "int32", + "type": "integer" + }, + "actionTokenGeneratedByAdminLifespan": { + "format": "int32", + "type": "integer" + }, + "actionTokenGeneratedByUserLifespan": { + "format": "int32", + "type": "integer" + }, + "oauth2DeviceCodeLifespan": { + "format": "int32", + "type": "integer" + }, + "oauth2DevicePollingInterval": { + "format": "int32", + "type": "integer" + }, + "enabled": { + "type": "boolean" + }, + "sslRequired": { + "type": "string" + }, + "passwordCredentialGrantAllowed": { + "type": "boolean", + "deprecated": true + }, + "registrationAllowed": { + "type": "boolean" + }, + "registrationEmailAsUsername": { + "type": "boolean" + }, + "rememberMe": { + "type": "boolean" + }, + "verifyEmail": { + "type": "boolean" + }, + "loginWithEmailAllowed": { + "type": "boolean" + }, + "duplicateEmailsAllowed": { + "type": "boolean" + }, + "resetPasswordAllowed": { + "type": "boolean" + }, + "editUsernameAllowed": { + "type": "boolean" + }, + "userCacheEnabled": { + "type": "boolean", + "deprecated": true + }, + "realmCacheEnabled": { + "type": "boolean", + "deprecated": true + }, + "bruteForceProtected": { + "type": "boolean" + }, + "permanentLockout": { + "type": "boolean" + }, + "maxTemporaryLockouts": { + "format": "int32", + "type": "integer" + }, + "maxFailureWaitSeconds": { + "format": "int32", + "type": "integer" + }, + "minimumQuickLoginWaitSeconds": { + "format": "int32", + "type": "integer" + }, + "waitIncrementSeconds": { + "format": "int32", + "type": "integer" + }, + "quickLoginCheckMilliSeconds": { + "format": "int64", + "type": "integer" + }, + "maxDeltaTimeSeconds": { + "format": "int32", + "type": "integer" + }, + "failureFactor": { + "format": "int32", + "type": "integer" + }, + "privateKey": { + "type": "string", + "deprecated": true + }, + "publicKey": { + "type": "string", + "deprecated": true + }, + "certificate": { + "type": "string", + "deprecated": true + }, + "codeSecret": { + "type": "string", + "deprecated": true + }, + "roles": { + "$ref": "#/components/schemas/RolesRepresentation" + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupRepresentation" + } + }, + "defaultRoles": { + "type": "array", + "items": { + "type": "string" + }, + "deprecated": true + }, + "defaultRole": { + "$ref": "#/components/schemas/RoleRepresentation" + }, + "defaultGroups": { + "type": "array", + "items": { + "type": "string" + } + }, + "requiredCredentials": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + }, + "deprecated": true + }, + "passwordPolicy": { + "type": "string" + }, + "otpPolicyType": { + "type": "string" + }, + "otpPolicyAlgorithm": { + "type": "string" + }, + "otpPolicyInitialCounter": { + "format": "int32", + "type": "integer" + }, + "otpPolicyDigits": { + "format": "int32", + "type": "integer" + }, + "otpPolicyLookAheadWindow": { + "format": "int32", + "type": "integer" + }, + "otpPolicyPeriod": { + "format": "int32", + "type": "integer" + }, + "otpPolicyCodeReusable": { + "type": "boolean" + }, + "otpSupportedApplications": { + "type": "array", + "items": { + "type": "string" + } + }, + "localizationTexts": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "webAuthnPolicyRpEntityName": { + "type": "string" + }, + "webAuthnPolicySignatureAlgorithms": { + "type": "array", + "items": { + "type": "string" + } + }, + "webAuthnPolicyRpId": { + "type": "string" + }, + "webAuthnPolicyAttestationConveyancePreference": { + "type": "string" + }, + "webAuthnPolicyAuthenticatorAttachment": { + "type": "string" + }, + "webAuthnPolicyRequireResidentKey": { + "type": "string" + }, + "webAuthnPolicyUserVerificationRequirement": { + "type": "string" + }, + "webAuthnPolicyCreateTimeout": { + "format": "int32", + "type": "integer" + }, + "webAuthnPolicyAvoidSameAuthenticatorRegister": { + "type": "boolean" + }, + "webAuthnPolicyAcceptableAaguids": { + "type": "array", + "items": { + "type": "string" + } + }, + "webAuthnPolicyExtraOrigins": { + "type": "array", + "items": { + "type": "string" + } + }, + "webAuthnPolicyPasswordlessRpEntityName": { + "type": "string" + }, + "webAuthnPolicyPasswordlessSignatureAlgorithms": { + "type": "array", + "items": { + "type": "string" + } + }, + "webAuthnPolicyPasswordlessRpId": { + "type": "string" + }, + "webAuthnPolicyPasswordlessAttestationConveyancePreference": { + "type": "string" + }, + "webAuthnPolicyPasswordlessAuthenticatorAttachment": { + "type": "string" + }, + "webAuthnPolicyPasswordlessRequireResidentKey": { + "type": "string" + }, + "webAuthnPolicyPasswordlessUserVerificationRequirement": { + "type": "string" + }, + "webAuthnPolicyPasswordlessCreateTimeout": { + "format": "int32", + "type": "integer" + }, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": { + "type": "boolean" + }, + "webAuthnPolicyPasswordlessAcceptableAaguids": { + "type": "array", + "items": { + "type": "string" + } + }, + "webAuthnPolicyPasswordlessExtraOrigins": { + "type": "array", + "items": { + "type": "string" + } + }, + "clientProfiles": { + "$ref": "#/components/schemas/ClientProfilesRepresentation" + }, + "clientPolicies": { + "$ref": "#/components/schemas/ClientPoliciesRepresentation" + }, + "users": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserRepresentation" + } + }, + "federatedUsers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserRepresentation" + } + }, + "scopeMappings": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScopeMappingRepresentation" + } + }, + "clientScopeMappings": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScopeMappingRepresentation" + } + } + }, + "clients": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ClientRepresentation" + } + }, + "clientScopes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ClientScopeRepresentation" + } + }, + "defaultDefaultClientScopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "defaultOptionalClientScopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "browserSecurityHeaders": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "smtpServer": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "userFederationProviders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserFederationProviderRepresentation" + } + }, + "userFederationMappers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserFederationMapperRepresentation" + } + }, + "loginTheme": { + "type": "string" + }, + "accountTheme": { + "type": "string" + }, + "adminTheme": { + "type": "string" + }, + "emailTheme": { + "type": "string" + }, + "eventsEnabled": { + "type": "boolean" + }, + "eventsExpiration": { + "format": "int64", + "type": "integer" + }, + "eventsListeners": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabledEventTypes": { + "type": "array", + "items": { + "type": "string" + } + }, + "adminEventsEnabled": { + "type": "boolean" + }, + "adminEventsDetailsEnabled": { + "type": "boolean" + }, + "identityProviders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IdentityProviderRepresentation" + } + }, + "identityProviderMappers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IdentityProviderMapperRepresentation" + } + }, + "protocolMappers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProtocolMapperRepresentation" + } + }, + "components": { + "$ref": "#/components/schemas/MultivaluedHashMapStringComponentExportRepresentation" + }, + "internationalizationEnabled": { + "type": "boolean" + }, + "supportedLocales": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + }, + "defaultLocale": { + "type": "string" + }, + "authenticationFlows": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AuthenticationFlowRepresentation" + } + }, + "authenticatorConfig": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AuthenticatorConfigRepresentation" + } + }, + "requiredActions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RequiredActionProviderRepresentation" + } + }, + "browserFlow": { + "type": "string" + }, + "registrationFlow": { + "type": "string" + }, + "directGrantFlow": { + "type": "string" + }, + "resetCredentialsFlow": { + "type": "string" + }, + "clientAuthenticationFlow": { + "type": "string" + }, + "dockerAuthenticationFlow": { + "type": "string" + }, + "firstBrokerLoginFlow": { + "type": "string" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "keycloakVersion": { + "type": "string" + }, + "userManagedAccessAllowed": { + "type": "boolean" + }, + "social": { + "type": "boolean", + "deprecated": true + }, + "updateProfileOnInitialSocialLogin": { + "type": "boolean", + "deprecated": true + }, + "socialProviders": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "deprecated": true + }, + "applicationScopeMappings": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScopeMappingRepresentation" + } + }, + "deprecated": true + }, + "applications": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationRepresentation" + }, + "deprecated": true + }, + "oauthClients": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OAuthClientRepresentation" + }, + "deprecated": true + }, + "clientTemplates": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ClientTemplateRepresentation" + }, + "deprecated": true + }, + "oAuth2DeviceCodeLifespan": { + "format": "int32", + "type": "integer" + }, + "oAuth2DevicePollingInterval": { + "format": "int32", + "type": "integer" + } + } + }, + "RequiredActionProviderRepresentation": { + "type": "object", + "properties": { + "alias": { + "type": "string" + }, + "name": { + "type": "string" + }, + "providerId": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "defaultAction": { + "type": "boolean" + }, + "priority": { + "format": "int32", + "type": "integer" + }, + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "ResourceOwnerRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "ResourceRepresentation": { + "type": "object", + "properties": { + "_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "uris": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string" + }, + "scopes": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/ScopeRepresentation" + } + }, + "icon_uri": { + "type": "string" + }, + "owner": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ResourceOwnerRepresentation" + } + ], + "readOnly": true + }, + "ownerManagedAccess": { + "type": "boolean" + }, + "displayName": { + "type": "string" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "uri": { + "type": "string", + "writeOnly": true, + "deprecated": true + }, + "scopesUma": { + "uniqueItems": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/ScopeRepresentation" + } + } + } + }, + "ResourceServerRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "allowRemoteResourceManagement": { + "type": "boolean" + }, + "policyEnforcementMode": { + "$ref": "#/components/schemas/PolicyEnforcementMode" + }, + "resources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceRepresentation" + } + }, + "policies": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PolicyRepresentation" + } + }, + "scopes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScopeRepresentation" + } + }, + "decisionStrategy": { + "$ref": "#/components/schemas/DecisionStrategy" + } + } + }, + "RoleRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "scopeParamRequired": { + "type": "boolean", + "deprecated": true + }, + "composite": { + "type": "boolean" + }, + "composites": { + "$ref": "#/components/schemas/Composites" + }, + "clientRole": { + "type": "boolean" + }, + "containerId": { + "type": "string" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "RolesRepresentation": { + "type": "object", + "properties": { + "realm": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + }, + "client": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + } + }, + "application": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleRepresentation" + } + }, + "deprecated": true + } + } + }, + "ScopeEnforcementMode": { + "enum": [ + "ALL", + "ANY", + "DISABLED" + ], + "type": "string" + }, + "ScopeMappingRepresentation": { + "type": "object", + "properties": { + "self": { + "type": "string" + }, + "client": { + "type": "string" + }, + "clientTemplate": { + "type": "string", + "deprecated": true + }, + "clientScope": { + "type": "string" + }, + "roles": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "ScopeRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "iconUri": { + "type": "string" + }, + "policies": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PolicyRepresentation" + } + }, + "resources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceRepresentation" + } + }, + "displayName": { + "type": "string" + } + } + }, + "SocialLinkRepresentation": { + "type": "object", + "properties": { + "socialProvider": { + "type": "string" + }, + "socialUserId": { + "type": "string" + }, + "socialUsername": { + "type": "string" + } + } + }, + "UPAttribute": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "validations": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + + } + } + }, + "annotations": { + "type": "object", + "additionalProperties": { + + } + }, + "required": { + "$ref": "#/components/schemas/UPAttributeRequired" + }, + "permissions": { + "$ref": "#/components/schemas/UPAttributePermissions" + }, + "selector": { + "$ref": "#/components/schemas/UPAttributeSelector" + }, + "group": { + "type": "string" + }, + "multivalued": { + "type": "boolean" + } + } + }, + "UPAttributePermissions": { + "type": "object", + "properties": { + "view": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + }, + "edit": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "UPAttributeRequired": { + "type": "object", + "properties": { + "roles": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + }, + "scopes": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "UPAttributeSelector": { + "type": "object", + "properties": { + "scopes": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "UPConfig": { + "type": "object", + "properties": { + "attributes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UPAttribute" + } + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UPGroup" + } + }, + "unmanagedAttributePolicy": { + "$ref": "#/components/schemas/UnmanagedAttributePolicy" + } + } + }, + "UPGroup": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "displayHeader": { + "type": "string" + }, + "displayDescription": { + "type": "string" + }, + "annotations": { + "type": "object", + "additionalProperties": { + + } + } + } + }, + "UnmanagedAttributePolicy": { + "enum": [ + "ENABLED", + "ADMIN_VIEW", + "ADMIN_EDIT" + ], + "type": "string" + }, + "UserConsentRepresentation": { + "type": "object", + "properties": { + "clientId": { + "type": "string" + }, + "grantedClientScopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "createdDate": { + "format": "int64", + "type": "integer" + }, + "lastUpdatedDate": { + "format": "int64", + "type": "integer" + }, + "grantedRealmRoles": { + "type": "array", + "items": { + "type": "string" + }, + "deprecated": true + } + } + }, + "UserFederationMapperRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "federationProviderDisplayName": { + "type": "string" + }, + "federationMapperType": { + "type": "string" + }, + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "UserFederationProviderRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "providerName": { + "type": "string" + }, + "config": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "priority": { + "format": "int32", + "type": "integer" + }, + "fullSyncPeriod": { + "format": "int32", + "type": "integer" + }, + "changedSyncPeriod": { + "format": "int32", + "type": "integer" + }, + "lastSync": { + "format": "int32", + "type": "integer" + } + } + }, + "UserManagedAccessConfig": { + "type": "object" + }, + "UserProfileAttributeGroupMetadata": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "displayHeader": { + "type": "string" + }, + "displayDescription": { + "type": "string" + }, + "annotations": { + "type": "object", + "additionalProperties": { + + } + } + } + }, + "UserProfileAttributeMetadata": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "readOnly": { + "type": "boolean" + }, + "annotations": { + "type": "object", + "additionalProperties": { + + } + }, + "validators": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + + } + } + }, + "group": { + "type": "string" + }, + "multivalued": { + "type": "boolean" + } + } + }, + "UserProfileMetadata": { + "type": "object", + "properties": { + "attributes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserProfileAttributeMetadata" + } + }, + "groups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserProfileAttributeGroupMetadata" + } + } + } + }, + "UserRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "emailVerified": { + "type": "boolean" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "userProfileMetadata": { + "$ref": "#/components/schemas/UserProfileMetadata" + }, + "self": { + "type": "string" + }, + "origin": { + "type": "string" + }, + "createdTimestamp": { + "format": "int64", + "type": "integer" + }, + "enabled": { + "type": "boolean" + }, + "totp": { + "type": "boolean" + }, + "federationLink": { + "type": "string" + }, + "serviceAccountClientId": { + "type": "string" + }, + "credentials": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CredentialRepresentation" + } + }, + "disableableCredentialTypes": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + }, + "requiredActions": { + "type": "array", + "items": { + "type": "string" + } + }, + "federatedIdentities": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FederatedIdentityRepresentation" + } + }, + "realmRoles": { + "type": "array", + "items": { + "type": "string" + } + }, + "clientRoles": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "clientConsents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserConsentRepresentation" + } + }, + "notBefore": { + "format": "int32", + "type": "integer" + }, + "applicationRoles": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + }, + "deprecated": true + }, + "socialLinks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SocialLinkRepresentation" + }, + "deprecated": true + }, + "groups": { + "type": "array", + "items": { + "type": "string" + } + }, + "access": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + } + } + }, + "UserSessionRepresentation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "ipAddress": { + "type": "string" + }, + "start": { + "format": "int64", + "type": "integer" + }, + "lastAccess": { + "format": "int64", + "type": "integer" + }, + "rememberMe": { + "type": "boolean" + }, + "clients": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + } \ No newline at end of file diff --git a/api/pom.xml b/api/pom.xml index 5cd3657..7b3b6c7 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -16,9 +16,9 @@ pom - 22 + 21 2023.0.1 - 7.8.0 + 7.8.2 0.2.0 1.6.0.Beta2 7.6.0 @@ -27,7 +27,7 @@ 1.4 http localhost - 8080 + 7080 ${project.basedir}/bindings/ca-certificates ${project.basedir}/../../angular-ui @@ -99,6 +99,11 @@ spring-addons-starter-oidc-test ${com.c4-soft.springaddons.version} + + com.c4-soft.springaddons + spring-addons-starter-rest + ${com.c4-soft.springaddons.version} + com.c4-soft.springaddons spring-addons-starter-openapi @@ -107,6 +112,27 @@ + + + arm64 + + + + + org.springframework.boot + spring-boot-maven-plugin + + + dashaun/builder:tiny + + + + + + + + + @@ -154,6 +180,9 @@ ${ca-certificates.binding}:/platform/bindings/ca-certificates:ro + + ${java.version} + @@ -175,6 +204,33 @@ false + + org.openapitools + openapi-generator-maven-plugin + ${openapi-generator-maven-plugin.version} + + spring + false + true + false + false + false + false + + none + none + true + false + true + spring-http-interface + false + true + true + true + true + + + diff --git a/api/quiz-api/pom.xml b/api/quiz-api/pom.xml index 2789257..2fa4131 100644 --- a/api/quiz-api/pom.xml +++ b/api/quiz-api/pom.xml @@ -24,6 +24,11 @@ org.springframework.boot spring-boot-starter-data-jpa + + jakarta.data + jakarta.data-api + 1.0.0 + org.springframework.boot spring-boot-starter-mail @@ -36,9 +41,11 @@ org.springframework.boot spring-boot-starter-web + + - org.springframework.cloud - spring-cloud-starter-openfeign + org.mapstruct + mapstruct @@ -51,6 +58,12 @@ spring-addons-starter-oidc + + + com.c4-soft.springaddons + spring-addons-starter-rest + + org.springframework.boot @@ -122,6 +135,50 @@ + + org.openapitools + openapi-generator-maven-plugin + + + + generate + + + true + + ${project.basedir}/../keycloak-admin-api.openapi.json + org.keycloak.admin.api + org.keycloak.admin.model + + auth_time=authTimeLong + + + + + + com.spotify.fmt + fmt-maven-plugin + 2.23 + + target/generated-sources + true + .*\.java + false + false + true + false + + + + + + format + + + + @@ -195,6 +252,11 @@ start + + + https://oidc.c4-soft.com/auth + + post-integration-test diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/ExceptionHandlers.java b/api/quiz-api/src/main/java/com/c4soft/quiz/ExceptionHandlers.java new file mode 100644 index 0000000..dbe3f04 --- /dev/null +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/ExceptionHandlers.java @@ -0,0 +1,104 @@ +/* (C)2024 */ +package com.c4soft.quiz; + +import java.net.URI; +import java.util.Map; +import java.util.stream.Collectors; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingPathVariableException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import com.c4soft.quiz.domain.exception.DraftAlreadyExistsException; +import com.c4soft.quiz.domain.exception.InvalidQuizException; +import com.c4soft.quiz.domain.exception.NotADraftException; +import com.c4soft.quiz.domain.exception.QuizAlreadyHasAnAnswerException; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import jakarta.persistence.EntityNotFoundException; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; + +@RestControllerAdvice +public class ExceptionHandlers { + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ApiResponse(responseCode = "422", + content = {@Content(schema = @Schema(implementation = ValidationProblemDetail.class))}) + public ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException ex) { + final var detail = new ValidationProblemDetail(ex.getMessage(), ex.getFieldErrors().stream() + .collect(Collectors.toMap(FieldError::getField, FieldError::getCode))); + return ResponseEntity.status(detail.getStatus()).body(detail); + } + + @ExceptionHandler(ConstraintViolationException.class) + @ApiResponse(responseCode = "422", + content = {@Content(schema = @Schema(implementation = ValidationProblemDetail.class))}) + public ResponseEntity handleConstraintViolation( + ConstraintViolationException ex) { + final var problem = new ValidationProblemDetail(ex.getMessage(), + ex.getConstraintViolations().stream().collect(Collectors + .toMap(cv -> cv.getPropertyPath().toString(), ConstraintViolation::getMessage))); + return ResponseEntity.status(problem.getStatus()).body(problem); + } + + @ExceptionHandler(DataIntegrityViolationException.class) + @ApiResponse(responseCode = "422", + content = {@Content(schema = @Schema(implementation = ProblemDetail.class))}) + public ResponseEntity handleDataIntegrityViolation( + DataIntegrityViolationException ex) { + final var detail = + ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY, ex.getMessage()); + return ResponseEntity.status(detail.getStatus()).body(detail); + } + + @ExceptionHandler(EntityNotFoundException.class) + @ApiResponse(responseCode = "404", + content = {@Content(schema = @Schema(implementation = ProblemDetail.class))}) + public ResponseEntity handleEntityNotFound(EntityNotFoundException ex) { + final var problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage()); + return ResponseEntity.status(problem.getStatus()).body(problem); + } + + @ExceptionHandler(MissingPathVariableException.class) + @ApiResponse(responseCode = "404", + content = {@Content(schema = @Schema(implementation = ProblemDetail.class))}) + public ResponseEntity handleMissingPathVariableException( + MissingPathVariableException ex) { + final var problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage()); + return ResponseEntity.status(problem.getStatus()).body(problem); + } + + @ExceptionHandler({InvalidQuizException.class, QuizAlreadyHasAnAnswerException.class, + DraftAlreadyExistsException.class, NotADraftException.class}) + @ApiResponse(responseCode = "409", + content = {@Content(schema = @Schema(implementation = ProblemDetail.class))}) + public ResponseEntity handleConflicts(NotADraftException ex) { + final var problem = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage()); + return ResponseEntity.status(problem.getStatus()).body(problem); + } + + public static class ValidationProblemDetail extends ProblemDetail { + public static final URI TYPE = URI.create("https://quiz.c4-soft.com/problems/validation"); + public static final String INVALID_FIELDS_PROPERTY = "invalidFields"; + + public ValidationProblemDetail(String message, Map invalidFields) { + super(ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY, message)); + super.setType(TYPE); + super.setProperty(INVALID_FIELDS_PROPERTY, invalidFields); + } + + @SuppressWarnings("unchecked") + @JsonSerialize + Map getInvalidFields() { + return (Map) super.getProperties().get(INVALID_FIELDS_PROPERTY); + } + } +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/PersistenceConfig.java b/api/quiz-api/src/main/java/com/c4soft/quiz/PersistenceConfig.java index 90b6514..d4abfb7 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/PersistenceConfig.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/PersistenceConfig.java @@ -1,3 +1,4 @@ +/* (C)2024 */ package com.c4soft.quiz; import org.springframework.boot.autoconfigure.domain.EntityScan; @@ -10,5 +11,4 @@ @EnableJpaRepositories @EntityScan public class PersistenceConfig { - } diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/QuizApiApplication.java b/api/quiz-api/src/main/java/com/c4soft/quiz/QuizApiApplication.java index 7215bcd..50b71dc 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/QuizApiApplication.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/QuizApiApplication.java @@ -1,3 +1,4 @@ +/* (C)2024 */ package com.c4soft.quiz; import org.springframework.boot.SpringApplication; @@ -6,7 +7,7 @@ @SpringBootApplication public class QuizApiApplication { - public static void main(String[] args) { - SpringApplication.run(QuizApiApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(QuizApiApplication.class, args); + } } diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/RestConfig.java b/api/quiz-api/src/main/java/com/c4soft/quiz/RestConfig.java new file mode 100644 index 0000000..a44303c --- /dev/null +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/RestConfig.java @@ -0,0 +1,15 @@ +package com.c4soft.quiz; + +import org.keycloak.admin.api.UsersApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.c4_soft.springaddons.rest.SpringAddonsRestClientSupport; + +@Configuration +public class RestConfig { + @Bean + UsersApi usersApi(SpringAddonsRestClientSupport restSupport) { + return restSupport.service("keycloak-admin-api", UsersApi.class); + } + +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/SecurityConfig.java b/api/quiz-api/src/main/java/com/c4soft/quiz/SecurityConfig.java index c01ab20..3182004 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/SecurityConfig.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/SecurityConfig.java @@ -1,14 +1,13 @@ +/* (C)2024 */ package com.c4soft.quiz; import java.util.Collection; import java.util.Map; - import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.core.GrantedAuthority; - import com.c4_soft.springaddons.security.oidc.OpenidClaimSet; import com.c4_soft.springaddons.security.oidc.starter.OpenidProviderPropertiesResolver; import com.c4_soft.springaddons.security.oidc.starter.properties.NotAConfiguredOpenidProviderException; @@ -18,14 +17,16 @@ @Configuration @EnableMethodSecurity() public class SecurityConfig { - @Bean - JwtAbstractAuthenticationTokenConverter authenticationFactory( - Converter, Collection> authoritiesConverter, - OpenidProviderPropertiesResolver addonsPropertiesResolver) { - return jwt -> { - final var opProperties = addonsPropertiesResolver.resolve(jwt.getClaims()).orElseThrow(() -> new NotAConfiguredOpenidProviderException(jwt.getClaims())); - final var claims = new OpenidClaimSet(jwt.getClaims(), opProperties.getUsernameClaim()); - return new QuizAuthentication(claims, authoritiesConverter.convert(claims), jwt.getTokenValue()); - }; - } + @Bean + JwtAbstractAuthenticationTokenConverter authenticationFactory( + Converter, Collection> authoritiesConverter, + OpenidProviderPropertiesResolver addonsPropertiesResolver) { + return jwt -> { + final var opProperties = addonsPropertiesResolver.resolve(jwt.getClaims()) + .orElseThrow(() -> new NotAConfiguredOpenidProviderException(jwt.getClaims())); + final var claims = new OpenidClaimSet(jwt.getClaims(), opProperties.getUsernameClaim()); + return new QuizAuthentication(claims, authoritiesConverter.convert(claims), + jwt.getTokenValue()); + }; + } } diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/Choice.java b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/Choice.java index 70a56cd..86389a3 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/Choice.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/Choice.java @@ -1,3 +1,4 @@ +/* (C)2024 */ package com.c4soft.quiz.domain; import jakarta.persistence.Column; @@ -7,34 +8,41 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.ToString; @Entity @Data @NoArgsConstructor +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +@ToString(onlyExplicitlyIncluded = true) public class Choice { - public Choice(String label, Boolean isGood) { - this.label = label; - this.isGood = isGood; - } - - public Choice(Choice other) { - this.label = other.label; - this.isGood = other.isGood; - } + public Choice(String label, Boolean isGood) { + this.label = label; + this.isGood = isGood; + } - @Id - @GeneratedValue - private Long id; - - @ManyToOne(optional = false) - @JoinColumn(name = "question_id", updatable = false, nullable = false) - private Question question; - - @Column - private String label; - - @Column - private Boolean isGood; + public Choice(Choice other) { + this.label = other.label; + this.isGood = other.isGood; + } + @Id + @GeneratedValue + @EqualsAndHashCode.Include + @ToString.Include + private Long id; + + @ManyToOne(optional = false) + @JoinColumn(name = "question_id", updatable = false, nullable = false) + private Question question; + + @Column + @ToString.Include + private String label; + + @Column + @ToString.Include + private Boolean isGood; } diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/ChoiceService.java b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/ChoiceService.java new file mode 100644 index 0000000..2e973db --- /dev/null +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/ChoiceService.java @@ -0,0 +1,19 @@ +package com.c4soft.quiz.domain; + +import org.springframework.stereotype.Service; +import com.c4soft.quiz.domain.jpa.ChoiceRepository; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ChoiceService { + private final ChoiceRepository choiceRepo; + + public void deleteAll(Iterable choices) { + if (choices == null) { + return; + } + this.choiceRepo.deleteAll(choices); + } + +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/DraftAlreadyExistsException.java b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/DraftAlreadyExistsException.java deleted file mode 100644 index 0b918d2..0000000 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/DraftAlreadyExistsException.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.c4soft.quiz.domain; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.CONFLICT) -public class DraftAlreadyExistsException extends RuntimeException { - private static final long serialVersionUID = -8566841487060787834L; - - public DraftAlreadyExistsException(Long quizId) { - super("A draft already exists for quiz %d".formatted(quizId)); - } - - -} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/NotADraftException.java b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/NotADraftException.java deleted file mode 100644 index b02ae6f..0000000 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/NotADraftException.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.c4soft.quiz.domain; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.CONFLICT) -public class NotADraftException extends RuntimeException { - private static final long serialVersionUID = -8566841487060787834L; - - public NotADraftException(Long quizId) { - super("Quiz %d is not a draft".formatted(quizId)); - } - - -} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/Question.java b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/Question.java index c6132cd..4f2a7f6 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/Question.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/Question.java @@ -1,9 +1,9 @@ +/* (C)2024 */ package com.c4soft.quiz.domain; import java.util.ArrayList; import java.util.Collections; import java.util.List; - import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -14,87 +14,95 @@ import jakarta.persistence.OneToMany; import lombok.AccessLevel; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.ToString; @Entity @Data @NoArgsConstructor +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +@ToString(onlyExplicitlyIncluded = true) public class Question { - public Question(String label, String formattedBody, Integer priority, String comment, Choice... choices) { - this.label = label; - this.formattedBody = formattedBody; - this.priority = priority; - this.comment = comment; - this.choices = new ArrayList<>(choices.length); - for (var c : choices) { - this.add(c); - } - } - - public Question(Question other) { - this.comment = other.comment; - this.label = other.label; - this.formattedBody = other.formattedBody; - this.priority = other.priority; - this.choices = new ArrayList<>(other.choices.size()); - for (var c : other.choices) { - final var choice = new Choice(c); - this.add(choice); - } - } - - @Id - @GeneratedValue - private Long id; - - @ManyToOne(optional = false) - @JoinColumn(name = "quiz_id", updatable = false, nullable = false) - private Quiz quiz; - - @Column - private String label; - - @Column(length = 2047) - private String formattedBody; - - @Column(nullable = false, updatable = true) - private Integer priority; - - @Setter(AccessLevel.NONE) - @OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true) - private List choices = new ArrayList<>(); - - @Column(length = 2048) - private String comment; - - public List getChoices() { - return Collections.unmodifiableList(choices); - } - - public Choice getChoice(Long choiceId) { - if (choiceId == null) { - return null; - } - return choices.stream().filter(q -> choiceId.equals(q.getId())).findAny().orElse(null); - } - - public Question add(Choice choice) { - if (choice.getQuestion() != null && choice.getQuestion() != this) { - throw new RuntimeException("Choice already belongs to another question"); - } - choice.setQuestion(this); - choices.add(choice); - return this; - } - - public Question remove(Choice choice) { - if (choice.getQuestion() != this) { - throw new RuntimeException("Choice does not belongs to another question"); - } - choices.remove(choice); - choice.setQuestion(null); - return this; - } - + public Question(String label, String formattedBody, Integer priority, String comment, + Choice... choices) { + this.label = label; + this.formattedBody = formattedBody; + this.priority = priority; + this.comment = comment; + this.choices = new ArrayList<>(choices.length); + for (var c : choices) { + this.add(c); + } + } + + public Question(Question other) { + this.comment = other.comment; + this.label = other.label; + this.formattedBody = other.formattedBody; + this.priority = other.priority; + this.choices = new ArrayList<>(other.choices.size()); + for (var c : other.choices) { + final var choice = new Choice(c); + this.add(choice); + } + } + + @Id + @GeneratedValue + @EqualsAndHashCode.Include + @ToString.Include + private Long id; + + @ManyToOne(optional = false) + @JoinColumn(name = "quiz_id", updatable = false, nullable = false) + private Quiz quiz; + + @Column + @ToString.Include + private String label; + + @Column(length = 2047) + private String formattedBody; + + @Column(nullable = false, updatable = true) + @ToString.Include + private Integer priority; + + @Setter(AccessLevel.NONE) + @OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true) + private List choices = new ArrayList<>(); + + @Column(length = 2048) + private String comment; + + public List getChoices() { + return Collections.unmodifiableList(choices); + } + + public Choice getChoice(Long choiceId) { + if (choiceId == null) { + return null; + } + return choices.stream().filter(q -> choiceId.equals(q.getId())).findAny().orElse(null); + } + + public Question add(Choice choice) { + if (choice.getQuestion() != null && choice.getQuestion() != this) { + throw new RuntimeException("Choice already belongs to another question"); + } + choice.setQuestion(this); + choices.add(choice); + return this; + } + + public Question remove(Choice choice) { + if (choice.getQuestion() != this) { + throw new RuntimeException("Choice does not belongs to another question"); + } + choices.remove(choice); + choice.setQuestion(null); + return this; + } } diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/QuestionService.java b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/QuestionService.java new file mode 100644 index 0000000..477b86a --- /dev/null +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/QuestionService.java @@ -0,0 +1,25 @@ +package com.c4soft.quiz.domain; + +import java.util.Collection; +import java.util.stream.Collectors; +import org.springframework.stereotype.Service; +import com.c4soft.quiz.domain.jpa.QuestionRepository; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class QuestionService { + private final QuestionRepository questionRepo; + private final ChoiceService choiceService; + + public void deleteAll(Collection questions) { + if (questions == null) { + return; + } + final var choices = + questions.stream().flatMap(q -> q.getChoices().stream()).collect(Collectors.toSet()); + this.choiceService.deleteAll(choices); + this.questionRepo.deleteAll(questions); + } + +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/Quiz.java b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/Quiz.java index fc95049..0b8ffd6 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/Quiz.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/Quiz.java @@ -1,8 +1,8 @@ +/* (C)2024 */ package com.c4soft.quiz.domain; import java.util.ArrayList; import java.util.List; - import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -10,118 +10,129 @@ import jakarta.persistence.Id; import jakarta.persistence.OneToMany; import jakarta.persistence.OneToOne; +import jakarta.persistence.OrderBy; import lombok.AccessLevel; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.ToString; @Entity @Data @NoArgsConstructor +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +@ToString(onlyExplicitlyIncluded = true) public class Quiz { - public Quiz(String title, String authorName, Question... questions) { - this.title = title; - this.authorName = authorName; - this.questions = new ArrayList<>(questions.length); - for (var q : questions) { - this.add(q); - } - } - - public Quiz(Quiz other, String authorName) { - this.authorName = authorName; - this.questions = new ArrayList<>(other.questions.size()); - for (var q : other.questions) { - final var question = new Question(q); - this.add(question); - } - this.isPublished = false; - this.moderatedBy = null; - this.title = other.title; - } - - @Id - @GeneratedValue - private Long id; - - @Column(nullable = false) - private String title; - - @Setter(AccessLevel.NONE) - @OneToMany(mappedBy = "quiz", cascade = CascadeType.ALL, orphanRemoval = true) - private List questions = new ArrayList<>(); - - @Column(nullable = false, updatable = false) - private String authorName; - - @Column(nullable = false, columnDefinition = "boolean default false") - private Boolean isSubmitted = false; - - @Column(nullable = false, columnDefinition = "boolean default false") - private Boolean isPublished = false; - - @OneToOne(optional = true) - private Quiz draft; - - @OneToOne(optional = true) - private Quiz replaces; - - @OneToOne(optional = true) - private Quiz replacedBy; - - @Column() - private String moderatorComment; - - @Column() - private String moderatedBy; - - @Column(nullable = false, columnDefinition = "boolean default true") - private Boolean isChoicesShuffled = true; - - @Column(nullable = false, columnDefinition = "boolean default true") - private Boolean isReplayEnabled = true; - - @Column(nullable = false, columnDefinition = "boolean default true") - private Boolean isPerQuestionResult = true; - - @Column(nullable = false, columnDefinition = "boolean default false") - private Boolean isTrainerNotifiedOfNewTests = false; - - @Setter(AccessLevel.NONE) - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) - private List skillTests = new ArrayList<>(); - - public Question getQuestion(Long questionId) { - if(questionId == null) { - return null; - } - return questions.stream().filter(q -> questionId.equals(q.getId())).findAny() - .orElse(null); - } - - public Quiz add(Question question) { - if (question.getQuiz() != null && question.getQuiz() != this) { - throw new RuntimeException("Question already belongs to another quiz"); - } - questions.stream().filter(q -> q.getPriority() >= question.getPriority()) - .forEach(q -> q.setPriority(q.getPriority() + 1)); - question.setQuiz(this); - questions.add(question); - questions.sort((a, b) -> a.getPriority() - b.getPriority()); - return this; - } - - public Quiz remove(Question question) { - if (question.getQuiz() != this) { - throw new RuntimeException("Question does not belong to this quiz"); - } - final var isRemoved = questions.remove(question); - question.setQuiz(null); - if (isRemoved) { - questions.stream().filter(q -> q.getPriority() >= question.getPriority()) - .forEach(q -> q.setPriority(q.getPriority() - 1)); - } - return this; - } + public Quiz(String title, String authorName, Question... questions) { + this.title = title; + this.authorName = authorName; + this.questions = new ArrayList<>(questions.length); + for (var q : questions) { + this.add(q); + } + } + + public Quiz(Quiz other, String authorName) { + this.authorName = authorName; + this.questions = new ArrayList<>(other.questions.size()); + for (var q : other.questions) { + final var question = new Question(q); + this.add(question); + } + this.isPublished = false; + this.moderatedBy = null; + this.title = other.title; + } + + @Id + @GeneratedValue + @EqualsAndHashCode.Include + @ToString.Include + private Long id; + + @Column(nullable = false) + @ToString.Include + private String title; + + @Setter(AccessLevel.NONE) + @OneToMany(mappedBy = "quiz", cascade = CascadeType.ALL, orphanRemoval = true) + @OrderBy("priority") + private List questions = new ArrayList<>(); + + @Column(nullable = false, updatable = false) + @ToString.Include + private String authorName; + + @Column(nullable = false, columnDefinition = "boolean default false") + @ToString.Include + private Boolean isSubmitted = false; + + @Column(nullable = false, columnDefinition = "boolean default false") + @ToString.Include + private Boolean isPublished = false; + + @OneToOne(optional = true) + private Quiz draft; + + @OneToOne(optional = true) + private Quiz replaces; + + @OneToOne(optional = true) + private Quiz replacedBy; + + @Column() + private String moderatorComment; + + @Column() + private String moderatedBy; + + @Column(nullable = false, columnDefinition = "boolean default true") + private Boolean isChoicesShuffled = true; + + @Column(nullable = false, columnDefinition = "boolean default true") + private Boolean isReplayEnabled = true; + + @Column(nullable = false, columnDefinition = "boolean default true") + private Boolean isPerQuestionResult = true; + + @Column(nullable = false, columnDefinition = "boolean default false") + private Boolean isTrainerNotifiedOfNewTests = false; + + @Setter(AccessLevel.NONE) + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + private List skillTests = new ArrayList<>(); + + public Question getQuestion(Long questionId) { + if (questionId == null) { + return null; + } + return questions.stream().filter(q -> questionId.equals(q.getId())).findAny().orElse(null); + } + + public Quiz add(Question question) { + if (question.getQuiz() != null && question.getQuiz() != this) { + throw new RuntimeException("Question already belongs to another quiz"); + } + questions.stream().filter(q -> q.getPriority() >= question.getPriority()) + .forEach(q -> q.setPriority(q.getPriority() + 1)); + question.setQuiz(this); + questions.add(question); + questions.sort((a, b) -> (a.getPriority() == null ? questions.size() -1 : a.getPriority()) - (b.getPriority() == null ? questions.size() -1 : b.getPriority())); + return this; + } + + public Quiz remove(Question question) { + if (question.getQuiz() != this) { + throw new RuntimeException("Question does not belong to this quiz"); + } + final var isRemoved = questions.remove(question); + question.setQuiz(null); + if (isRemoved) { + questions.stream().filter(q -> q.getPriority() >= question.getPriority()) + .forEach(q -> q.setPriority((q.getPriority() == null ? questions.size() -1 : q.getPriority()) - 1)); + } + return this; + } } diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/QuizAuthentication.java b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/QuizAuthentication.java index 9caaab3..44aecc2 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/QuizAuthentication.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/QuizAuthentication.java @@ -1,31 +1,32 @@ +/* (C)2024 */ package com.c4soft.quiz.domain; import java.util.Collection; import java.util.stream.Collectors; - import org.springframework.security.core.GrantedAuthority; - import com.c4_soft.springaddons.security.oidc.OAuthentication; import com.c4_soft.springaddons.security.oidc.OpenidClaimSet; - import lombok.Getter; @Getter public class QuizAuthentication extends OAuthentication { - private static final long serialVersionUID = 1L; - public static final String AUTHORITY_MODERATOR = "moderator"; - public static final String AUTHORITY_TRAINER = "trainer"; - public static final String SPEL_IS_MODERATOR_OR_TRAINER = "hasAnyAuthority('moderator', 'trainer')"; - public static final String SPEL_IS_MODERATOR = "hasAuthority('moderator')"; - public static final String SPEL_IS_TRAINER = "hasAuthority('trainer')"; + private static final long serialVersionUID = 1L; + public static final String AUTHORITY_MODERATOR = "moderator"; + public static final String AUTHORITY_TRAINER = "trainer"; + public static final String SPEL_IS_MODERATOR_OR_TRAINER = + "hasAnyAuthority('moderator', 'trainer')"; + public static final String SPEL_IS_MODERATOR = "hasAuthority('moderator')"; + public static final String SPEL_IS_TRAINER = "hasAuthority('trainer')"; - private final boolean isModerator; - private final boolean isTrainer; + private final boolean isModerator; + private final boolean isTrainer; - public QuizAuthentication(OpenidClaimSet claims, Collection authorities, String tokenString) { - super(claims, authorities, tokenString); - final var authoritiesStrings = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet()); - this.isModerator = authoritiesStrings.contains(AUTHORITY_MODERATOR); - this.isTrainer = authoritiesStrings.contains(AUTHORITY_TRAINER); - } + public QuizAuthentication(OpenidClaimSet claims, + Collection authorities, String tokenString) { + super(claims, authorities, tokenString); + final var authoritiesStrings = + authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet()); + this.isModerator = authoritiesStrings.contains(AUTHORITY_MODERATOR); + this.isTrainer = authoritiesStrings.contains(AUTHORITY_TRAINER); + } } diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/QuizRejectionDto.java b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/QuizRejectionDto.java index 42df467..8653270 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/QuizRejectionDto.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/QuizRejectionDto.java @@ -1,7 +1,8 @@ +/* (C)2024 */ package com.c4soft.quiz.domain; import jakarta.validation.constraints.NotEmpty; -public record QuizRejectionDto(@NotEmpty(message = "A non-empty message is mandatory") String message) { - +public record QuizRejectionDto( + @NotEmpty(message = "A non-empty message is mandatory") String message) { } diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/QuizRepository.java b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/QuizRepository.java deleted file mode 100644 index 5eac295..0000000 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/QuizRepository.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.c4soft.quiz.domain; - -import java.util.List; -import java.util.Optional; - -import org.springframework.data.jpa.domain.Specification; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -import org.springframework.security.core.context.SecurityContextHolder; - -public interface QuizRepository extends JpaRepository, JpaSpecificationExecutor { - - Optional findByReplacesId(Long replacedQuizId); - - List findByIsSubmitted(boolean isSubmitted); - - static Specification searchSpec(Optional authorName, Optional quizTitle) { - final var currentUserName = SecurityContextHolder.getContext().getAuthentication().getName(); - var spec = Specification.where(isPublished()).or(authoredBy(currentUserName)); - if (authorName.isPresent()) { - spec = spec.and(authoredBy(authorName.get())); - } - if (quizTitle.isPresent()) { - spec = spec.and(titleContains(quizTitle.get())); - } - return orderByTitleDesc(spec); - } - - static Specification isPublished() { - return (root, query, cb) -> cb.isTrue(root.get(Quiz_.isPublished)); - } - - static Specification authoredBy(String authorName) { - return (root, query, cb) -> cb.like(cb.upper(root.get(Quiz_.authorName)), "%%%s%%".formatted(authorName.toUpperCase())); - } - - static Specification titleContains(String title) { - return (root, query, cb) -> cb.like(cb.upper(root.get(Quiz_.title)), "%%%s%%".formatted(title.toUpperCase())); - } - - static Specification orderByTitleDesc(Specification spec) { - return (root, query, cb) -> { - query.orderBy(cb.asc(root.get(Quiz_.title))); - return spec.toPredicate(root, query, cb); - }; - } -} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/SkillTest.java b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/SkillTest.java index 05e0fc7..8300955 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/SkillTest.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/SkillTest.java @@ -1,3 +1,4 @@ +/* (C)2024 */ package com.c4soft.quiz.domain; import java.time.Instant; @@ -5,10 +6,8 @@ import java.util.Collection; import java.util.List; import java.util.Objects; - import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; - import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import jakarta.persistence.EmbeddedId; @@ -17,59 +16,66 @@ import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.ToString; @Entity @Data @NoArgsConstructor +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +@ToString(onlyExplicitlyIncluded = true) public class SkillTest { - public SkillTest(Quiz quiz, String traineeName, Collection choices) { - this.submittedOn = Instant.now().toEpochMilli(); - this.choices = new ArrayList<>(choices); - this.id.traineeName = traineeName; - this.choices.forEach(c -> { - if (c.getQuestion() == null || c.getQuestion().getQuiz() == null || !Objects.equals(c.getQuestion().getQuiz().getId(), quiz.getId())) { - throw new NotAcceptableSkillTestException("All choices must target the same quiz (%s)".formatted(quiz.getTitle())); - } - }); - this.id.quizId = quiz.getId(); - } + public SkillTest(Quiz quiz, String traineeName, Collection choices) { + this.submittedOn = Instant.now().toEpochMilli(); + this.choices = new ArrayList<>(choices); + this.id.traineeName = traineeName; + this.choices.forEach(c -> { + if (c.getQuestion() == null || c.getQuestion().getQuiz() == null + || !Objects.equals(c.getQuestion().getQuiz().getId(), quiz.getId())) { + throw new NotAcceptableSkillTestException( + "All choices must target the same quiz (%s)".formatted(quiz.getTitle())); + } + }); + this.id.quizId = quiz.getId(); + } - @Setter(AccessLevel.NONE) - @EmbeddedId - private final SkillTestPk id = new SkillTestPk(); + @Setter(AccessLevel.NONE) + @EmbeddedId + private final SkillTestPk id = new SkillTestPk(); - @Column - private Long submittedOn; + @Column + private Long submittedOn; - @ManyToMany - private List choices = new ArrayList<>(); + @ManyToMany + private List choices = new ArrayList<>(); - public List getChoices(Long questionId) { - return choices.stream().filter(c -> Objects.equals(c.getQuestion().getId(), questionId)).toList(); - } + public List getChoices(Long questionId) { + return choices.stream().filter(c -> Objects.equals(c.getQuestion().getId(), questionId)) + .toList(); + } - @ResponseStatus(HttpStatus.NOT_ACCEPTABLE) - static class NotAcceptableSkillTestException extends RuntimeException { - private static final long serialVersionUID = -6754084213295394103L; + @ResponseStatus(HttpStatus.NOT_ACCEPTABLE) + static class NotAcceptableSkillTestException extends RuntimeException { + private static final long serialVersionUID = -6754084213295394103L; - public NotAcceptableSkillTestException(String message) { - super(message); - } - } + public NotAcceptableSkillTestException(String message) { + super(message); + } + } - @Embeddable - @Data - @NoArgsConstructor - @AllArgsConstructor - public static class SkillTestPk { - @Setter(AccessLevel.NONE) - @Column - private Long quizId; + @Embeddable + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class SkillTestPk { + @Setter(AccessLevel.NONE) + @Column + private Long quizId; - @Setter(AccessLevel.NONE) - @Column - private String traineeName; - } + @Setter(AccessLevel.NONE) + @Column + private String traineeName; + } } diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/SkillTestRepository.java b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/SkillTestRepository.java deleted file mode 100644 index 4e770db..0000000 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/SkillTestRepository.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.c4soft.quiz.domain; - -import java.time.Instant; -import java.util.List; -import java.util.Optional; - -import org.springframework.data.jpa.domain.Specification; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; - -import com.c4soft.quiz.domain.SkillTest.SkillTestPk; - -public interface SkillTestRepository extends JpaRepository, JpaSpecificationExecutor { - Optional findByIdQuizIdAndIdTraineeName(Long quizId, String traineeName); - - List findByIdQuizId(Long quizId); - - void deleteByIdQuizId(Long quizId); - - static Specification quizIdSpec(Long quizId) { - return (skillTest, cq, cb) -> cb.equal(skillTest.get("id").get("quizId"), quizId); - } - - static Specification traineeNameSpec(String traineeName) { - return (skillTest, cq, cb) -> cb.equal(skillTest.get("id").get("traineeName"), traineeName); - } - - static Specification sinceSpec(Long since) { - return (skillTest, cq, cb) -> cb.ge(skillTest.get("submittedOn"), since); - } - - static Specification untilSpec(Long until) { - return (skillTest, cq, cb) -> cb.le(skillTest.get("submittedOn"), until); - } - - static Specification orderByIdDesc(Specification spec) { - return (root, query, cb) -> { - query.orderBy(cb.desc(root.get(SkillTest_.id))); - return spec.toPredicate(root, query, cb); - }; - } - - static Specification spec(Long quizId, Long since, Optional until) { - return orderByIdDesc(Specification.where(quizIdSpec(quizId)).and(sinceSpec(since)).and(untilSpec(until.orElse(Instant.now().toEpochMilli())))); - } -} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/exception/DraftAlreadyExistsException.java b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/exception/DraftAlreadyExistsException.java new file mode 100644 index 0000000..aca2be2 --- /dev/null +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/exception/DraftAlreadyExistsException.java @@ -0,0 +1,10 @@ +/* (C)2024 */ +package com.c4soft.quiz.domain.exception; + +public class DraftAlreadyExistsException extends RuntimeException { + private static final long serialVersionUID = -8566841487060787834L; + + public DraftAlreadyExistsException(Long quizId) { + super("A draft already exists for quiz %d".formatted(quizId)); + } +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/exception/InvalidQuizException.java b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/exception/InvalidQuizException.java new file mode 100644 index 0000000..f018868 --- /dev/null +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/exception/InvalidQuizException.java @@ -0,0 +1,9 @@ +package com.c4soft.quiz.domain.exception; + +public class InvalidQuizException extends RuntimeException { + private static final long serialVersionUID = 8816930385638385805L; + + public InvalidQuizException(Long quizId) { + super("Quiz %d doesn't accept answers anymore.".formatted(quizId)); + } +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/exception/NotADraftException.java b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/exception/NotADraftException.java new file mode 100644 index 0000000..3bd8f93 --- /dev/null +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/exception/NotADraftException.java @@ -0,0 +1,14 @@ +/* (C)2024 */ +package com.c4soft.quiz.domain.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.CONFLICT) +public class NotADraftException extends RuntimeException { + private static final long serialVersionUID = -8566841487060787834L; + + public NotADraftException(Long quizId) { + super("Quiz %d is not a draft".formatted(quizId)); + } +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/exception/QuizAlreadyHasAnAnswerException.java b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/exception/QuizAlreadyHasAnAnswerException.java new file mode 100644 index 0000000..01a1b08 --- /dev/null +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/exception/QuizAlreadyHasAnAnswerException.java @@ -0,0 +1,12 @@ +package com.c4soft.quiz.domain.exception; + +public class QuizAlreadyHasAnAnswerException extends RuntimeException { + + private static final long serialVersionUID = 6171083302507116601L; + + public QuizAlreadyHasAnAnswerException(Long quizId, String traineeName) { + super( + "Quiz %d already has an answer for %s and doesn't accept replay. Ask the trainer to delete the answer before submitting a new one." + .formatted(quizId, traineeName)); + } +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/ChoiceRepository.java b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/jpa/ChoiceRepository.java similarity index 61% rename from api/quiz-api/src/main/java/com/c4soft/quiz/domain/ChoiceRepository.java rename to api/quiz-api/src/main/java/com/c4soft/quiz/domain/jpa/ChoiceRepository.java index 0c35f78..c15e3bc 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/ChoiceRepository.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/jpa/ChoiceRepository.java @@ -1,6 +1,8 @@ -package com.c4soft.quiz.domain; +/* (C)2024 */ +package com.c4soft.quiz.domain.jpa; import org.springframework.data.jpa.repository.JpaRepository; +import com.c4soft.quiz.domain.Choice; public interface ChoiceRepository extends JpaRepository { } diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/QuestionRepository.java b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/jpa/QuestionRepository.java similarity index 61% rename from api/quiz-api/src/main/java/com/c4soft/quiz/domain/QuestionRepository.java rename to api/quiz-api/src/main/java/com/c4soft/quiz/domain/jpa/QuestionRepository.java index 2d18bc0..4b61889 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/QuestionRepository.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/jpa/QuestionRepository.java @@ -1,6 +1,8 @@ -package com.c4soft.quiz.domain; +/* (C)2024 */ +package com.c4soft.quiz.domain.jpa; import org.springframework.data.jpa.repository.JpaRepository; +import com.c4soft.quiz.domain.Question; public interface QuestionRepository extends JpaRepository { } diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/jpa/QuizRepository.java b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/jpa/QuizRepository.java new file mode 100644 index 0000000..02b3f10 --- /dev/null +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/jpa/QuizRepository.java @@ -0,0 +1,51 @@ +/* (C)2024 */ +package com.c4soft.quiz.domain.jpa; + +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.security.core.context.SecurityContextHolder; +import com.c4soft.quiz.domain.Quiz; +import com.c4soft.quiz.domain.Quiz_; + +public interface QuizRepository extends JpaRepository, JpaSpecificationExecutor { + + Optional findByReplacesId(Long replacedQuizId); + + List findByIsSubmitted(boolean isSubmitted); + + static Specification searchSpec(Optional authorName, Optional quizTitle) { + final var currentUserName = SecurityContextHolder.getContext().getAuthentication().getName(); + var spec = Specification.where(isPublished()).or(authoredBy(currentUserName)); + if (authorName.isPresent()) { + spec = spec.and(authoredBy(authorName.get())); + } + if (quizTitle.isPresent()) { + spec = spec.and(titleContains(quizTitle.get())); + } + return orderByTitleDesc(spec); + } + + static Specification isPublished() { + return (root, query, cb) -> cb.isTrue(root.get(Quiz_.isPublished)); + } + + static Specification authoredBy(String authorName) { + return (root, query, cb) -> cb.like(cb.upper(root.get(Quiz_.authorName)), + "%%%s%%".formatted(authorName.toUpperCase())); + } + + static Specification titleContains(String title) { + return (root, query, cb) -> cb.like(cb.upper(root.get(Quiz_.title)), + "%%%s%%".formatted(title.toUpperCase())); + } + + static Specification orderByTitleDesc(Specification spec) { + return (root, query, cb) -> { + query.orderBy(cb.asc(root.get(Quiz_.title))); + return spec.toPredicate(root, query, cb); + }; + } +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/domain/jpa/SkillTestRepository.java b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/jpa/SkillTestRepository.java new file mode 100644 index 0000000..80deb0a --- /dev/null +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/domain/jpa/SkillTestRepository.java @@ -0,0 +1,49 @@ +/* (C)2024 */ +package com.c4soft.quiz.domain.jpa; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import com.c4soft.quiz.domain.SkillTest; +import com.c4soft.quiz.domain.SkillTest.SkillTestPk; +import com.c4soft.quiz.domain.SkillTest_; + +public interface SkillTestRepository + extends JpaRepository, JpaSpecificationExecutor { + Optional findByIdQuizIdAndIdTraineeName(Long quizId, String traineeName); + + List findByIdQuizId(Long quizId); + + void deleteByIdQuizId(Long quizId); + + static Specification quizIdSpec(Long quizId) { + return (skillTest, cq, cb) -> cb.equal(skillTest.get("id").get("quizId"), quizId); + } + + static Specification traineeNameSpec(String traineeName) { + return (skillTest, cq, cb) -> cb.equal(skillTest.get("id").get("traineeName"), traineeName); + } + + static Specification sinceSpec(Long since) { + return (skillTest, cq, cb) -> cb.ge(skillTest.get("submittedOn"), since); + } + + static Specification untilSpec(Long until) { + return (skillTest, cq, cb) -> cb.le(skillTest.get("submittedOn"), until); + } + + static Specification orderByIdDesc(Specification spec) { + return (root, query, cb) -> { + query.orderBy(cb.desc(root.get(SkillTest_.id))); + return spec.toPredicate(root, query, cb); + }; + } + + static Specification spec(Long quizId, Long since, Optional until) { + return orderByIdDesc(Specification.where(quizIdSpec(quizId)).and(sinceSpec(since)) + .and(untilSpec(until.orElse(Instant.now().toEpochMilli())))); + } +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/feign/FeignConfig.java b/api/quiz-api/src/main/java/com/c4soft/quiz/feign/FeignConfig.java deleted file mode 100644 index 3d08c17..0000000 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/feign/FeignConfig.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.c4soft.quiz.feign; - -import org.springframework.cloud.openfeign.EnableFeignClients; -import org.springframework.context.annotation.Configuration; - -@Configuration -@EnableFeignClients -public class FeignConfig { - -} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/feign/KeycloakAdminApiClient.java b/api/quiz-api/src/main/java/com/c4soft/quiz/feign/KeycloakAdminApiClient.java deleted file mode 100644 index 12a596a..0000000 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/feign/KeycloakAdminApiClient.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.c4soft.quiz.feign; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import org.springframework.cloud.openfeign.FeignClient; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; - -@FeignClient(name = "quiz-admin") -public interface KeycloakAdminApiClient { - @GetMapping(value = "/users") - List getUser(@RequestParam(value="username") String username, @RequestParam(value="exact") boolean exact); - - public static class UserRepresentation extends HashMap { - - private static final long serialVersionUID = -2288285379516753557L; - - public UserRepresentation() { - super(); - } - - public UserRepresentation(Map m) { - super(m); - } - - public String getEmail() { - return Optional.ofNullable(this.get("email")).map(Object::toString).orElse(null); - } - - public String getFirtsName() { - return Optional.ofNullable(this.get("firstName")).map(Object::toString).orElse(null); - } - - public String getLastName() { - return Optional.ofNullable(this.get("lastName")).map(Object::toString).orElse(null); - } - } -} \ No newline at end of file diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/web/ChoiceController.java b/api/quiz-api/src/main/java/com/c4soft/quiz/web/ChoiceController.java new file mode 100644 index 0000000..8aaa2aa --- /dev/null +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/web/ChoiceController.java @@ -0,0 +1,146 @@ +/* (C)2024 */ +package com.c4soft.quiz.web; + +import java.net.URI; +import java.util.Objects; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.c4soft.quiz.domain.Choice; +import com.c4soft.quiz.domain.Quiz; +import com.c4soft.quiz.domain.QuizAuthentication; +import com.c4soft.quiz.domain.exception.NotADraftException; +import com.c4soft.quiz.domain.jpa.ChoiceRepository; +import com.c4soft.quiz.domain.jpa.QuestionRepository; +import com.c4soft.quiz.domain.jpa.SkillTestRepository; +import com.c4soft.quiz.web.dto.ChoiceUpdateDto; +import com.c4soft.quiz.web.dto.mapping.ChoiceMapper; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping(path = "/quizzes/{quiz-id}/questions/{question-id}/choices") +@RequiredArgsConstructor +@Validated +@Tag(name = "Quizzes") +public class ChoiceController { + private final ChoiceRepository choiceRepo; + private final QuestionRepository questionRepo; + private final ChoiceMapper choiceMapper; + private final SkillTestRepository skillTestRepo; + + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAuthority('trainer') && #quiz.authorName == authentication.name") + @Transactional(readOnly = false) + @Operation(responses = {@ApiResponse(responseCode = "201", + headers = @Header(name = HttpHeaders.LOCATION, description = "ID of the created choice"))}) + public ResponseEntity addChoice( + @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, + @Parameter(schema = @Schema(type = "integer")) @PathVariable("question-id") Long questionId, + @RequestBody @Valid ChoiceUpdateDto dto, QuizAuthentication auth) throws NotADraftException { + if (quiz.getReplacedBy() != null) { + throw new NotADraftException(quiz.getId()); + } + if (quiz.getIsPublished() && !auth.isModerator()) { + throw new NotADraftException(quiz.getId()); + } + + final var question = quiz.getQuestion(questionId); + if (question == null) { + return ResponseEntity.notFound().build(); + } + final var choice = choiceMapper.update(dto, new Choice()); + question.add(choice); + choice.setQuestion(question); + final var created = choiceRepo.save(choice); + return ResponseEntity.created(URI.create("%d".formatted(created.getId()))).build(); + } + + @PutMapping(path = "/{choice-id}", consumes = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAuthority('trainer') && #quiz.authorName == authentication.name") + @Transactional(readOnly = false) + @Operation(responses = {@ApiResponse(responseCode = "202")}) + public ResponseEntity updateChoice( + @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, + @Parameter(schema = @Schema(type = "integer")) @PathVariable("question-id") Long questionId, + @Parameter(schema = @Schema(type = "integer")) @PathVariable("choice-id") Long choiceId, + @RequestBody @Valid ChoiceUpdateDto dto, QuizAuthentication auth) throws NotADraftException { + if (quiz.getReplacedBy() != null) { + throw new NotADraftException(quiz.getId()); + } + if (quiz.getIsPublished() && !auth.isModerator()) { + throw new NotADraftException(quiz.getId()); + } + + final var question = quiz.getQuestion(questionId); + if (question == null) { + return ResponseEntity.notFound().build(); + } + final var choice = question.getChoice(choiceId); + if (choice == null) { + return ResponseEntity.notFound().build(); + } + if (!Objects.equals(dto.label(), choice.getLabel()) && !auth.isModerator() + && (quiz.getIsPublished() || quiz.getReplacedBy() != null)) { + throw new NotADraftException(quiz.getId()); + } + choiceRepo.save(choiceMapper.update(dto, choice)); + return ResponseEntity.accepted().build(); + } + + @DeleteMapping(path = "/{choice-id}") + @PreAuthorize("hasAuthority('trainer') && #quiz.authorName == authentication.name") + @Transactional(readOnly = false) + @Operation(responses = {@ApiResponse(responseCode = "202")}) + public ResponseEntity deleteChoice( + @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, + @Parameter(schema = @Schema(type = "integer")) @PathVariable("question-id") Long questionId, + @Parameter(schema = @Schema(type = "integer")) @PathVariable("choice-id") Long choiceId, + QuizAuthentication auth) throws NotADraftException { + if (quiz.getReplacedBy() != null) { + throw new NotADraftException(quiz.getId()); + } + + final var question = quiz.getQuestion(questionId); + if (question == null) { + return ResponseEntity.notFound().build(); + } + final var choice = question.getChoice(choiceId); + if (choice == null) { + return ResponseEntity.notFound().build(); + } + + final var tests = skillTestRepo.findByIdQuizId(quiz.getId()); + tests.forEach(t -> { + final var filtered = t.getChoices().stream().filter(c -> { + return c.getId() != choiceId; + }).toList(); + t.setChoices(filtered); + }); + skillTestRepo.saveAll(tests); + + question.remove(choice); + questionRepo.save(question); + choiceRepo.delete(choice); + return ResponseEntity.accepted().build(); + } +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/web/ExceptionHandlers.java b/api/quiz-api/src/main/java/com/c4soft/quiz/web/ExceptionHandlers.java deleted file mode 100644 index ce8d9e2..0000000 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/web/ExceptionHandlers.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.c4soft.quiz.web; - -import java.net.URI; -import java.util.Map; -import java.util.stream.Collectors; - -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.http.HttpStatus; -import org.springframework.http.ProblemDetail; -import org.springframework.http.ResponseEntity; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.MissingPathVariableException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; - -import com.c4soft.quiz.web.SkillTestController.InvalidQuizException; -import com.c4soft.quiz.web.SkillTestController.QuizAlreadyHasAnAnswerException; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; - -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import jakarta.persistence.EntityNotFoundException; -import jakarta.validation.ConstraintViolation; -import jakarta.validation.ConstraintViolationException; - -@RestControllerAdvice -public class ExceptionHandlers { - - @ExceptionHandler(MethodArgumentNotValidException.class) - @ApiResponse(responseCode = "422", content = { @Content(schema = @Schema(implementation = ValidationProblemDetail.class)) }) - public ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex) { - final var detail = new ValidationProblemDetail( - ex.getMessage(), - ex.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getCode))); - return ResponseEntity.status(detail.getStatus()).body(detail); - } - - @ExceptionHandler(ConstraintViolationException.class) - @ApiResponse(responseCode = "422", content = { @Content(schema = @Schema(implementation = ValidationProblemDetail.class)) }) - public ResponseEntity handleConstraintViolation(ConstraintViolationException ex) { - final var problem = new ValidationProblemDetail( - ex.getMessage(), - ex.getConstraintViolations().stream().collect(Collectors.toMap(cv -> cv.getPropertyPath().toString(), ConstraintViolation::getMessage))); - return ResponseEntity.status(problem.getStatus()).body(problem); - } - - @ExceptionHandler(DataIntegrityViolationException.class) - @ApiResponse(responseCode = "422", content = { @Content(schema = @Schema(implementation = ProblemDetail.class)) }) - public ResponseEntity handleDataIntegrityViolation(DataIntegrityViolationException ex) { - final var detail = ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY, ex.getMessage()); - return ResponseEntity.status(detail.getStatus()).body(detail); - } - - @ExceptionHandler(EntityNotFoundException.class) - @ApiResponse(responseCode = "404", content = { @Content(schema = @Schema(implementation = ProblemDetail.class)) }) - public ResponseEntity handleEntityNotFound(EntityNotFoundException ex) { - final var problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage()); - return ResponseEntity.status(problem.getStatus()).body(problem); - } - - @ExceptionHandler(MissingPathVariableException.class) - @ApiResponse(responseCode = "404", content = { @Content(schema = @Schema(implementation = ProblemDetail.class)) }) - public ResponseEntity handleMissingPathVariableException(MissingPathVariableException ex) { - final var problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage()); - return ResponseEntity.status(problem.getStatus()).body(problem); - } - - @ExceptionHandler(InvalidQuizException.class) - @ApiResponse(responseCode = "409", content = { @Content(schema = @Schema(implementation = ProblemDetail.class)) }) - public ResponseEntity handleInvalidQuiz(InvalidQuizException ex) { - final var problem = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage()); - return ResponseEntity.status(problem.getStatus()).body(problem); - } - - @ExceptionHandler(QuizAlreadyHasAnAnswerException.class) - @ApiResponse(responseCode = "409", content = { @Content(schema = @Schema(implementation = ProblemDetail.class)) }) - public ResponseEntity handleQuizAlreadyHasAnAnswer(QuizAlreadyHasAnAnswerException ex) { - final var problem = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage()); - return ResponseEntity.status(problem.getStatus()).body(problem); - } - - public static class ValidationProblemDetail extends ProblemDetail { - public static final URI TYPE = URI.create("https://quiz.c4-soft.com/problems/validation"); - public static final String INVALID_FIELDS_PROPERTY = "invalidFields"; - - public ValidationProblemDetail(String message, Map invalidFields) { - super(ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY, message)); - super.setType(TYPE); - super.setProperty(INVALID_FIELDS_PROPERTY, invalidFields); - } - - @SuppressWarnings("unchecked") - @JsonSerialize - Map getInvalidFields() { - return (Map) super.getProperties().get(INVALID_FIELDS_PROPERTY); - } - } -} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/web/QuestionController.java b/api/quiz-api/src/main/java/com/c4soft/quiz/web/QuestionController.java new file mode 100644 index 0000000..486e3c4 --- /dev/null +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/web/QuestionController.java @@ -0,0 +1,154 @@ +/* (C)2024 */ +package com.c4soft.quiz.web; + +import java.net.URI; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.c4soft.quiz.domain.Question; +import com.c4soft.quiz.domain.Quiz; +import com.c4soft.quiz.domain.QuizAuthentication; +import com.c4soft.quiz.domain.exception.NotADraftException; +import com.c4soft.quiz.domain.jpa.QuestionRepository; +import com.c4soft.quiz.domain.jpa.SkillTestRepository; +import com.c4soft.quiz.web.dto.QuestionUpdateDto; +import com.c4soft.quiz.web.dto.mapping.QuestionMapper; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping(path = "/quizzes/{quiz-id}/questions") +@RequiredArgsConstructor +@Validated +@Tag(name = "Quizzes") +public class QuestionController { + private final QuestionRepository questionRepo; + private final QuestionMapper questionMapper; + private final SkillTestRepository skillTestRepo; + + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAuthority('trainer') && #quiz.authorName == authentication.name") + @Transactional(readOnly = false) + @Operation(responses = {@ApiResponse(responseCode = "201", + headers = @Header(name = HttpHeaders.LOCATION, description = "ID of the created question"))}) + public ResponseEntity addQuestion( + @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, + @RequestBody @Valid QuestionUpdateDto dto, QuizAuthentication auth) + throws NotADraftException { + if (quiz.getReplacedBy() != null) { + throw new NotADraftException(quiz.getId()); + } + if (quiz.getIsPublished() && !auth.isModerator()) { + throw new NotADraftException(quiz.getId()); + } + + final var question = questionMapper.update(dto, new Question()); + question.setPriority(quiz.getQuestions().size()); + quiz.add(question); + final var created = questionRepo.save(question); + return ResponseEntity.created(URI.create("%d".formatted(created.getId()))).build(); + } + + @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAuthority('trainer') && #quiz.authorName == authentication.name") + @Transactional(readOnly = false) + @Operation(responses = {@ApiResponse(responseCode = "202")}) + public ResponseEntity updateQuestionsOrder( + @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, + @RequestBody @NotEmpty List questionIds) throws NotADraftException { + if (quiz.getReplacedBy() != null) { + throw new NotADraftException(quiz.getId()); + } + + if (quiz.getQuestions().size() != questionIds.size()) { + return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).build(); + } + final var quizQuestionIds = + quiz.getQuestions().stream().map(Question::getId).collect(Collectors.toSet()); + for (var id : quizQuestionIds) { + if (!questionIds.contains(id)) { + return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).build(); + } + } + quiz.getQuestions().stream().forEach(q -> q.setPriority(questionIds.indexOf(q.getId()))); + questionRepo.saveAllAndFlush(quiz.getQuestions()); + return ResponseEntity.accepted().build(); + } + + @PutMapping(path = "/{question-id}", consumes = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAuthority('trainer') && #quiz.authorName == authentication.name") + @Transactional(readOnly = false) + @Operation(responses = {@ApiResponse(responseCode = "202")}) + public ResponseEntity updateQuestion( + @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, + @Parameter(schema = @Schema(type = "integer")) @PathVariable("question-id") Long questionId, + @RequestBody @Valid QuestionUpdateDto dto, QuizAuthentication auth) { + if (quiz.getReplacedBy() != null) { + throw new NotADraftException(quiz.getId()); + } + if (quiz.getIsPublished() && !auth.isModerator()) { + throw new NotADraftException(quiz.getId()); + } + + final var question = quiz.getQuestion(questionId); + if (question == null) { + return ResponseEntity.notFound().build(); + } + question.setComment(dto.comment()); + question.setLabel(dto.label()); + question.setFormattedBody(dto.formattedBody()); + questionRepo.save(question); + return ResponseEntity.accepted().build(); + } + + @DeleteMapping(path = "/{question-id}") + @PreAuthorize("hasAuthority('trainer') && #quiz.authorName == authentication.name") + @Transactional(readOnly = false) + @Operation(responses = {@ApiResponse(responseCode = "202")}) + public ResponseEntity deleteQuestion( + @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, + @Parameter(schema = @Schema(type = "integer")) @PathVariable("question-id") Long questionId, + QuizAuthentication auth) { + if (quiz.getReplacedBy() != null) { + throw new NotADraftException(quiz.getId()); + } + + final var question = quiz.getQuestion(questionId); + if (question == null) { + return ResponseEntity.notFound().build(); + } + + final var tests = skillTestRepo.findByIdQuizId(quiz.getId()); + tests.forEach(t -> { + final var filtered = t.getChoices().stream().filter(c -> { + return c.getQuestion().getId() != questionId; + }).toList(); + t.setChoices(filtered); + }); + skillTestRepo.saveAll(tests); + + quiz.remove(question); + questionRepo.delete(question); + return ResponseEntity.accepted().build(); + } +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/web/QuizController.java b/api/quiz-api/src/main/java/com/c4soft/quiz/web/QuizController.java index 221476b..04cd870 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/web/QuizController.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/web/QuizController.java @@ -1,13 +1,11 @@ +/* (C)2024 */ package com.c4soft.quiz.web; import java.net.URI; import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.stream.Collectors; - import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -23,25 +21,16 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; - -import com.c4soft.quiz.domain.Choice; -import com.c4soft.quiz.domain.ChoiceRepository; -import com.c4soft.quiz.domain.DraftAlreadyExistsException; -import com.c4soft.quiz.domain.NotADraftException; -import com.c4soft.quiz.domain.Question; -import com.c4soft.quiz.domain.QuestionRepository; import com.c4soft.quiz.domain.Quiz; import com.c4soft.quiz.domain.QuizAuthentication; import com.c4soft.quiz.domain.QuizRejectionDto; -import com.c4soft.quiz.domain.QuizRepository; -import com.c4soft.quiz.domain.SkillTestRepository; -import com.c4soft.quiz.web.dto.ChoiceDto; -import com.c4soft.quiz.web.dto.ChoiceUpdateDto; -import com.c4soft.quiz.web.dto.QuestionDto; -import com.c4soft.quiz.web.dto.QuestionUpdateDto; +import com.c4soft.quiz.domain.exception.DraftAlreadyExistsException; +import com.c4soft.quiz.domain.exception.NotADraftException; +import com.c4soft.quiz.domain.jpa.QuizRepository; +import com.c4soft.quiz.domain.jpa.SkillTestRepository; import com.c4soft.quiz.web.dto.QuizDto; import com.c4soft.quiz.web.dto.QuizUpdateDto; - +import com.c4soft.quiz.web.dto.mapping.QuizMapper; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.headers.Header; @@ -49,7 +38,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; -import jakarta.validation.constraints.NotEmpty; import lombok.RequiredArgsConstructor; @RestController @@ -58,433 +46,196 @@ @Validated @Tag(name = "Quizzes") public class QuizController { - private final QuizRepository quizRepo; - private final QuestionRepository questionRepo; - private final ChoiceRepository choiceRepo; - private final SkillTestRepository skillTestRepo; - - @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) - @Transactional(readOnly = true) - public List getQuizList( - @RequestParam(name = "authorLike", required = false) Optional author, - @RequestParam(name = "titleLike", required = false) Optional title) { - final var spec = QuizRepository.searchSpec(author, title); - final var quizzes = quizRepo.findAll(spec); - return quizzes.stream().map(QuizController::toDto).toList(); - } - - @GetMapping(path = "/submitted", produces = MediaType.APPLICATION_JSON_VALUE) - @PreAuthorize("hasAuthority('moderator')") - @Transactional(readOnly = true) - public List getSubmittedQuizzes() { - final var quizzes = quizRepo.findByIsSubmitted(true); - return quizzes.stream().map(QuizController::toDto).toList(); - } - - @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) - @PreAuthorize("hasAuthority('trainer')") - @Transactional(readOnly = false) - @Operation(responses = { @ApiResponse(responseCode = "201", headers = @Header(name = HttpHeaders.LOCATION, description = "ID of the created quiz")) }) - public ResponseEntity createQuiz(@RequestBody @Valid QuizUpdateDto dto, QuizAuthentication auth) { - final var quiz = new Quiz(dto.title(), auth.getName()); - quiz.setIsChoicesShuffled(dto.isChoicesShuffled()); - quiz.setIsPerQuestionResult(dto.isPerQuestionResult()); - quiz.setIsReplayEnabled(dto.isReplayEnabled()); - quiz.setIsTrainerNotifiedOfNewTests(dto.isTrainerNotifiedOfNewTests()); - final var created = quizRepo.save(quiz); - return ResponseEntity.created(URI.create("%d".formatted(created.getId()))).build(); - } - - @GetMapping(path = "/{quiz-id}", produces = MediaType.APPLICATION_JSON_VALUE) - @Transactional(readOnly = true) - public QuizDto getQuiz(@Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz) { - return toDto(quiz); - } - - @PutMapping(path = "/{quiz-id}", consumes = MediaType.APPLICATION_JSON_VALUE) - @PreAuthorize("hasAuthority('trainer') && #quiz.authorName == authentication.name") - @Transactional(readOnly = false) - @Operation(responses = { @ApiResponse(responseCode = "202") }) - public ResponseEntity updateQuiz( - @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, - @RequestBody @Valid QuizUpdateDto dto, - QuizAuthentication auth) { - updateTitle(quiz, dto.title(), auth); - quiz.setIsChoicesShuffled(dto.isChoicesShuffled()); - quiz.setIsReplayEnabled(dto.isReplayEnabled()); - quiz.setIsPerQuestionResult(dto.isPerQuestionResult()); - quiz.setIsTrainerNotifiedOfNewTests(dto.isTrainerNotifiedOfNewTests()); - quizRepo.save(quiz); - return ResponseEntity.accepted().build(); - } - - void updateTitle(Quiz quiz, String title, QuizAuthentication auth) { - if (Objects.equals(quiz.getTitle(), title)) { - return; - } - if (quiz.getIsPublished() && !auth.isModerator()) { - throw new NotADraftException(quiz.getId()); - } - if (quiz.getReplacedBy() != null) { - throw new NotADraftException(quiz.getId()); - } - quiz.setTitle(title); - } - - @PostMapping(path = "/{quiz-id}/duplicate") - @PreAuthorize("hasAuthority('trainer')") - @Transactional(readOnly = false) - @Operation(responses = { @ApiResponse(responseCode = "201", headers = @Header(name = HttpHeaders.LOCATION, description = "ID of the created quiz")) }) - public ResponseEntity createCopy(@Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, Authentication auth) { - final var draft = new Quiz(quiz, auth.getName()); - final var created = quizRepo.save(draft); - return ResponseEntity.created(URI.create("%d".formatted(created.getId()))).build(); - } - - @PostMapping(path = "/{quiz-id}/draft") - @PreAuthorize("hasAuthority('trainer') && #quiz.authorName == authentication.name") - @Transactional(readOnly = false) - @Operation(responses = { @ApiResponse(responseCode = "201", headers = @Header(name = HttpHeaders.LOCATION, description = "ID of the created quiz")) }) - public ResponseEntity createDraft(@Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, Authentication auth) { - if (quiz.getDraft() != null) { - throw new DraftAlreadyExistsException(quiz.getId()); - } - if (!quiz.getIsPublished()) { - throw new DraftAlreadyExistsException(quiz.getId()); - } - final var draft = new Quiz(quiz, auth.getName()); - draft.setReplaces(quiz); - final var created = quizRepo.save(draft); - quiz.setDraft(created); - quizRepo.save(quiz); - return ResponseEntity.created(URI.create("%d".formatted(created.getId()))).build(); - } - - @PutMapping(path = "/{quiz-id}/submit") - @PreAuthorize("hasAuthority('trainer') && #quiz.authorName == authentication.name") - @Transactional(readOnly = false) - @Operation(responses = { @ApiResponse(responseCode = "202") }) - public ResponseEntity submitDraft(@Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, QuizAuthentication auth) { - if (auth.isModerator()) { - return publishDraft(quiz, auth); - } - quiz.setIsSubmitted(!auth.isModerator()); - quizRepo.save(quiz); - return ResponseEntity.accepted().build(); - } - - @PutMapping(path = "/{quiz-id}/publish") - @PreAuthorize("hasAuthority('moderator')") - @Transactional(readOnly = false) - @Operation(responses = { @ApiResponse(responseCode = "202") }) - public ResponseEntity publishDraft(@Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, Authentication auth) { - quiz.setIsSubmitted(false); - quiz.setIsPublished(true); - quiz.setModeratorComment(null); - quiz.setModeratedBy(auth.getName()); - if (quiz.getReplaces() != null) { - final var replaced = quiz.getReplaces(); - replaced.setReplacedBy(quiz); - replaced.setIsPublished(false); - quiz.setReplaces(null); - quizRepo.save(replaced); - } - - quizRepo.save(quiz); - return ResponseEntity.accepted().build(); - } - - @PutMapping(path = "/{quiz-id}/reject", consumes = MediaType.APPLICATION_JSON_VALUE) - @PreAuthorize("hasAuthority('moderator')") - @Transactional(readOnly = false) - @Operation(responses = { @ApiResponse(responseCode = "202") }) - public ResponseEntity rejectDraft( - @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, - @Valid QuizRejectionDto dto, - Authentication auth) { - quiz.setIsSubmitted(false); - quiz.setIsPublished(false); - quiz.setModeratorComment(dto.message()); - quiz.setModeratedBy(auth.getName()); - quizRepo.save(quiz); - return ResponseEntity.accepted().build(); - } - - @DeleteMapping(path = "/{quiz-id}") - @PreAuthorize("hasAuthority('moderator') || (hasAuthority('trainer') && #quiz.authorName == authentication.name)") - @Transactional(readOnly = false) - @Operation(responses = { @ApiResponse(responseCode = "202") }) - public ResponseEntity deleteQuiz(@Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz) { - final var draft = quiz.getDraft(); - if (draft != null) { - draft.setReplaces(null); - quiz.setDraft(null); - quizRepo.save(draft); - } - - final var replacedBy = quiz.getReplacedBy(); - if (replacedBy != null) { - replacedBy.setReplaces(null); - quiz.setReplacedBy(null); - quizRepo.save(replacedBy); - } - - final var replaces = quiz.getReplaces(); - if (replaces != null) { - if (replaces.getReplacedBy() != null && Objects.equals(replaces.getReplacedBy().getId(), quiz.getId())) { - replaces.setReplacedBy(null); - } - if (replaces.getDraft().getId() != null && Objects.equals(replaces.getDraft().getId(), quiz.getId())) { - replaces.setDraft(null); - } - quiz.setReplaces(null); - quizRepo.save(replaces); - } - - skillTestRepo.deleteByIdQuizId(quiz.getId()); - questionRepo.deleteAll(quiz.getQuestions()); - quiz.getQuestions().clear(); - - quizRepo.delete(quiz); - return ResponseEntity.accepted().build(); - } - - @PostMapping(path = "/{quiz-id}/questions", consumes = MediaType.APPLICATION_JSON_VALUE) - @PreAuthorize("hasAuthority('trainer') && #quiz.authorName == authentication.name") - @Transactional(readOnly = false) - @Operation(responses = { @ApiResponse(responseCode = "201", headers = @Header(name = HttpHeaders.LOCATION, description = "ID of the created question")) }) - public ResponseEntity addQuestion( - @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, - @RequestBody @Valid QuestionUpdateDto dto, - QuizAuthentication auth) { - if (quiz.getReplacedBy() != null) { - throw new NotADraftException(quiz.getId()); - } - if (quiz.getIsPublished() && !auth.isModerator()) { - throw new NotADraftException(quiz.getId()); - } - - final var question = new Question(dto.label(), dto.formattedBody(), quiz.getQuestions().size(), dto.comment()); - quiz.add(question); - final var created = questionRepo.save(question); - return ResponseEntity.created(URI.create("%d".formatted(created.getId()))).build(); - } - - @PutMapping(path = "/{quiz-id}/questions", consumes = MediaType.APPLICATION_JSON_VALUE) - @PreAuthorize("hasAuthority('trainer') && #quiz.authorName == authentication.name") - @Transactional(readOnly = false) - @Operation(responses = { @ApiResponse(responseCode = "202") }) - public ResponseEntity updateQuestionsOrder( - @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, - @RequestBody @NotEmpty List questionIds) { - if (quiz.getReplacedBy() != null) { - throw new NotADraftException(quiz.getId()); - } - - if (quiz.getQuestions().size() != questionIds.size()) { - return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).build(); - } - final var quizQuestionIds = quiz.getQuestions().stream().map(Question::getId).collect(Collectors.toSet()); - for (var id : quizQuestionIds) { - if (!questionIds.contains(id)) { - return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).build(); - } - } - quiz.getQuestions().stream().forEach(q -> q.setPriority(questionIds.indexOf(q.getId()))); - quiz = quizRepo.saveAndFlush(quiz); - return ResponseEntity.accepted().build(); - } - - @PutMapping(path = "/{quiz-id}/questions/{question-id}", consumes = MediaType.APPLICATION_JSON_VALUE) - @PreAuthorize("hasAuthority('trainer') && #quiz.authorName == authentication.name") - @Transactional(readOnly = false) - @Operation(responses = { @ApiResponse(responseCode = "202") }) - public ResponseEntity updateQuestion( - @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, - @Parameter(schema = @Schema(type = "integer")) @PathVariable("question-id") Long questionId, - @RequestBody @Valid QuestionUpdateDto dto, - QuizAuthentication auth) { - if (quiz.getReplacedBy() != null) { - throw new NotADraftException(quiz.getId()); - } - if (quiz.getIsPublished() && !auth.isModerator()) { - throw new NotADraftException(quiz.getId()); - } - - final var question = quiz.getQuestion(questionId); - if (question == null) { - return ResponseEntity.notFound().build(); - } - question.setComment(dto.comment()); - question.setLabel(dto.label()); - question.setFormattedBody(dto.formattedBody()); - questionRepo.save(question); - return ResponseEntity.accepted().build(); - } - - @DeleteMapping(path = "/{quiz-id}/questions/{question-id}") - @PreAuthorize("hasAuthority('trainer') && #quiz.authorName == authentication.name") - @Transactional(readOnly = false) - @Operation(responses = { @ApiResponse(responseCode = "202") }) - public ResponseEntity deleteQuestion( - @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, - @Parameter(schema = @Schema(type = "integer")) @PathVariable("question-id") Long questionId, - QuizAuthentication auth) { - if (quiz.getReplacedBy() != null) { - throw new NotADraftException(quiz.getId()); - } - - final var question = quiz.getQuestion(questionId); - if (question == null) { - return ResponseEntity.notFound().build(); - } - - final var tests = skillTestRepo.findByIdQuizId(quiz.getId()); - tests.forEach(t -> { - final var filtered = t.getChoices().stream().filter(c -> { - return c.getQuestion().getId() != questionId; - }).toList(); - t.setChoices(filtered); - }); - skillTestRepo.saveAll(tests); - - quiz.remove(question); - quizRepo.save(quiz); - choiceRepo.deleteAll(question.getChoices()); - questionRepo.delete(question); - return ResponseEntity.accepted().build(); - } - - @PostMapping(path = "/{quiz-id}/questions/{question-id}/choices", consumes = MediaType.APPLICATION_JSON_VALUE) - @PreAuthorize("hasAuthority('trainer') && #quiz.authorName == authentication.name") - @Transactional(readOnly = false) - @Operation(responses = { @ApiResponse(responseCode = "201", headers = @Header(name = HttpHeaders.LOCATION, description = "ID of the created choice")) }) - public ResponseEntity addChoice( - @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, - @Parameter(schema = @Schema(type = "integer")) @PathVariable("question-id") Long questionId, - @RequestBody @Valid ChoiceUpdateDto dto, - QuizAuthentication auth) { - if (quiz.getReplacedBy() != null) { - throw new NotADraftException(quiz.getId()); - } - if (quiz.getIsPublished() && !auth.isModerator()) { - throw new NotADraftException(quiz.getId()); - } - - final var question = quiz.getQuestion(questionId); - if (question == null) { - return ResponseEntity.notFound().build(); - } - final var choice = new Choice(dto.label(), dto.isGood()); - question.add(choice); - final var created = choiceRepo.save(choice); - return ResponseEntity.created(URI.create("%d".formatted(created.getId()))).build(); - } - - @PutMapping(path = "/{quiz-id}/questions/{question-id}/choices/{choice-id}", consumes = MediaType.APPLICATION_JSON_VALUE) - @PreAuthorize("hasAuthority('trainer') && #quiz.authorName == authentication.name") - @Transactional(readOnly = false) - @Operation(responses = { @ApiResponse(responseCode = "202") }) - public ResponseEntity updateChoice( - @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, - @Parameter(schema = @Schema(type = "integer")) @PathVariable("question-id") Long questionId, - @Parameter(schema = @Schema(type = "integer")) @PathVariable("choice-id") Long choiceId, - @RequestBody @Valid ChoiceUpdateDto dto, - QuizAuthentication auth) { - if (quiz.getReplacedBy() != null) { - throw new NotADraftException(quiz.getId()); - } - if (quiz.getIsPublished() && !auth.isModerator()) { - throw new NotADraftException(quiz.getId()); - } - - final var question = quiz.getQuestion(questionId); - if (question == null) { - return ResponseEntity.notFound().build(); - } - final var choice = question.getChoice(choiceId); - if (choice == null) { - return ResponseEntity.notFound().build(); - } - if (!Objects.equals(dto.label(), choice.getLabel()) && !auth.isModerator() && (quiz.getIsPublished() || quiz.getReplacedBy() != null)) { - throw new NotADraftException(quiz.getId()); - } - choice.setIsGood(dto.isGood()); - choice.setLabel(dto.label()); - choiceRepo.save(choice); - return ResponseEntity.accepted().build(); - } - - @DeleteMapping(path = "/{quiz-id}/questions/{question-id}/choices/{choice-id}") - @PreAuthorize("hasAuthority('trainer') && #quiz.authorName == authentication.name") - @Transactional(readOnly = false) - @Operation(responses = { @ApiResponse(responseCode = "202") }) - public ResponseEntity deleteChoice( - @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, - @Parameter(schema = @Schema(type = "integer")) @PathVariable("question-id") Long questionId, - @Parameter(schema = @Schema(type = "integer")) @PathVariable("choice-id") Long choiceId, - QuizAuthentication auth) { - if (quiz.getReplacedBy() != null) { - throw new NotADraftException(quiz.getId()); - } - - final var question = quiz.getQuestion(questionId); - if (question == null) { - return ResponseEntity.notFound().build(); - } - final var choice = question.getChoice(choiceId); - if (choice == null) { - return ResponseEntity.notFound().build(); - } - - final var tests = skillTestRepo.findByIdQuizId(quiz.getId()); - tests.forEach(t -> { - final var filtered = t.getChoices().stream().filter(c -> { - return c.getId() != choiceId; - }).toList(); - t.setChoices(filtered); - }); - skillTestRepo.saveAll(tests); - - question.remove(choice); - choiceRepo.delete(choice); - questionRepo.save(question); - return ResponseEntity.accepted().build(); - } - - private static QuizDto toDto(Quiz q) { - return q == null - ? null - : new QuizDto( - q.getId(), - q.getTitle(), - q.getQuestions().stream().sorted((a, b) -> a.getPriority() - b.getPriority()).map(QuizController::toDto).toList(), - q.getAuthorName(), - q.getIsPublished(), - q.getIsSubmitted(), - q.getReplacedBy() != null, - q.getIsChoicesShuffled(), - q.getIsReplayEnabled(), - q.getIsPerQuestionResult(), - q.getIsTrainerNotifiedOfNewTests(), - q.getModeratorComment(), - q.getDraft() == null ? null : q.getDraft().getId(), - q.getReplaces() == null ? null : q.getReplaces().getId()); - } - - private static QuestionDto toDto(Question q) { - return q == null - ? null - : new QuestionDto( - q.getQuiz().getId(), - q.getId(), - q.getLabel(), - q.getFormattedBody(), - q.getChoices().stream().map(QuizController::toDto).toList(), - q.getComment()); - } - - private static ChoiceDto toDto(Choice c) { - return c == null ? null : new ChoiceDto(c.getQuestion().getQuiz().getId(), c.getQuestion().getId(), c.getId(), c.getLabel(), c.getIsGood()); - } + private final QuizRepository quizRepo; + private final SkillTestRepository skillTestRepo; + private final QuizMapper quizMapper; + + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + @Transactional(readOnly = true) + public List getQuizList( + @RequestParam(name = "authorLike", required = false) Optional author, + @RequestParam(name = "titleLike", required = false) Optional title) { + final var spec = QuizRepository.searchSpec(author, title); + final var quizzes = quizRepo.findAll(spec); + return quizzes.stream().map(quizMapper::toDto).toList(); + } + + @GetMapping(path = "/submitted", produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAuthority('moderator')") + @Transactional(readOnly = true) + public List getSubmittedQuizzes() { + final var quizzes = quizRepo.findByIsSubmitted(true); + return quizzes.stream().map(quizMapper::toDto).toList(); + } + + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAuthority('trainer')") + @Transactional(readOnly = false) + @Operation(responses = {@ApiResponse(responseCode = "201", + headers = @Header(name = HttpHeaders.LOCATION, description = "ID of the created quiz"))}) + public ResponseEntity createQuiz(@RequestBody @Valid QuizUpdateDto dto, + QuizAuthentication auth) { + final var quiz = quizMapper.update(dto, new Quiz()); + quiz.setAuthorName(auth.getName()); + final var created = quizRepo.save(quiz); + return ResponseEntity.created(URI.create("%d".formatted(created.getId()))).build(); + } + + @GetMapping(path = "/{quiz-id}", produces = MediaType.APPLICATION_JSON_VALUE) + @Transactional(readOnly = true) + public QuizDto getQuiz( + @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz) { + return quizMapper.toDto(quiz); + } + + @PutMapping(path = "/{quiz-id}", consumes = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAuthority('trainer') && #quiz.authorName == authentication.name") + @Transactional(readOnly = false) + @Operation(responses = {@ApiResponse(responseCode = "202")}) + public ResponseEntity updateQuiz( + @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, + @RequestBody @Valid QuizUpdateDto dto, QuizAuthentication auth) throws NotADraftException { + checkCanUpdate(quiz, auth); + quizRepo.save(quizMapper.update(dto, quiz)); + return ResponseEntity.accepted().build(); + } + + void checkCanUpdate(Quiz quiz, QuizAuthentication auth) throws NotADraftException { + if (quiz.getIsPublished() && !auth.isModerator()) { + throw new NotADraftException(quiz.getId()); + } + if (quiz.getReplacedBy() != null) { + throw new NotADraftException(quiz.getId()); + } + } + + @PostMapping(path = "/{quiz-id}/duplicate") + @PreAuthorize("hasAuthority('trainer')") + @Transactional(readOnly = false) + @Operation(responses = {@ApiResponse(responseCode = "201", + headers = @Header(name = HttpHeaders.LOCATION, description = "ID of the created quiz"))}) + public ResponseEntity createCopy( + @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, + Authentication auth) { + final var draft = new Quiz(quiz, auth.getName()); + final var created = quizRepo.save(draft); + return ResponseEntity.created(URI.create("%d".formatted(created.getId()))).build(); + } + + @PostMapping(path = "/{quiz-id}/draft") + @PreAuthorize("hasAuthority('trainer') && #quiz.authorName == authentication.name") + @Transactional(readOnly = false) + @Operation(responses = {@ApiResponse(responseCode = "201", + headers = @Header(name = HttpHeaders.LOCATION, description = "ID of the created quiz"))}) + public ResponseEntity createDraft( + @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, + Authentication auth) throws DraftAlreadyExistsException { + if (quiz.getDraft() != null) { + throw new DraftAlreadyExistsException(quiz.getId()); + } + if (!quiz.getIsPublished()) { + throw new DraftAlreadyExistsException(quiz.getId()); + } + final var draft = new Quiz(quiz, auth.getName()); + draft.setReplaces(quiz); + final var created = quizRepo.save(draft); + quiz.setDraft(created); + quizRepo.save(quiz); + return ResponseEntity.created(URI.create("%d".formatted(created.getId()))).build(); + } + + @PutMapping(path = "/{quiz-id}/submit") + @PreAuthorize("hasAuthority('trainer') && #quiz.authorName == authentication.name") + @Transactional(readOnly = false) + @Operation(responses = {@ApiResponse(responseCode = "202")}) + public ResponseEntity submitDraft( + @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, + QuizAuthentication auth) { + if (auth.isModerator()) { + return publishDraft(quiz, auth); + } + quiz.setIsSubmitted(!auth.isModerator()); + quizRepo.save(quiz); + return ResponseEntity.accepted().build(); + } + + @PutMapping(path = "/{quiz-id}/publish") + @PreAuthorize("hasAuthority('moderator')") + @Transactional(readOnly = false) + @Operation(responses = {@ApiResponse(responseCode = "202")}) + public ResponseEntity publishDraft( + @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, + Authentication auth) { + quiz.setIsSubmitted(false); + quiz.setIsPublished(true); + quiz.setModeratorComment(null); + quiz.setModeratedBy(auth.getName()); + if (quiz.getReplaces() != null) { + final var replaced = quiz.getReplaces(); + replaced.setReplacedBy(quiz); + replaced.setIsPublished(false); + quiz.setReplaces(null); + quizRepo.save(replaced); + } + + quizRepo.save(quiz); + return ResponseEntity.accepted().build(); + } + + @PutMapping(path = "/{quiz-id}/reject", consumes = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAuthority('moderator')") + @Transactional(readOnly = false) + @Operation(responses = {@ApiResponse(responseCode = "202")}) + public ResponseEntity rejectDraft( + @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz, + @Valid QuizRejectionDto dto, Authentication auth) { + quiz.setIsSubmitted(false); + quiz.setIsPublished(false); + quiz.setModeratorComment(dto.message()); + quiz.setModeratedBy(auth.getName()); + quizRepo.save(quiz); + return ResponseEntity.accepted().build(); + } + + @DeleteMapping(path = "/{quiz-id}") + @PreAuthorize("hasAuthority('moderator') || (hasAuthority('trainer') && #quiz.authorName ==" + + " authentication.name)") + @Transactional(readOnly = false) + @Operation(responses = {@ApiResponse(responseCode = "202")}) + public ResponseEntity deleteQuiz( + @Parameter(schema = @Schema(type = "integer")) @PathVariable("quiz-id") Quiz quiz) { + final var draft = quiz.getDraft(); + if (draft != null) { + draft.setReplaces(null); + quiz.setDraft(null); + quizRepo.save(draft); + } + + final var replacedBy = quiz.getReplacedBy(); + if (replacedBy != null) { + replacedBy.setReplaces(null); + quiz.setReplacedBy(null); + quizRepo.save(replacedBy); + } + + final var replaces = quiz.getReplaces(); + if (replaces != null) { + if (replaces.getReplacedBy() != null + && Objects.equals(replaces.getReplacedBy().getId(), quiz.getId())) { + replaces.setReplacedBy(null); + } + if (replaces.getDraft().getId() != null + && Objects.equals(replaces.getDraft().getId(), quiz.getId())) { + replaces.setDraft(null); + } + quiz.setReplaces(null); + quizRepo.save(replaces); + } + + skillTestRepo.deleteByIdQuizId(quiz.getId()); + quiz.getQuestions().clear(); + + quizRepo.delete(quiz); + return ResponseEntity.accepted().build(); + } } diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/web/SkillTestController.java b/api/quiz-api/src/main/java/com/c4soft/quiz/web/SkillTestController.java index fb5f5c6..f8fc935 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/web/SkillTestController.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/web/SkillTestController.java @@ -1,3 +1,4 @@ +/* (C)2024 */ package com.c4soft.quiz.web; import java.net.URI; @@ -6,7 +7,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; - +import org.keycloak.admin.api.UsersApi; +import org.keycloak.admin.model.UserRepresentation; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -23,22 +25,19 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; - import com.c4soft.quiz.domain.Choice; import com.c4soft.quiz.domain.Quiz; import com.c4soft.quiz.domain.QuizAuthentication; -import com.c4soft.quiz.domain.QuizRepository; import com.c4soft.quiz.domain.SkillTest; import com.c4soft.quiz.domain.SkillTest.SkillTestPk; -import com.c4soft.quiz.domain.SkillTestRepository; -import com.c4soft.quiz.feign.KeycloakAdminApiClient; -import com.c4soft.quiz.feign.KeycloakAdminApiClient.UserRepresentation; +import com.c4soft.quiz.domain.exception.InvalidQuizException; +import com.c4soft.quiz.domain.exception.QuizAlreadyHasAnAnswerException; +import com.c4soft.quiz.domain.jpa.QuizRepository; +import com.c4soft.quiz.domain.jpa.SkillTestRepository; import com.c4soft.quiz.web.dto.SkillTestDto; import com.c4soft.quiz.web.dto.SkillTestQuestionDto; import com.c4soft.quiz.web.dto.SkillTestResultDetailsDto; import com.c4soft.quiz.web.dto.SkillTestResultPreviewDto; - -import feign.FeignException; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; @@ -56,181 +55,181 @@ @Tag(name = "SkillTest") @Log4j2 public class SkillTestController { - private final SkillTestRepository testRepo; - private final QuizRepository quizRepo; - private final KeycloakAdminApiClient keycloakAdminApi; - private final JavaMailSender mailSender; - @Value("${ui-external-uri}") - URI uiUri; - - @GetMapping(path = "/{quizId}", produces = MediaType.APPLICATION_JSON_VALUE) - @Transactional(readOnly = true) - @Operation(description = "Returns the answers to a quiz, by default for all trainees over the last 2 weeks") - public List getSkillTestList( - @PathVariable(value = "quizId", required = true) Long quizId, - @RequestParam(value = "since", required = false) Optional since, - @RequestParam(value = "until", required = false) Optional until) { - final var resolvedSince = since.map(sec -> 1000 * sec).orElse(Instant.now().minus(14, ChronoUnit.DAYS).toEpochMilli()); - final var tests = testRepo.findAll(SkillTestRepository.spec(quizId, resolvedSince, until.map(sec -> 1000 * sec))).stream(); - return tests.map(this::toPreviewDto).toList(); - } - - @GetMapping(path = "/{quizId}/{traineeName}", produces = MediaType.APPLICATION_JSON_VALUE) - @Transactional(readOnly = true) - @Operation(description = "Returns the answers to a quiz, by default for all trainees over the last 2 weeks") - public SkillTestResultDetailsDto getSkillTest( - @PathVariable(value = "quizId", required = true) Long quizId, - @PathVariable(value = "traineeName", required = true) String traineeName) { - final var skillTest = testRepo.findById(new SkillTestPk(quizId, traineeName)); - if (skillTest.isEmpty()) { - throw new EntityNotFoundException("No skill-test for quiz %d and trainee %s".formatted(quizId, traineeName)); - } - return toDetailsDto(skillTest.get()); - } - - @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - @PreAuthorize("isAuthenticated()") - @Transactional(readOnly = false) - @Operation(responses = { @ApiResponse(responseCode = "202") }) - public ResponseEntity submitSkillTest(@RequestBody @Valid SkillTestDto dto, QuizAuthentication auth) { - final var quiz = quizRepo.findById(dto.quizId()).orElseThrow(() -> new InvalidQuizException(dto.quizId())); - if (!quiz.getIsPublished()) { - throw new InvalidQuizException(dto.quizId()); - } - if (!quiz.getIsReplayEnabled() && testRepo.findByIdQuizIdAndIdTraineeName(dto.quizId(), auth.getName()).isPresent()) { - throw new QuizAlreadyHasAnAnswerException(dto.quizId(), auth.getName()); - } - final var traineeChoices = new ArrayList(); - for (var question : quiz.getQuestions()) { - final var questionDto = dto.getQuestion(question.getId()); - for (var choice : question.getChoices()) { - if (questionDto.choices().contains(choice.getId())) { - traineeChoices.add(choice); - } - } - } - final var test = testRepo.findByIdQuizIdAndIdTraineeName(quiz.getId(), auth.getName()).orElse(new SkillTest(quiz, auth.getName(), traineeChoices)); - test.setSubmittedOn(Instant.now().toEpochMilli()); - test.setChoices(traineeChoices); - final var saved = testRepo.save(test); - - final var testUri = "%s/tests/%d/%s".formatted(uiUri, quiz.getId(), auth.getName()); - try { - SimpleMailMessage message = new SimpleMailMessage(); - message.setFrom("noreply@c4-soft.com"); - message.setTo(auth.getAttributes().getEmail()); - message.setSubject("C4 - Quiz: your answer to %s".formatted(quiz.getTitle())); - message.setText(testUri); - mailSender.send(message); - - if (quiz.getIsTrainerNotifiedOfNewTests()) { - final var authors = keycloakAdminApi.getUser(quiz.getAuthorName(), true); - if (authors.size() == 1) { - message.setTo(authors.get(0).getEmail()); - message.setSubject("C4 - Quiz: New answer to %s by %s".formatted(quiz.getTitle(), auth.getName())); - mailSender.send(message); - } - } - } catch (Exception e) { - log.error(e); - } - - return ResponseEntity.accepted().location(URI.create(testUri)).body(toPreviewDto(saved)); - } - - @DeleteMapping(path = "/{quizId}/{traineeName}") - @PreAuthorize("authentication.name == #quiz.authorName") - @Transactional(readOnly = false) - @Operation( - responses = { @ApiResponse(responseCode = "202") }, - description = "Deletes the answer to given quiz for given trainee. Only the author of a quiz can delete skill-tests.") - public ResponseEntity deleteSkillTest( - @Parameter(schema = @Schema(type = "integer")) @PathVariable(value = "quizId", required = true) Quiz quiz, - @PathVariable(value = "traineeName", required = true) String traineeName) { - testRepo.deleteById(new SkillTestPk(quiz.getId(), traineeName)); - return ResponseEntity.accepted().build(); - } - - private SkillTestResultPreviewDto toPreviewDto(SkillTest traineeAnswer) { - var score = 0; - var totalChoices = 0; - final var quiz = quizRepo - .findById(traineeAnswer.getId().getQuizId()) - .orElseThrow(() -> new EntityNotFoundException("Unknown quiz: %d".formatted(traineeAnswer.getId().getQuizId()))); - final var testDto = new SkillTestDto(traineeAnswer.getId().getQuizId(), new ArrayList<>()); - for (var question : quiz.getQuestions()) { - final var traineeQuestionChoices = traineeAnswer.getChoices(question.getId()); - final var questionDto = new SkillTestQuestionDto(question.getId(), new ArrayList<>()); - testDto.questions().add(questionDto); - for (var choice : question.getChoices()) { - totalChoices += 1; - if (traineeQuestionChoices.contains(choice)) { - questionDto.choices().add(choice.getId()); - score += choice.getIsGood() ? 1 : -1; - } else { - score += choice.getIsGood() ? 0 : 1; - } - } - } - - return new SkillTestResultPreviewDto(traineeAnswer.getId().getTraineeName(), totalChoices == 0 ? null : 100.0 * score / totalChoices); - } - - private SkillTestResultDetailsDto toDetailsDto(SkillTest traineeAnswer) { - var score = 0; - var totalChoices = 0; - final var quiz = quizRepo - .findById(traineeAnswer.getId().getQuizId()) - .orElseThrow(() -> new EntityNotFoundException("Unknown quiz: %d".formatted(traineeAnswer.getId().getQuizId()))); - final var testDto = new SkillTestDto(traineeAnswer.getId().getQuizId(), new ArrayList<>()); - for (var question : quiz.getQuestions()) { - final var traineeQuestionChoices = traineeAnswer.getChoices(question.getId()); - final var questionDto = new SkillTestQuestionDto(question.getId(), new ArrayList<>()); - testDto.questions().add(questionDto); - for (var choice : question.getChoices()) { - totalChoices += 1; - if (traineeQuestionChoices.contains(choice)) { - questionDto.choices().add(choice.getId()); - score += choice.getIsGood() ? 1 : -1; - } else { - score += choice.getIsGood() ? 0 : 1; - } - } - } - List users = List.of(); - try { - users = keycloakAdminApi.getUser(traineeAnswer.getId().getTraineeName(), true); - } catch (FeignException e) { - log.error("Failed to fetch trainee data", e); - } - final var email = users.size() == 1 ? users.get(0).getEmail() : ""; - final var firstName = users.size() == 1 ? users.get(0).getFirtsName() : ""; - final var lastName = users.size() == 1 ? users.get(0).getLastName() : ""; - return new SkillTestResultDetailsDto( - testDto, - traineeAnswer.getId().getTraineeName(), - firstName, - lastName, - email, - totalChoices == 0 ? null : 100.0 * score / totalChoices); - } - - static class InvalidQuizException extends RuntimeException { - private static final long serialVersionUID = 8816930385638385805L; - - InvalidQuizException(Long quizId) { - super("Quiz %d doesn't accept answers anymore.".formatted(quizId)); - } - } - - static class QuizAlreadyHasAnAnswerException extends RuntimeException { - - private static final long serialVersionUID = 6171083302507116601L; - - QuizAlreadyHasAnAnswerException(Long quizId, String traineeName) { - super( - "Quiz %d already has an answer for %s and doesn't accept replay. Ask the trainer to delete the answer before submitting a new one." - .formatted(quizId, traineeName)); - } - } + private final SkillTestRepository testRepo; + private final QuizRepository quizRepo; + private final UsersApi keycloakUsersApi; + private final JavaMailSender mailSender; + + @Value("${ui-external-uri}") + URI uiUri; + + @GetMapping(path = "/{quizId}", produces = MediaType.APPLICATION_JSON_VALUE) + @Transactional(readOnly = true) + @Operation( + description = "Returns the answers to a quiz, by default for all trainees over the last 2" + + " weeks") + public List getSkillTestList( + @PathVariable(value = "quizId", required = true) Long quizId, + @RequestParam(value = "since", required = false) Optional since, + @RequestParam(value = "until", required = false) Optional until) { + final var resolvedSince = since.map(sec -> 1000 * sec) + .orElse(Instant.now().minus(14, ChronoUnit.DAYS).toEpochMilli()); + final var tests = testRepo + .findAll(SkillTestRepository.spec(quizId, resolvedSince, until.map(sec -> 1000 * sec))) + .stream(); + return tests.map(this::toPreviewDto).toList(); + } + + @GetMapping(path = "/{quizId}/{traineeName}", produces = MediaType.APPLICATION_JSON_VALUE) + @Transactional(readOnly = true) + @Operation( + description = "Returns a given trainee answers to a quiz, by default over the last 2" + + " weeks") + public SkillTestResultDetailsDto getSkillTest( + @PathVariable(value = "quizId", required = true) Long quizId, + @PathVariable(value = "traineeName", required = true) String traineeName) { + final var skillTest = testRepo.findById(new SkillTestPk(quizId, traineeName)); + if (skillTest.isEmpty()) { + throw new EntityNotFoundException( + "No skill-test for quiz %d and trainee %s".formatted(quizId, traineeName)); + } + return toDetailsDto(skillTest.get()); + } + + @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("isAuthenticated()") + @Transactional(readOnly = false) + @Operation(responses = {@ApiResponse(responseCode = "202")}) + public ResponseEntity submitSkillTest( + @RequestBody @Valid SkillTestDto dto, QuizAuthentication auth) { + final var quiz = + quizRepo.findById(dto.quizId()).orElseThrow(() -> new InvalidQuizException(dto.quizId())); + if (!quiz.getIsPublished()) { + throw new InvalidQuizException(dto.quizId()); + } + if (!quiz.getIsReplayEnabled() + && testRepo.findByIdQuizIdAndIdTraineeName(dto.quizId(), auth.getName()).isPresent()) { + throw new QuizAlreadyHasAnAnswerException(dto.quizId(), auth.getName()); + } + final var traineeChoices = new ArrayList(); + for (var question : quiz.getQuestions()) { + final var questionDto = dto.getQuestion(question.getId()); + for (var choice : question.getChoices()) { + if (questionDto.choices().contains(choice.getId())) { + traineeChoices.add(choice); + } + } + } + final var test = testRepo.findByIdQuizIdAndIdTraineeName(quiz.getId(), auth.getName()) + .orElse(new SkillTest(quiz, auth.getName(), traineeChoices)); + test.setSubmittedOn(Instant.now().toEpochMilli()); + test.setChoices(traineeChoices); + final var saved = testRepo.save(test); + + final var testUri = "%s/tests/%d/%s".formatted(uiUri, quiz.getId(), auth.getName()); + try { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom("noreply@c4-soft.com"); + message.setTo(auth.getAttributes().getEmail()); + message.setSubject("C4 - Quiz: your answer to %s".formatted(quiz.getTitle())); + message.setText(testUri); + mailSender.send(message); + + if (quiz.getIsTrainerNotifiedOfNewTests()) { + final var authors = getUsers(quiz.getAuthorName()); + if (authors.size() == 1) { + message.setTo(authors.get(0).getEmail()); + message.setSubject( + "C4 - Quiz: New answer to %s by %s".formatted(quiz.getTitle(), auth.getName())); + mailSender.send(message); + } + } + } catch (Exception e) { + log.error(e); + } + + return ResponseEntity.accepted().location(URI.create(testUri)).body(toPreviewDto(saved)); + } + + @DeleteMapping(path = "/{quizId}/{traineeName}") + @PreAuthorize("authentication.name == #quiz.authorName") + @Transactional(readOnly = false) + @Operation(responses = {@ApiResponse(responseCode = "202")}, + description = "Deletes the answer to given quiz for given trainee. Only the author of a quiz" + + " can delete skill-tests.") + public ResponseEntity deleteSkillTest( + @Parameter(schema = @Schema(type = "integer")) @PathVariable(value = "quizId", + required = true) Quiz quiz, + @PathVariable(value = "traineeName", required = true) String traineeName) { + testRepo.deleteById(new SkillTestPk(quiz.getId(), traineeName)); + return ResponseEntity.accepted().build(); + } + + private SkillTestResultPreviewDto toPreviewDto(SkillTest traineeAnswer) { + var score = 0; + var totalChoices = 0; + final var quiz = quizRepo.findById(traineeAnswer.getId().getQuizId()) + .orElseThrow(() -> new EntityNotFoundException( + "Unknown quiz: %d".formatted(traineeAnswer.getId().getQuizId()))); + final var testDto = new SkillTestDto(traineeAnswer.getId().getQuizId(), new ArrayList<>()); + for (var question : quiz.getQuestions()) { + final var traineeQuestionChoices = traineeAnswer.getChoices(question.getId()); + final var questionDto = new SkillTestQuestionDto(question.getId(), new ArrayList<>()); + testDto.questions().add(questionDto); + for (var choice : question.getChoices()) { + totalChoices += 1; + if (traineeQuestionChoices.contains(choice)) { + questionDto.choices().add(choice.getId()); + score += choice.getIsGood() ? 1 : -1; + } else { + score += choice.getIsGood() ? 0 : 1; + } + } + } + + return new SkillTestResultPreviewDto(traineeAnswer.getId().getTraineeName(), + totalChoices == 0 ? null : 100.0 * score / totalChoices); + } + + private SkillTestResultDetailsDto toDetailsDto(SkillTest traineeAnswer) { + var score = 0; + var totalChoices = 0; + final var quiz = quizRepo.findById(traineeAnswer.getId().getQuizId()) + .orElseThrow(() -> new EntityNotFoundException( + "Unknown quiz: %d".formatted(traineeAnswer.getId().getQuizId()))); + final var testDto = new SkillTestDto(traineeAnswer.getId().getQuizId(), new ArrayList<>()); + for (var question : quiz.getQuestions()) { + final var traineeQuestionChoices = traineeAnswer.getChoices(question.getId()); + final var questionDto = new SkillTestQuestionDto(question.getId(), new ArrayList<>()); + testDto.questions().add(questionDto); + for (var choice : question.getChoices()) { + totalChoices += 1; + if (traineeQuestionChoices.contains(choice)) { + questionDto.choices().add(choice.getId()); + score += choice.getIsGood() ? 1 : -1; + } else { + score += choice.getIsGood() ? 0 : 1; + } + } + } + List users = List.of(); + try { + users = getUsers(traineeAnswer.getId().getTraineeName()); + } catch (Exception e) { + log.error("Failed to fetch trainee data", e); + } + final var email = users.size() == 1 ? users.get(0).getEmail() : ""; + final var firstName = users.size() == 1 ? users.get(0).getFirstName() : ""; + final var lastName = users.size() == 1 ? users.get(0).getLastName() : ""; + return new SkillTestResultDetailsDto(testDto, traineeAnswer.getId().getTraineeName(), firstName, + lastName, email, totalChoices == 0 ? null : 100.0 * score / totalChoices); + } + + private List getUsers(final String username) { + return keycloakUsersApi.adminRealmsRealmUsersGet("quiz", Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty(), Optional.of(true), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.of(username)).getBody(); + } } diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/web/UsersController.java b/api/quiz-api/src/main/java/com/c4soft/quiz/web/UsersController.java index f10f5c4..579164e 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/web/UsersController.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/web/UsersController.java @@ -1,3 +1,4 @@ +/* (C)2024 */ package com.c4soft.quiz.web; import org.springframework.http.MediaType; @@ -7,10 +8,8 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; - import com.c4soft.quiz.domain.QuizAuthentication; import com.c4soft.quiz.web.dto.UserInfoDto; - import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -21,14 +20,14 @@ @Tag(name = "Users") public class UsersController { - @GetMapping(path = "/me", produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_PROBLEM_JSON_VALUE }) - public UserInfoDto getMe(Authentication auth) { - if (auth instanceof QuizAuthentication quizAuth) { - return new UserInfoDto( - quizAuth.getName(), - quizAuth.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList(), - quizAuth.getAttributes().getExpiresAt().getEpochSecond()); - } - return UserInfoDto.ANONYMOUS; - } + @GetMapping(path = "/me", + produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_PROBLEM_JSON_VALUE}) + public UserInfoDto getMe(Authentication auth) { + if (auth instanceof QuizAuthentication quizAuth) { + return new UserInfoDto(quizAuth.getName(), + quizAuth.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList(), + quizAuth.getAttributes().getExpiresAt().getEpochSecond()); + } + return UserInfoDto.ANONYMOUS; + } } diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/ChoiceDto.java b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/ChoiceDto.java index 3bede8d..b53f612 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/ChoiceDto.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/ChoiceDto.java @@ -1,8 +1,9 @@ +/* (C)2024 */ package com.c4soft.quiz.web.dto; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; -public record ChoiceDto(@NotNull Long quizId, @NotNull Long questionId, @NotNull Long choiceId, @NotEmpty String label, - boolean isGood) { -} \ No newline at end of file +public record ChoiceDto(@NotNull Long quizId, @NotNull Long questionId, @NotNull Long choiceId, + @NotEmpty String label, boolean isGood) { +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/ChoiceUpdateDto.java b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/ChoiceUpdateDto.java index 7b095de..469d73c 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/ChoiceUpdateDto.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/ChoiceUpdateDto.java @@ -1,7 +1,8 @@ +/* (C)2024 */ package com.c4soft.quiz.web.dto; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; public record ChoiceUpdateDto(@NotEmpty @Size(max = 255) String label, boolean isGood) { -} \ No newline at end of file +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/QuestionDto.java b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/QuestionDto.java index 53bd78c..2113f1b 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/QuestionDto.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/QuestionDto.java @@ -1,14 +1,9 @@ +/* (C)2024 */ package com.c4soft.quiz.web.dto; import java.util.List; - import jakarta.validation.constraints.NotNull; -public record QuestionDto( - @NotNull Long quizId, - @NotNull Long questionId, - String label, - String formattedBody, - List choices, - @NotNull String comment) { -} \ No newline at end of file +public record QuestionDto(@NotNull Long quizId, @NotNull Long questionId, @NotNull Long priority, + String label, String formattedBody, List choices, @NotNull String comment) { +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/QuestionUpdateDto.java b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/QuestionUpdateDto.java index c05e661..2f1247f 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/QuestionUpdateDto.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/QuestionUpdateDto.java @@ -1,3 +1,4 @@ +/* (C)2024 */ package com.c4soft.quiz.web.dto; import jakarta.validation.constraints.NotEmpty; @@ -8,8 +9,6 @@ * @parameter label the new label for the question * @parameter comment a new explanation for the right answer */ -public record QuestionUpdateDto( - @NotEmpty @Size(max = 255) String label, - @NotNull @Size(max = 2047) String formattedBody, - @NotNull @Size(max = 2047) String comment) { -} \ No newline at end of file +public record QuestionUpdateDto(@NotEmpty @Size(max = 255) String label, + @NotNull @Size(max = 2047) String formattedBody, @NotNull @Size(max = 2047) String comment) { +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/QuizDto.java b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/QuizDto.java index f2791e1..ac04a39 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/QuizDto.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/QuizDto.java @@ -1,40 +1,35 @@ +/* (C)2024 */ package com.c4soft.quiz.web.dto; import java.util.List; - import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; /** - * @param id unique identifier for the quiz - * @param title the quiz title - * @param questions an array with all of the quiz questions - * @param authorName name of the trainer who authored the quiz - * @param isPublished is the quiz available for trainees - * @param isSubmitted is the quiz submitted to moderation - * @param isReplaced was a new version of this quiz published (and replaces this one) - * @param isChoicesShuffled should the choices display order be shuffled from a question display to another - * @param isReplayEnabled can a trainee submit a new answer before his former one was deleted by the trainer - * @param isPerQuestionResult should the right answer as well as comment be displayed as soon as choices are validated for a question or only when the - * skill test was accepted by the server - * @param isTrainerNotifiedOfNewTests if true, trainers receive an email each time a new skill-test is submitted (for quizzes they authored only) - * @param ModeratorComment an explanation why this quiz version was rejected by a moderator - * @param draftId unique identifier for a modified version of this quiz - * @param replacesId identifier of the former version of this quiz + * @param id unique identifier for the quiz + * @param title the quiz title + * @param questions an array with all of the quiz questions + * @param authorName name of the trainer who authored the quiz + * @param isPublished is the quiz available for trainees + * @param isSubmitted is the quiz submitted to moderation + * @param isReplaced was a new version of this quiz published (and replaces this one) + * @param isChoicesShuffled should the choices display order be shuffled from a question display to + * another + * @param isReplayEnabled can a trainee submit a new answer before his former one was deleted by the + * trainer + * @param isPerQuestionResult should the right answer as well as comment be displayed as soon as + * choices are validated for a question or only when the skill test was accepted by the + * server + * @param isTrainerNotifiedOfNewTests if true, trainers receive an email each time a new skill-test + * is submitted (for quizzes they authored only) + * @param ModeratorComment an explanation why this quiz version was rejected by a moderator + * @param draftId unique identifier for a modified version of this quiz + * @param replacesId identifier of the former version of this quiz */ -public record QuizDto( - @NotNull Long id, - @NotEmpty String title, - @NotNull List questions, - @NotEmpty String authorName, - @NotNull Boolean isPublished, - @NotNull Boolean isSubmitted, - @NotNull Boolean isReplaced, - @NotNull Boolean isChoicesShuffled, - @NotNull Boolean isReplayEnabled, - @NotNull Boolean isPerQuestionResult, - @NotNull Boolean isTrainerNotifiedOfNewTests, - String ModeratorComment, - Long draftId, - Long replacesId) { -} \ No newline at end of file +public record QuizDto(@NotNull Long id, @NotEmpty String title, + @NotNull List questions, @NotEmpty String authorName, @NotNull Boolean isPublished, + @NotNull Boolean isSubmitted, @NotNull Boolean isReplaced, @NotNull Boolean isChoicesShuffled, + @NotNull Boolean isReplayEnabled, @NotNull Boolean isPerQuestionResult, + @NotNull Boolean isTrainerNotifiedOfNewTests, String moderatorComment, Long draftId, + Long replacesId) { +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/QuizUpdateDto.java b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/QuizUpdateDto.java index f99b394..89ff7b2 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/QuizUpdateDto.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/QuizUpdateDto.java @@ -1,3 +1,4 @@ +/* (C)2024 */ package com.c4soft.quiz.web.dto; import jakarta.validation.constraints.NotEmpty; @@ -5,17 +6,17 @@ import jakarta.validation.constraints.Size; /** - * @param title the new title for the quiz - * @param isChoicesShuffled should choices display order be randomized from a display to another. - * @param isReplayEnabled can a trainee submit a new skill test before the former one was deleted by the trainer. - * @param isPerQuestionResult if true, the right answer as well as comment should be displayed as soon as choices for a question are validated. - * Otherwise, it should be displayed only when the test was accepted by the server. - * @param isTrainerNotifiedOfNewTests if true, trainers receive an email each time a new skill-test is submitted (for quizzes they authored only) + * @param title the new title for the quiz + * @param isChoicesShuffled should choices display order be randomized from a display to another. + * @param isReplayEnabled can a trainee submit a new skill test before the former one was deleted by + * the trainer. + * @param isPerQuestionResult if true, the right answer as well as comment should be displayed as + * soon as choices for a question are validated. Otherwise, it should be displayed only when + * the test was accepted by the server. + * @param isTrainerNotifiedOfNewTests if true, trainers receive an email each time a new skill-test + * is submitted (for quizzes they authored only) */ -public record QuizUpdateDto( - @NotEmpty @Size(max = 255) String title, - @NotNull Boolean isChoicesShuffled, - @NotNull Boolean isReplayEnabled, - @NotNull Boolean isPerQuestionResult, - @NotNull Boolean isTrainerNotifiedOfNewTests) { -} \ No newline at end of file +public record QuizUpdateDto(@NotEmpty @Size(max = 255) String title, + @NotNull Boolean isChoicesShuffled, @NotNull Boolean isReplayEnabled, + @NotNull Boolean isPerQuestionResult, @NotNull Boolean isTrainerNotifiedOfNewTests) { +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/SkillTestDto.java b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/SkillTestDto.java index bfdd3f0..a7d135f 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/SkillTestDto.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/SkillTestDto.java @@ -1,13 +1,14 @@ +/* (C)2024 */ package com.c4soft.quiz.web.dto; import java.util.List; import java.util.Objects; - import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; public record SkillTestDto(@NotNull Long quizId, @NotEmpty List questions) { - public SkillTestQuestionDto getQuestion(Long questionId) { - return questions.stream().filter(q -> Objects.equals(q.questionId(), questionId)).findAny().orElse(new SkillTestQuestionDto(questionId, List.of())); - } -} \ No newline at end of file + public SkillTestQuestionDto getQuestion(Long questionId) { + return questions.stream().filter(q -> Objects.equals(q.questionId(), questionId)).findAny() + .orElse(new SkillTestQuestionDto(questionId, List.of())); + } +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/SkillTestQuestionDto.java b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/SkillTestQuestionDto.java index 15b69ce..da3bbef 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/SkillTestQuestionDto.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/SkillTestQuestionDto.java @@ -1,8 +1,7 @@ +/* (C)2024 */ package com.c4soft.quiz.web.dto; -import java.util.List; - import jakarta.validation.constraints.NotNull; +import java.util.List; -public record SkillTestQuestionDto(@NotNull Long questionId, List choices) { -} \ No newline at end of file +public record SkillTestQuestionDto(@NotNull Long questionId, List choices) {} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/SkillTestResultDetailsDto.java b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/SkillTestResultDetailsDto.java index afbcf8e..0cf1690 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/SkillTestResultDetailsDto.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/SkillTestResultDetailsDto.java @@ -1,12 +1,9 @@ +/* (C)2024 */ package com.c4soft.quiz.web.dto; import jakarta.validation.constraints.NotNull; -public record SkillTestResultDetailsDto( - @NotNull SkillTestDto test, - @NotNull String traineeUsername, - @NotNull String traineeFirstName, - @NotNull String traineeLastName, - @NotNull String traineeEmail, - @NotNull Double score) { -} \ No newline at end of file +public record SkillTestResultDetailsDto(@NotNull SkillTestDto test, @NotNull String traineeUsername, + @NotNull String traineeFirstName, @NotNull String traineeLastName, @NotNull String traineeEmail, + @NotNull Double score) { +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/SkillTestResultPreviewDto.java b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/SkillTestResultPreviewDto.java index 5bbc194..833a4d1 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/SkillTestResultPreviewDto.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/SkillTestResultPreviewDto.java @@ -1,5 +1,7 @@ +/* (C)2024 */ package com.c4soft.quiz.web.dto; import jakarta.validation.constraints.NotNull; -public record SkillTestResultPreviewDto(@NotNull String traineeName, @NotNull Double score) {} \ No newline at end of file +public record SkillTestResultPreviewDto(@NotNull String traineeName, @NotNull Double score) { +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/UserInfoDto.java b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/UserInfoDto.java index 4a74faa..dc20b0b 100644 --- a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/UserInfoDto.java +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/UserInfoDto.java @@ -1,14 +1,15 @@ +/* (C)2024 */ package com.c4soft.quiz.web.dto; -import java.util.List; - import jakarta.validation.constraints.NotNull; +import java.util.List; /** * @param username the user unique name * @param roles the user roles * @param exp seconds since epoch time at which access will expire */ -public record UserInfoDto(@NotNull String username, @NotNull List roles, @NotNull Long exp) { - public static final UserInfoDto ANONYMOUS = new UserInfoDto("", List.of(), Long.MAX_VALUE); -} \ No newline at end of file +public record UserInfoDto( + @NotNull String username, @NotNull List roles, @NotNull Long exp) { + public static final UserInfoDto ANONYMOUS = new UserInfoDto("", List.of(), Long.MAX_VALUE); +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/mapping/ChoiceMapper.java b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/mapping/ChoiceMapper.java new file mode 100644 index 0000000..4015619 --- /dev/null +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/mapping/ChoiceMapper.java @@ -0,0 +1,21 @@ +package com.c4soft.quiz.web.dto.mapping; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import com.c4soft.quiz.domain.Choice; +import com.c4soft.quiz.web.dto.ChoiceDto; +import com.c4soft.quiz.web.dto.ChoiceUpdateDto; + +@Mapper(componentModel = "spring") +public interface ChoiceMapper { + + @Mapping(target = "choiceId", source = "id") + @Mapping(target = "questionId", source = "question.id") + @Mapping(target = "quizId", source = "question.quiz.id") + ChoiceDto toDto(Choice entity); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "question", ignore = true) + Choice update(ChoiceUpdateDto dto, @MappingTarget Choice entity); +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/mapping/QuestionMapper.java b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/mapping/QuestionMapper.java new file mode 100644 index 0000000..86d0191 --- /dev/null +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/mapping/QuestionMapper.java @@ -0,0 +1,24 @@ +package com.c4soft.quiz.web.dto.mapping; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import com.c4soft.quiz.domain.Question; +import com.c4soft.quiz.web.dto.QuestionDto; +import com.c4soft.quiz.web.dto.QuestionUpdateDto; + +@Mapper(componentModel = "spring", uses = {ChoiceMapper.class}) +public interface QuestionMapper { + + @Mapping(target = "questionId", source = "id") + @Mapping(target = "quizId", source = "quiz.id") + QuestionDto toDto(Question entity); + + @Mapping(target = "add", ignore = true) + @Mapping(target = "remove", ignore = true) + @Mapping(target = "id", ignore = true) + @Mapping(target = "priority", ignore = true) + @Mapping(target = "quiz", ignore = true) + @Mapping(target = "choices", ignore = true) + Question update(QuestionUpdateDto dto, @MappingTarget Question entity); +} diff --git a/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/mapping/QuizMapper.java b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/mapping/QuizMapper.java new file mode 100644 index 0000000..740c510 --- /dev/null +++ b/api/quiz-api/src/main/java/com/c4soft/quiz/web/dto/mapping/QuizMapper.java @@ -0,0 +1,32 @@ +package com.c4soft.quiz.web.dto.mapping; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import com.c4soft.quiz.domain.Quiz; +import com.c4soft.quiz.web.dto.QuizDto; +import com.c4soft.quiz.web.dto.QuizUpdateDto; + +@Mapper(componentModel = "spring", uses = {QuestionMapper.class}) +public interface QuizMapper { + + @Mapping(target = "isReplaced", expression = "java(entity.getReplacedBy() != null)") + @Mapping(target = "draftId", source = "draft.id") + @Mapping(target = "replacesId", source = "replaces.id") + QuizDto toDto(Quiz entity); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "add", ignore = true) + @Mapping(target = "remove", ignore = true) + @Mapping(target = "authorName", ignore = true) + @Mapping(target = "draft", ignore = true) + @Mapping(target = "isPublished", ignore = true) + @Mapping(target = "isSubmitted", ignore = true) + @Mapping(target = "moderatedBy", ignore = true) + @Mapping(target = "moderatorComment", ignore = true) + @Mapping(target = "replacedBy", ignore = true) + @Mapping(target = "replaces", ignore = true) + @Mapping(target = "questions", ignore = true) + @Mapping(target = "skillTests", ignore = true) + Quiz update(QuizUpdateDto dto, @MappingTarget Quiz entity); +} diff --git a/api/quiz-api/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/api/quiz-api/src/main/resources/META-INF/additional-spring-configuration-metadata.json index bcafbb0..38e14f5 100644 --- a/api/quiz-api/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/api/quiz-api/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1,4 +1,14 @@ {"properties": [ + { + "name": "scheme", + "type": "java.lang.String", + "description": "'http' or 'https', used to build URIs involved in authentication and can be used in 'gateway-uri', 'bao-loc-api-uri' or 'ui-uri'" + }, + { + "name": "hostname", + "type": "java.lang.String", + "description": "the host name" + }, { "name": "oauth2-issuer", "type": "java.net.URI", diff --git a/api/quiz-api/src/main/resources/application.yml b/api/quiz-api/src/main/resources/application.yml index 5f26a9b..758b13d 100644 --- a/api/quiz-api/src/main/resources/application.yml +++ b/api/quiz-api/src/main/resources/application.yml @@ -1,10 +1,11 @@ scheme: http -keycloak-host: https://oidc.c4-soft.com/auth +hostname: localhost +keycloak-host: http://${hostname}/auth keycloak-realm: quiz oauth2-issuer: ${keycloak-host}/realms/${keycloak-realm} oauth2-client-id: quiz-admin -oauth2-client-secret: change-me -ui-external-uri: ${scheme}://localhost:8080/ui +oauth2-client-secret: secret +ui-external-uri: ${scheme}://${hostname}/ui server: port: 7084 @@ -22,9 +23,9 @@ spring: lifecycle: timeout-per-shutdown-phase: 30s datasource: - url: jdbc:postgresql://localhost:5432/quiz + url: jdbc:postgresql://${hostname}:5432/quiz username: quiz - password: change-me + password: secret jpa: database-platform: org.hibernate.dialect.PostgreSQLDialect hibernate: @@ -32,15 +33,6 @@ spring: show-sql: false liquibase: change-log: classpath:db/changelog/db.changelog-master.xml - cloud: - openfeign: - client: - config: - quiz-admin: - url: ${keycloak-host}/admin/realms/${keycloak-realm} - oauth2: - enabled: true - clientRegistrationId: quiz-admin mail: host: smtp.gmail.com port: 587 @@ -64,9 +56,7 @@ spring: client-id: ${oauth2-client-id} client-secret: ${oauth2-client-secret} authorization-grant-type: client_credentials - scope: - - openid - - offline_access + scope: "openid" com: c4-soft: @@ -84,6 +74,13 @@ com: - "/actuator/health/readiness" - "/actuator/health/liveness" - "/v3/api-docs/**" + rest: + client: + keycloak-admin-api: + base-url: ${keycloak-host} + authorization: + oauth2: + oauth2-registration-id: quiz-admin management: endpoint: diff --git a/api/quiz-api/src/test/java/com/c4soft/quiz/EnableSpringDataWebSupportTestConf.java b/api/quiz-api/src/test/java/com/c4soft/quiz/EnableSpringDataWebSupportTestConf.java index a25276f..99694f2 100644 --- a/api/quiz-api/src/test/java/com/c4soft/quiz/EnableSpringDataWebSupportTestConf.java +++ b/api/quiz-api/src/test/java/com/c4soft/quiz/EnableSpringDataWebSupportTestConf.java @@ -1,5 +1,12 @@ +/* (C)2024 */ package com.c4soft.quiz; +import com.c4soft.quiz.domain.Choice; +import com.c4soft.quiz.domain.Question; +import com.c4soft.quiz.domain.Quiz; +import com.c4soft.quiz.domain.jpa.ChoiceRepository; +import com.c4soft.quiz.domain.jpa.QuestionRepository; +import com.c4soft.quiz.domain.jpa.QuizRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureDataJpa; import org.springframework.boot.test.context.TestConfiguration; @@ -7,13 +14,6 @@ import org.springframework.format.FormatterRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import com.c4soft.quiz.domain.Choice; -import com.c4soft.quiz.domain.ChoiceRepository; -import com.c4soft.quiz.domain.Question; -import com.c4soft.quiz.domain.QuestionRepository; -import com.c4soft.quiz.domain.Quiz; -import com.c4soft.quiz.domain.QuizRepository; - /** * Avoid MethodArgumentConversionNotSupportedException with mocked repos * @@ -22,25 +22,25 @@ @TestConfiguration @AutoConfigureDataJpa public class EnableSpringDataWebSupportTestConf { - @Autowired - QuizRepository quizRepo; - - @Autowired - QuestionRepository questionRepo; - - @Autowired - ChoiceRepository choiceRepo; + @Autowired QuizRepository quizRepo; + + @Autowired QuestionRepository questionRepo; + + @Autowired ChoiceRepository choiceRepo; - @Bean - WebMvcConfigurer configurer() { - return new WebMvcConfigurer() { + @Bean + WebMvcConfigurer configurer() { + return new WebMvcConfigurer() { - @Override - public void addFormatters(FormatterRegistry registry) { - registry.addConverter(Long.class, Quiz.class, id -> quizRepo.findById(id).orElse(null)); - registry.addConverter(Long.class, Question.class, id -> questionRepo.findById(id).orElse(null)); - registry.addConverter(Long.class, Choice.class, id -> choiceRepo.findById(id).orElse(null)); - } - }; - } -} \ No newline at end of file + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addConverter( + Long.class, Quiz.class, id -> quizRepo.findById(id).orElse(null)); + registry.addConverter( + Long.class, Question.class, id -> questionRepo.findById(id).orElse(null)); + registry.addConverter( + Long.class, Choice.class, id -> choiceRepo.findById(id).orElse(null)); + } + }; + } +} diff --git a/api/quiz-api/src/test/java/com/c4soft/quiz/QuizApiApplicationTest.java b/api/quiz-api/src/test/java/com/c4soft/quiz/QuizApiApplicationTest.java index fbca4df..824ac26 100644 --- a/api/quiz-api/src/test/java/com/c4soft/quiz/QuizApiApplicationTest.java +++ b/api/quiz-api/src/test/java/com/c4soft/quiz/QuizApiApplicationTest.java @@ -1,3 +1,4 @@ +/* (C)2024 */ package com.c4soft.quiz; import static org.hamcrest.CoreMatchers.hasItems; @@ -8,237 +9,236 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; -import java.util.Map; - +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.keycloak.admin.api.UsersApi; +import org.keycloak.admin.model.UserRepresentation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MvcResult; - import com.c4_soft.springaddons.security.oauth2.test.annotations.WithJwt; import com.c4_soft.springaddons.security.oauth2.test.webmvc.AddonsWebmvcTestConf; import com.c4_soft.springaddons.security.oauth2.test.webmvc.MockMvcSupport; import com.c4soft.quiz.domain.Choice; -import com.c4soft.quiz.domain.ChoiceRepository; import com.c4soft.quiz.domain.Question; -import com.c4soft.quiz.domain.QuestionRepository; import com.c4soft.quiz.domain.Quiz; -import com.c4soft.quiz.domain.QuizRepository; -import com.c4soft.quiz.feign.KeycloakAdminApiClient; -import com.c4soft.quiz.feign.KeycloakAdminApiClient.UserRepresentation; +import com.c4soft.quiz.domain.jpa.ChoiceRepository; +import com.c4soft.quiz.domain.jpa.QuestionRepository; +import com.c4soft.quiz.domain.jpa.QuizRepository; import com.c4soft.quiz.web.dto.ChoiceUpdateDto; import com.c4soft.quiz.web.dto.QuestionUpdateDto; import com.c4soft.quiz.web.dto.QuizUpdateDto; import com.c4soft.quiz.web.dto.SkillTestDto; import com.c4soft.quiz.web.dto.SkillTestQuestionDto; - import net.minidev.json.JSONArray; import net.minidev.json.JSONObject; import net.minidev.json.parser.JSONParser; import net.minidev.json.parser.ParseException; -@SpringBootTest +@SpringBootTest(properties = {"hostname=localhost"}) @ActiveProfiles("h2") @AutoConfigureMockMvc @Import(AddonsWebmvcTestConf.class) class QuizApiApplicationTest { - @Autowired - QuizRepository quizRepo; - - @Autowired - QuestionRepository questionRepo; - - @Autowired - ChoiceRepository choiceRepo; - - @Autowired - MockMvcSupport api; - - @MockBean - KeycloakAdminApiClient keycloakAdminApi; - - @BeforeEach - void setUp() { - quizRepo.save(Fixtures.openIdTraingQuiz()); - } - - @Test - @WithJwt("ch4mp.json") - void givenUserIsCh4mp_whenGoingThroughQuizNominalCrudOperations_thenOk() throws UnsupportedEncodingException, Exception { - // Database is initialized with 1 quiz - var actual = parse(api.get("/quizzes").andExpect(status().isOk()).andReturn(), JSONArray.class); - assertEquals(1L, actual.size()); - assertEquals("OAuth2 and OpenID in web echosystem with Spring", ((JSONObject) actual.get(0)).get("title")); - - // Create a draft from existing quiz - final var draftId = api - .post(null, "/quizzes/{quiz-id}/draft", ((JSONObject) actual.get(0)).get("id")) - .andExpect(status().isCreated()) - .andReturn() - .getResponse() - .getHeader(HttpHeaders.LOCATION); - actual = parse(api.get("/quizzes").andExpect(status().isOk()).andReturn(), JSONArray.class); - assertEquals(2L, actual.size()); - - // Update draft title - api.put(new QuizUpdateDto("Updated title", true, true, true, false), "/quizzes/{quiz-id}", draftId).andExpect(status().isAccepted()); - - // Submit draft to moderation (with auto-publication as ch4mp is both trainer and moderator - api.put(null, "/quizzes/{quiz-id}/submit", draftId).andExpect(status().isAccepted()); - actual = parse(api.get("/quizzes").andExpect(status().isOk()).andReturn(), JSONArray.class); - assertEquals(2L, actual.size()); - assertEquals(1L, actual.stream().filter(q -> (boolean) ((JSONObject) q).get("isPublished")).toList().size()); - - // Create a new quiz - final var quiz1Id = api - .post(new QuizUpdateDto("Second Quiz", true, true, true, false), "/quizzes") - .andExpect(status().isCreated()) - .andReturn() - .getResponse() - .getHeader(HttpHeaders.LOCATION); - api.get("/quizzes?titleLike=second").andExpect(status().isOk()).andExpect(jsonPath("$.*.title", hasItems("Second Quiz"))); - - // Add 2 questions to the new quiz - final var question10Id = api - .post(new QuestionUpdateDto("machin", "", "truc"), "/quizzes/{quiz-id}/questions", quiz1Id) - .andExpect(status().isCreated()) - .andReturn() - .getResponse() - .getHeader(HttpHeaders.LOCATION); - final var question11Id = api - .post(new QuestionUpdateDto("bidule", "", "chode"), "/quizzes/{quiz-id}/questions", quiz1Id) - .andExpect(status().isCreated()) - .andReturn() - .getResponse() - .getHeader(HttpHeaders.LOCATION); - api.get("/quizzes/{quiz-id}", quiz1Id).andExpect(status().isOk()).andExpect(jsonPath("$.questions.*.label", hasItems("machin", "bidule"))); - - // Reverse questions order - api.put(List.of(question11Id, question10Id), "/quizzes/{quiz-id}/questions", quiz1Id).andExpect(status().isAccepted()); - - // Update the 1st question title and comment - api - .put( - new QuestionUpdateDto("What is the answer to machin?", "", "The answer is truc."), - "/quizzes/{quiz-id}/questions/{question-id}", - quiz1Id, - question10Id) - .andExpect(status().isAccepted()); - api - .get("/quizzes/{quiz-id}", quiz1Id) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.questions[0].label", is("bidule"))) - .andExpect(jsonPath("$.questions[1].label", is("What is the answer to machin?"))); - - // Add 2 choices - final var choice100Id = api - .post(new ChoiceUpdateDto("truc", true), "/quizzes/{quiz-id}/questions/{question-id}/choices", quiz1Id, question10Id) - .andExpect(status().isCreated()) - .andReturn() - .getResponse() - .getHeader(HttpHeaders.LOCATION); - final var choice101Id = api - .post(new ChoiceUpdateDto("chouette", false), "/quizzes/{quiz-id}/questions/{question-id}/choices", quiz1Id, question10Id) - .andExpect(status().isCreated()) - .andReturn() - .getResponse() - .getHeader(HttpHeaders.LOCATION); - api.get("/quizzes/{quiz-id}", quiz1Id).andExpect(status().isOk()).andExpect(jsonPath("$.questions[1].choices.*.label", hasItems("truc", "chouette"))); - api - .put(new ChoiceUpdateDto("chose", true), "/quizzes/{quiz-id}/questions/{question-id}/choices/{choice-id}", quiz1Id, question10Id, choice101Id) - .andExpect(status().isAccepted()); - api.get("/quizzes/{quiz-id}", quiz1Id).andExpect(status().isOk()).andExpect(jsonPath("$.questions[1].choices.*.label", hasItems("truc", "chose"))); - - // Submit draft to moderation (with auto-publication as ch4mp is both trainer and moderator - api.put(null, "/quizzes/{quiz-id}/submit", quiz1Id).andExpect(status().isAccepted()); - actual = parse(api.get("/quizzes").andExpect(status().isOk()).andReturn(), JSONArray.class); - assertEquals(3L, actual.size()); - assertEquals(2L, actual.stream().filter(q -> (boolean) ((JSONObject) q).get("isPublished")).toList().size()); - - // Delete a question - api.delete("/quizzes/{quiz-id}/questions/{question-id}/choices/{choice-id}", quiz1Id, question10Id, choice100Id).andExpect(status().isAccepted()); - api.get("/quizzes/{quiz-id}", quiz1Id).andExpect(status().isOk()).andExpect(jsonPath("$.questions[1].choices.*.label", hasSize(1))); - - // Delete the other question - api.delete("/quizzes/{quiz-id}/questions/{question-id}", quiz1Id, question10Id).andExpect(status().isAccepted()); - api.get("/quizzes/{quiz-id}", quiz1Id).andExpect(status().isOk()).andExpect(jsonPath("$.questions.*.label", hasSize(1))); - - // Delete the new quiz - api.delete("/quizzes/{quiz-id}", quiz1Id).andExpect(status().isAccepted()); - api.get("/quizzes").andExpect(status().isOk()).andExpect(jsonPath("$", hasSize(2))); - api.get("/quizzes/{quiz-id}", draftId).andExpect(status().isOk()).andExpect(jsonPath("$.title", is("Updated title"))); - api.get("/quizzes/{quiz-id}", quiz1Id).andExpect(status().isNotFound()); - } - - @Test - @WithJwt("tonton-pirate.json") - void givenUserIsATrainee_whenGoingThroughSkillTestNominalOperations_thenOk() throws UnsupportedEncodingException, Exception { - var quiz = (JSONObject) parse(api.get("/quizzes").andExpect(status().isOk()).andReturn(), JSONArray.class).get(0); - final var quizId = Long.valueOf(quiz.get("id").toString()); - - final var worstPossibleAnswer = new SkillTestDto(quizId, new ArrayList<>()); - final var perfectAnswer = new SkillTestDto(quizId, new ArrayList<>()); - for (var q : (JSONArray) quiz.get("questions")) { - final var question = (JSONObject) q; - final var questionId = Long.valueOf(question.get("questionId").toString()); - final var questionBestAnswer = new SkillTestQuestionDto(questionId, new ArrayList<>()); - final var questionWorstAnswer = new SkillTestQuestionDto(questionId, new ArrayList<>()); - for (var c : (JSONArray) question.get("choices")) { - final var choice = (JSONObject) c; - final var choiceId = Long.valueOf(choice.get("choiceId").toString()); - if ((Boolean) choice.get("isGood")) { - questionBestAnswer.choices().add(choiceId); - } else { - questionWorstAnswer.choices().add(choiceId); - } - } - perfectAnswer.questions().add(questionBestAnswer); - worstPossibleAnswer.questions().add(questionWorstAnswer); - } - - api.put(worstPossibleAnswer, "/skill-tests").andExpect(status().isAccepted()).andExpect(jsonPath("$.score", is(-50.0))); - when(keycloakAdminApi.getUser("tonton-pirate", true)).thenReturn(List.of(new UserRepresentation(Map.of("email", "tonton-pirate@c4-soft.com")))); - api - .perform(get("/skill-tests/{quizId}/{traineeName}", quizId.toString(), "tonton-pirate")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.score", is(-50.0))); - - api.put(perfectAnswer, "/skill-tests").andExpect(status().isAccepted()).andExpect(jsonPath("$.score", is(100.0))); - api - .perform(get("/skill-tests/{quizId}/{traineeName}", quizId.toString(), "tonton-pirate")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.score", is(100.0))); - - } - - private T parse(MvcResult result, Class clazz) throws UnsupportedEncodingException, ParseException { - return new JSONParser(JSONParser.MODE_PERMISSIVE).parse(result.getResponse().getContentAsString(), clazz); - } - - public static class Fixtures { - static final String trainer1 = "ch4mp"; - static final String moderator1 = "ch4mp"; - - public static Quiz openIdTraingQuiz() { - final var quiz = new Quiz( - "OAuth2 and OpenID in web echosystem with Spring", - trainer1, - new Question("Question 1", "", 0, "Good is right", new Choice("good", true), new Choice("bad", false)), - new Question("Question 2", "", 1, "Bad is wrong", new Choice("bad", false), new Choice("good", true))); - quiz.setModeratedBy(moderator1); - quiz.setIsPublished(true); - return quiz; - } - } - + @Autowired + QuizRepository quizRepo; + + @Autowired + QuestionRepository questionRepo; + + @Autowired + ChoiceRepository choiceRepo; + + @Autowired + MockMvcSupport api; + + @MockBean + UsersApi keycloakAdminApi; + + @BeforeEach + void setUp() { + quizRepo.deleteAll(); + quizRepo.save(Fixtures.openIdTraingQuiz()); + } + + @Test + @WithJwt("ch4mp.json") + void givenUserIsCh4mp_whenGoingThroughQuizNominalCrudOperations_thenOk() + throws UnsupportedEncodingException, Exception { + // Database is initialized with 1 quiz + var actual = parse(api.get("/quizzes").andExpect(status().isOk()).andReturn(), JSONArray.class); + assertEquals(1L, actual.size()); + assertEquals("OAuth2 and OpenID in web echosystem with Spring", + ((JSONObject) actual.get(0)).get("title")); + + // Create a draft from existing quiz + final var draftId = api + .post(null, "/quizzes/{quiz-id}/draft", ((JSONObject) actual.get(0)).get("id")) + .andExpect(status().isCreated()).andReturn().getResponse().getHeader(HttpHeaders.LOCATION); + actual = parse(api.get("/quizzes").andExpect(status().isOk()).andReturn(), JSONArray.class); + assertEquals(2L, actual.size()); + + // Update draft title + api.put(new QuizUpdateDto("Updated title", true, true, true, false), "/quizzes/{quiz-id}", + draftId).andExpect(status().isAccepted()); + + // Submit draft to moderation (with auto-publication as ch4mp is both trainer and moderator + api.put(null, "/quizzes/{quiz-id}/submit", draftId).andExpect(status().isAccepted()); + actual = parse(api.get("/quizzes").andExpect(status().isOk()).andReturn(), JSONArray.class); + assertEquals(2L, actual.size()); + assertEquals(1L, + actual.stream().filter(q -> (boolean) ((JSONObject) q).get("isPublished")).toList().size()); + + // Create a new quiz + final var quiz1Id = api + .post(new QuizUpdateDto("Second Quiz", true, true, true, false), "/quizzes") + .andExpect(status().isCreated()).andReturn().getResponse().getHeader(HttpHeaders.LOCATION); + api.get("/quizzes?titleLike=second").andExpect(status().isOk()) + .andExpect(jsonPath("$.*.title", hasItems("Second Quiz"))); + + // Add 2 questions to the new quiz + final var question10Id = api + .post(new QuestionUpdateDto("machin", "", "truc"), "/quizzes/{quiz-id}/questions", quiz1Id) + .andExpect(status().isCreated()).andReturn().getResponse().getHeader(HttpHeaders.LOCATION); + final var question11Id = api + .post(new QuestionUpdateDto("bidule", "", "chode"), "/quizzes/{quiz-id}/questions", quiz1Id) + .andExpect(status().isCreated()).andReturn().getResponse().getHeader(HttpHeaders.LOCATION); + api.get("/quizzes/{quiz-id}", quiz1Id).andExpect(status().isOk()) + .andExpect(jsonPath("$.questions.*.label", hasItems("machin", "bidule"))); + + // Reverse questions order + api.put(List.of(question11Id, question10Id), "/quizzes/{quiz-id}/questions", quiz1Id) + .andExpect(status().isAccepted()); + + // Update the 1st question title and comment + api.put(new QuestionUpdateDto("What is the answer to machin?", "", "The answer is truc."), + "/quizzes/{quiz-id}/questions/{question-id}", quiz1Id, question10Id) + .andExpect(status().isAccepted()); + api.get("/quizzes/{quiz-id}", quiz1Id).andExpect(status().isOk()) + .andExpect(jsonPath("$.questions[1].label", is("What is the answer to machin?"))) + .andExpect(jsonPath("$.questions[1].priority", is(1))) + .andExpect(jsonPath("$.questions[0].label", is("bidule"))) + .andExpect(jsonPath("$.questions[0].priority", is(0))); + + // Add 2 choices + final var choice100Id = api + .post(new ChoiceUpdateDto("truc", true), + "/quizzes/{quiz-id}/questions/{question-id}/choices", quiz1Id, question10Id) + .andExpect(status().isCreated()).andReturn().getResponse().getHeader(HttpHeaders.LOCATION); + final var choice101Id = api + .post(new ChoiceUpdateDto("chouette", false), + "/quizzes/{quiz-id}/questions/{question-id}/choices", quiz1Id, question10Id) + .andExpect(status().isCreated()).andReturn().getResponse().getHeader(HttpHeaders.LOCATION); + api.get("/quizzes/{quiz-id}", quiz1Id).andExpect(status().isOk()) + .andExpect(jsonPath("$.questions[1].choices.*.label", hasItems("truc", "chouette"))); + api.put(new ChoiceUpdateDto("chose", true), + "/quizzes/{quiz-id}/questions/{question-id}/choices/{choice-id}", quiz1Id, question10Id, + choice101Id).andExpect(status().isAccepted()); + api.get("/quizzes/{quiz-id}", quiz1Id).andExpect(status().isOk()) + .andExpect(jsonPath("$.questions[1].choices.*.label", hasItems("truc", "chose"))); + + // Submit draft to moderation (with auto-publication as ch4mp is both trainer and moderator + api.put(null, "/quizzes/{quiz-id}/submit", quiz1Id).andExpect(status().isAccepted()); + actual = parse(api.get("/quizzes").andExpect(status().isOk()).andReturn(), JSONArray.class); + assertEquals(3L, actual.size()); + assertEquals(2L, + actual.stream().filter(q -> (boolean) ((JSONObject) q).get("isPublished")).toList().size()); + + // Delete a question + api.delete("/quizzes/{quiz-id}/questions/{question-id}/choices/{choice-id}", quiz1Id, + question10Id, choice100Id).andExpect(status().isAccepted()); + api.get("/quizzes/{quiz-id}", quiz1Id).andExpect(status().isOk()) + .andExpect(jsonPath("$.questions[1].choices.*.label", hasSize(1))); + + // Delete the other question + api.delete("/quizzes/{quiz-id}/questions/{question-id}", quiz1Id, question10Id) + .andExpect(status().isAccepted()); + api.get("/quizzes/{quiz-id}", quiz1Id).andExpect(status().isOk()) + .andExpect(jsonPath("$.questions.*.label", hasSize(1))); + + // Delete the new quiz + api.delete("/quizzes/{quiz-id}", quiz1Id).andExpect(status().isAccepted()); + api.get("/quizzes").andExpect(status().isOk()).andExpect(jsonPath("$", hasSize(2))); + api.get("/quizzes/{quiz-id}", draftId).andExpect(status().isOk()) + .andExpect(jsonPath("$.title", is("Updated title"))); + api.get("/quizzes/{quiz-id}", quiz1Id).andExpect(status().isNotFound()); + } + + @Test + @WithJwt("trainee.json") + void givenUserIsATrainee_whenGoingThroughSkillTestNominalOperations_thenOk() + throws UnsupportedEncodingException, Exception { + var quiz = (JSONObject) parse(api.get("/quizzes").andExpect(status().isOk()).andReturn(), + JSONArray.class).get(0); + final var quizId = Long.valueOf(quiz.get("id").toString()); + + final var worstPossibleAnswer = new SkillTestDto(quizId, new ArrayList<>()); + final var perfectAnswer = new SkillTestDto(quizId, new ArrayList<>()); + for (var q : (JSONArray) quiz.get("questions")) { + final var question = (JSONObject) q; + final var questionId = Long.valueOf(question.get("questionId").toString()); + final var questionBestAnswer = new SkillTestQuestionDto(questionId, new ArrayList<>()); + final var questionWorstAnswer = new SkillTestQuestionDto(questionId, new ArrayList<>()); + for (var c : (JSONArray) question.get("choices")) { + final var choice = (JSONObject) c; + final var choiceId = Long.valueOf(choice.get("choiceId").toString()); + if ((Boolean) choice.get("isGood")) { + questionBestAnswer.choices().add(choiceId); + } else { + questionWorstAnswer.choices().add(choiceId); + } + } + perfectAnswer.questions().add(questionBestAnswer); + worstPossibleAnswer.questions().add(questionWorstAnswer); + } + + api.put(worstPossibleAnswer, "/skill-tests").andExpect(status().isAccepted()) + .andExpect(jsonPath("$.score", is(-50.0))); + final var tontonPirate = new UserRepresentation(); + tontonPirate.setEmail("tonton-pirate@c4-soft.com"); + when(keycloakAdminApi.adminRealmsRealmUsersGet("quiz", Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty(), Optional.of(true), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.of("tonton-pirate"))) + .thenReturn(ResponseEntity.ok(List.of(tontonPirate))); + api.perform(get("/skill-tests/{quizId}/{traineeName}", quizId.toString(), "tonton-pirate")) + .andExpect(status().isOk()).andExpect(jsonPath("$.score", is(-50.0))); + + api.put(perfectAnswer, "/skill-tests").andExpect(status().isAccepted()) + .andExpect(jsonPath("$.score", is(100.0))); + api.perform(get("/skill-tests/{quizId}/{traineeName}", quizId.toString(), "tonton-pirate")) + .andExpect(status().isOk()).andExpect(jsonPath("$.score", is(100.0))); + } + + private T parse(MvcResult result, Class clazz) + throws UnsupportedEncodingException, ParseException { + return new JSONParser(JSONParser.MODE_PERMISSIVE) + .parse(result.getResponse().getContentAsString(), clazz); + } + + public static class Fixtures { + static final String trainer1 = "ch4mp"; + static final String moderator1 = "ch4mp"; + + public static Quiz openIdTraingQuiz() { + final var quiz = new Quiz("OAuth2 and OpenID in web echosystem with Spring", trainer1, + new Question("Question 1", "", 0, "Good is right", new Choice("good", true), + new Choice("bad", false)), + new Question("Question 2", "", 1, "Bad is wrong", new Choice("bad", false), + new Choice("good", true))); + quiz.setModeratedBy(moderator1); + quiz.setIsPublished(true); + return quiz; + } + } } diff --git a/api/quiz-api/src/test/java/com/c4soft/quiz/QuizFixtures.java b/api/quiz-api/src/test/java/com/c4soft/quiz/QuizFixtures.java new file mode 100644 index 0000000..3feadb9 --- /dev/null +++ b/api/quiz-api/src/test/java/com/c4soft/quiz/QuizFixtures.java @@ -0,0 +1,31 @@ +package com.c4soft.quiz; + +import com.c4soft.quiz.domain.Choice; +import com.c4soft.quiz.domain.Question; +import com.c4soft.quiz.domain.Quiz; + +public class QuizFixtures { + + public static Choice testChoice(Long quizId, Long questionId, Long choiceId) { + return new Choice("Quiz %d, question %d, choice %d".formatted(quizId, questionId, choiceId), + (choiceId % 2) != 0); + } + + public static Question testQuestion(Long quizId, Long questionId) { + final var question = new Question("Quiz %d, question %d".formatted(quizId, questionId), + "Yest another interesting question", questionId.intValue(), "This is why", + testChoice(quizId, questionId, questionId * 2), + testChoice(quizId, questionId, questionId * 2 + 1)); + question.setPriority(questionId.intValue() / 2); + return question; + } + + public static Quiz testQuiz(Long quizId, String authorName, boolean isPublished) { + final var quiz = new Quiz("Quiz %d".formatted(quizId), authorName, + testQuestion(quizId, quizId * 2), testQuestion(quizId, quizId * 2 + 1)); + quiz.setIsPublished(isPublished); + return quiz; + } + + +} diff --git a/api/quiz-api/src/test/java/com/c4soft/quiz/SecuredTest.java b/api/quiz-api/src/test/java/com/c4soft/quiz/SecuredTest.java index d942ba9..c30a829 100644 --- a/api/quiz-api/src/test/java/com/c4soft/quiz/SecuredTest.java +++ b/api/quiz-api/src/test/java/com/c4soft/quiz/SecuredTest.java @@ -1,14 +1,13 @@ +/* (C)2024 */ package com.c4soft.quiz; +import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; - import org.springframework.context.annotation.Import; -import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; - /** * Avoid MethodArgumentConversionNotSupportedException with repos MockBean * @@ -17,7 +16,5 @@ @AutoConfigureAddonsWebmvcResourceServerSecurity @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) -@Import({ SecurityConfig.class }) -public @interface SecuredTest { - -} \ No newline at end of file +@Import({SecurityConfig.class}) +public @interface SecuredTest {} diff --git a/api/quiz-api/src/test/java/com/c4soft/quiz/web/QuizControllerTest.java b/api/quiz-api/src/test/java/com/c4soft/quiz/web/QuizControllerTest.java index 55acebb..8f66951 100644 --- a/api/quiz-api/src/test/java/com/c4soft/quiz/web/QuizControllerTest.java +++ b/api/quiz-api/src/test/java/com/c4soft/quiz/web/QuizControllerTest.java @@ -1,40 +1,134 @@ +/* (C)2024 */ package com.c4soft.quiz.web; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - +import java.util.List; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; +import org.springframework.http.HttpHeaders; +import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.test.context.ActiveProfiles; - +import org.springframework.transaction.annotation.Transactional; import com.c4_soft.springaddons.security.oauth2.test.annotations.WithJwt; import com.c4_soft.springaddons.security.oauth2.test.webmvc.AutoConfigureAddonsWebmvcResourceServerSecurity; import com.c4_soft.springaddons.security.oauth2.test.webmvc.MockMvcSupport; import com.c4soft.quiz.EnableSpringDataWebSupportTestConf; +import com.c4soft.quiz.ExceptionHandlers; +import com.c4soft.quiz.QuizFixtures; import com.c4soft.quiz.SecurityConfig; +import com.c4soft.quiz.domain.Quiz; +import com.c4soft.quiz.domain.jpa.ChoiceRepository; +import com.c4soft.quiz.domain.jpa.QuestionRepository; +import com.c4soft.quiz.domain.jpa.QuizRepository; +import com.c4soft.quiz.domain.jpa.SkillTestRepository; import com.c4soft.quiz.web.dto.QuizUpdateDto; +import com.c4soft.quiz.web.dto.mapping.ChoiceMapperImpl; +import com.c4soft.quiz.web.dto.mapping.QuestionMapperImpl; +import com.c4soft.quiz.web.dto.mapping.QuizMapperImpl; -@WebMvcTest(controllers = QuizController.class) +@WebMvcTest(controllers = QuizController.class, properties = {"hostname=localhost"}) @ActiveProfiles("h2") -@Import({ EnableSpringDataWebSupportTestConf.class, ExceptionHandlers.class, SecurityConfig.class }) +@Import({EnableSpringDataWebSupportTestConf.class, ExceptionHandlers.class, SecurityConfig.class, + QuizMapperImpl.class, QuestionMapperImpl.class, ChoiceMapperImpl.class}) @AutoConfigureAddonsWebmvcResourceServerSecurity class QuizControllerTest { - @Autowired - MockMvcSupport api; - - @Test - @WithJwt("ch4mp.json") - void givenUserIsCh4mp_whenPayloadIsInvalid_then422WithValidationExceptionsInProblemDetails() throws Exception { - api - .post(new QuizUpdateDto("", null, null, null, null), "/quizzes") - .andExpect(status().isUnprocessableEntity()) - .andExpect(jsonPath("$.invalidFields.title", is("NotEmpty"))) - .andExpect(jsonPath("$.invalidFields.isChoicesShuffled", is("NotNull"))) - .andExpect(jsonPath("$.invalidFields.isReplayEnabled", is("NotNull"))) - .andExpect(jsonPath("$.invalidFields.isPerQuestionResult", is("NotNull"))) - .andExpect(jsonPath("$.invalidFields.isTrainerNotifiedOfNewTests", is("NotNull"))); - } + @Autowired + MockMvcSupport api; + + @Autowired + QuizRepository quizRepo; + + @Autowired + QuestionRepository questionRepo; + + @Autowired + ChoiceRepository choiceRepo; + + @Autowired + SkillTestRepository skillTestRepo; + + Quiz quiz1; + Quiz quiz2; + Quiz quiz3; + + @BeforeEach + @Transactional(readOnly = false) + void setup() { + quiz1 = QuizFixtures.testQuiz(1L, "ch4mp", true); + quiz2 = QuizFixtures.testQuiz(2L, "vickette", true); + quiz3 = QuizFixtures.testQuiz(3L, "ch4mp", true); + skillTestRepo.deleteAll(); + quizRepo.deleteAll(); + quizRepo.saveAllAndFlush(List.of(quiz1, quiz2, quiz3)); + } + + @Test + @WithJwt("ch4mp.json") + void givenUserIsCh4mp_whenPayloadIsInvalid_then422WithValidationExceptionsInProblemDetails() + throws Exception { + api.post(new QuizUpdateDto("", null, null, null, null), "/quizzes") + .andExpect(status().isUnprocessableEntity()) + .andExpect(jsonPath("$.invalidFields.title", is("NotEmpty"))) + .andExpect(jsonPath("$.invalidFields.isChoicesShuffled", is("NotNull"))) + .andExpect(jsonPath("$.invalidFields.isReplayEnabled", is("NotNull"))) + .andExpect(jsonPath("$.invalidFields.isPerQuestionResult", is("NotNull"))) + .andExpect(jsonPath("$.invalidFields.isTrainerNotifiedOfNewTests", is("NotNull"))); + assertEquals(3, quizRepo.count()); + assertEquals(6, questionRepo.count()); + assertEquals(12, choiceRepo.count()); + } + + @Test + @WithJwt("unprivieldged-trainer.json") + void givenUserIsCh4mp_whenPayloadIsValid_thenCreated() throws Exception { + final var quizzesCount = quizRepo.count(); + + final var location = api + .post(new QuizUpdateDto("Test quiz", true, false, false, false), "/quizzes") + .andExpect(status().isCreated()).andReturn().getResponse().getHeader(HttpHeaders.LOCATION); + + assertFalse(location.isEmpty()); + assertEquals(quizzesCount + 1, quizRepo.count()); + assertTrue(quizRepo.findById(Long.parseLong(location)).isPresent()); + + assertEquals(quizzesCount * 2, questionRepo.count()); + assertEquals(quizzesCount * 4, choiceRepo.count()); + } + + @Test + @WithJwt("trainee.json") + void givenUserIsTrainee_whenPayloadIsValid_thenForbidden() throws Exception { + final var quizzesCount = quizRepo.count(); + + api.post(new QuizUpdateDto("Test quiz", true, false, false, false), "/quizzes") + .andExpect(status().isForbidden()); + + assertEquals(quizzesCount, quizRepo.count()); + assertEquals(quizzesCount * 2, questionRepo.count()); + assertEquals(quizzesCount * 4, choiceRepo.count()); + } + + @Test + @WithAnonymousUser + void givenRequestIsAnonymous_whenGetQuizList_thenOk() throws Exception { + api.perform(get("/quizzes")).andExpect(status().isOk()).andExpect(jsonPath("$", hasSize(3))); + + api.perform(get("/quizzes").param("authorLike", "4").param("titleLike", "3")) + .andExpect(status().isOk()).andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].title", is(quiz3.getTitle()))); + + assertEquals(3, quizRepo.count()); + assertEquals(6, questionRepo.count()); + assertEquals(12, choiceRepo.count()); + } } diff --git a/api/quiz-api/src/test/resources/ch4mp.json b/api/quiz-api/src/test/resources/ch4mp.json index 14b22cb..b55a3ed 100644 --- a/api/quiz-api/src/test/resources/ch4mp.json +++ b/api/quiz-api/src/test/resources/ch4mp.json @@ -7,5 +7,6 @@ ] }, "scope": "openid email", - "iss": "https://oidc.c4-soft.com/auth/realms/quiz" + "iss": "http://localhost/auth/realms/quiz", + "email": "ch4mp@c4-soft.com" } \ No newline at end of file diff --git a/api/quiz-api/src/test/resources/tonton-pirate.json b/api/quiz-api/src/test/resources/trainee.json similarity index 66% rename from api/quiz-api/src/test/resources/tonton-pirate.json rename to api/quiz-api/src/test/resources/trainee.json index 10ebf81..fa264c8 100644 --- a/api/quiz-api/src/test/resources/tonton-pirate.json +++ b/api/quiz-api/src/test/resources/trainee.json @@ -8,5 +8,6 @@ "resource_access": { }, "scope": "openid email", - "iss": "https://oidc.c4-soft.com/auth/realms/quiz" + "iss": "http://localhost/auth/realms/quiz", + "email": "tonton-pirate@c4-soft.com" } \ No newline at end of file diff --git a/api/quiz-api/src/test/resources/unprivieldged-trainer.json b/api/quiz-api/src/test/resources/unprivieldged-trainer.json new file mode 100644 index 0000000..e6a8a94 --- /dev/null +++ b/api/quiz-api/src/test/resources/unprivieldged-trainer.json @@ -0,0 +1,11 @@ +{ + "preferred_username": "vickette", + "realm_access": { + "roles": [ + "trainer" + ] + }, + "scope": "openid email", + "iss": "http://localhost/auth/realms/quiz", + "email": "vickette@c4-soft.com" +} \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..9951e6a --- /dev/null +++ b/build.sh @@ -0,0 +1,85 @@ +#!/bin/sh +echo "***************************************************************************************************************************************" +echo "* To build Spring Boot native images, run with the \"native\" argument: \"sh ./build.sh native\" (images will take much longer to build). *" +echo "* *" +echo "* This build script tries to auto-detect ARM64 (Apple Silicon) to build the appropriate Spring Boot Docker images. *" +echo "***************************************************************************************************************************************" +echo "" + +if [[ "$OSTYPE" == "darwin"* ]]; then + SED="sed -i '' -e" +else + SED="sed -i -e" +fi + +MAVEN_PROFILES=() +if [[ `uname -m` == "arm64" ]]; then + MAVEN_PROFILES+=("arm64") +fi +if [[ " $@ " =~ [[:space:]]native[[:space:]] ]]; then + MAVEN_PROFILES+=("native") +fi +if [ ${#MAVEN_PROFILES[@]} -eq 0 ]; then + MAVEN_PROFILE_ARG="" +else + MAVEN_PROFILE_ARG=-P$(IFS=, ; echo "${MAVEN_PROFILES[*]}") +fi + +host=$(echo $HOSTNAME | tr '[A-Z]' '[a-z]') + +cd api +echo "***********************" +echo "sh ./mvnw clean install" +echo "***********************" +echo "" +sh ./mvnw clean install + +echo "" +echo "*****************************************************************************************************************************************" +echo "sh ./mvnw -pl quiz-api spring-boot:build-image -Dspring-boot.build-image.imageName=quiz/api $MAVEN_PROFILE_ARG" +echo "*****************************************************************************************************************************************" +echo "" +sh ./mvnw -pl quiz-api spring-boot:build-image -Dspring-boot.build-image.imageName=quiz/api $MAVEN_PROFILE_ARG + +echo "" +echo "*****************************************************************************************************************" +echo "sh ./mvnw -pl bff spring-boot:build-image -Dspring-boot.build-image.imageName=quiz/bff $MAVEN_PROFILE_ARG" +echo "*****************************************************************************************************************" +echo "" +sh ./mvnw -pl bff spring-boot:build-image -Dspring-boot.build-image.imageName=quiz/bff $MAVEN_PROFILE_ARG +cd .. + +rm -f "compose-${host}.yml" +cp compose.yml "compose-${host}.yml" +$SED "s/LOCALHOST_NAME/${host}/g" "compose-${host}.yml" +rm -f "compose-${host}.yml''" + +rm keycloak/import/quiz-realm.json +cp quiz-realm.json keycloak/import/quiz-realm.json +$SED "s/LOCALHOST_NAME/${host}/g" keycloak/import/quiz-realm.json +rm "keycloak/import/quiz-realm.json''" + +cd angular-ui/ +rm src/app/app.config.ts +cp ../angular-ui.app.config.ts src/app/app.config.ts +$SED "s/LOCALHOST_NAME/${host}/g" src/app/app.config.ts +rm "src/app/app.config.ts''" +npm i +npm run build +cd .. + +docker build -t quiz/nginx-reverse-proxy ./nginx-reverse-proxy +docker build -t quiz/ui ./angular-ui + +docker compose -f compose-${host}.yml up -d + +echo "" +echo "Open the following in a new private navigation window." + +echo "" +echo "Keycloak as admin / admin:" +echo "http://${host}/auth/admin/master/console/#/quiz" + +echo "" +echo "Sample user author / author" +echo http://${host}/ui/ \ No newline at end of file diff --git a/compose-desktop-ch4mp.yml b/compose-desktop-ch4mp.yml new file mode 100644 index 0000000..6b89338 --- /dev/null +++ b/compose-desktop-ch4mp.yml @@ -0,0 +1,104 @@ +name: quiz + +volumes: + quiz_api_postgres_data: + +services: + + nginx-reverse-proxy: + container_name: quiz.nginx-reverse-proxy + image: quiz/nginx-reverse-proxy + ports: + - 80:80 + extra_hosts: + - "host.docker.internal:host-gateway" + - "desktop-ch4mp:host-gateway" + + keycloak: + container_name: quiz.auth + image: quay.io/keycloak/keycloak:latest + command: + - start-dev + - --import-realm + ports: + - 8080:8080 + volumes: + - ./keycloak/import/:/opt/keycloak/data/import/ + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} + KC_HTTP_PORT: 8080 + KC_HOSTNAME_URL: http://desktop-ch4mp/auth + KC_HOSTNAME_ADMIN_URL: http://desktop-ch4mp/auth + KC_HOSTNAME_STRICT_BACKCHANNEL: true + #KC_HOSTNAME_DEBUG: true + KC_HTTP_RELATIVE_PATH: /auth/ + KC_HTTP_ENABLED: true + KC_HEALTH_ENABLED: true + KC_METRICS_ENABLED: true + #KC_LOG_LEVEL: DEBUG + extra_hosts: + - "host.docker.internal:host-gateway" + - "desktop-ch4mp:host-gateway" + healthcheck: + test: ['CMD-SHELL', '[ -f /tmp/HealthCheck.java ] || echo "public class HealthCheck { public static void main(String[] args) throws java.lang.Throwable { System.exit(java.net.HttpURLConnection.HTTP_OK == ((java.net.HttpURLConnection)new java.net.URL(args[0]).openConnection()).getResponseCode() ? 0 : 1); } }" > /tmp/HealthCheck.java && java /tmp/HealthCheck.java http://localhost/auth/health/live'] + interval: 5s + timeout: 5s + retries: 20 + + angular-ui: + container_name: quiz.ui + image: quiz/ui + ports: + - 4200:80 + extra_hosts: + - "host.docker.internal:host-gateway" + - "desktop-ch4mp:host-gateway" + + quiz-db: + container_name: quiz.api.postgres + image: postgres:latest + environment: + - POSTGRES_DB=quiz + - POSTGRES_PASSWORD=${POSTGRES_QUIZ_PASSWORD} + - POSTGRES_USER=quiz + volumes: + - quiz_api_postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready", "-d", "quiz", "-U", "quiz"] + interval: 3s + timeout: 60s + retries: 20 + + quiz-api: + container_name: quiz.api + image: quiz/api + ports: + - 7084:7084 + environment: + HOSTNAME: desktop-ch4mp + SERVER_ADDRESS: 0.0.0.0 + depends_on: + keycloak: + condition: service_healthy + quiz-db: + condition: service_healthy + extra_hosts: + - "host.docker.internal:host-gateway" + - "desktop-ch4mp:host-gateway" + + bff: + container_name: quiz.bff + image: quiz/bff + ports: + - 7080:7080 + environment: + HOSTNAME: desktop-ch4mp + SERVER_ADDRESS: 0.0.0.0 + CLIENT_SECRET: secret + depends_on: + keycloak: + condition: service_healthy + extra_hosts: + - "host.docker.internal:host-gateway" + - "desktop-ch4mp:host-gateway" diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..c8e0b14 --- /dev/null +++ b/compose.yml @@ -0,0 +1,106 @@ +name: quiz + +volumes: + quiz_api_postgres_data: + +services: + + nginx-reverse-proxy: + container_name: quiz.nginx-reverse-proxy + image: quiz/nginx-reverse-proxy + ports: + - 80:80 + extra_hosts: + - "host.docker.internal:host-gateway" + - "LOCALHOST_NAME:host-gateway" + + keycloak: + container_name: quiz.auth + image: quay.io/keycloak/keycloak:24.0.0 + command: + - start-dev + - --import-realm + ports: + - 8080:8080 + volumes: + - ./keycloak/import/:/opt/keycloak/data/import/ + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} + KC_HTTP_PORT: 8080 + KC_HOSTNAME_URL: http://LOCALHOST_NAME/auth + KC_HOSTNAME_ADMIN_URL: http://LOCALHOST_NAME/auth + KC_HOSTNAME_STRICT_BACKCHANNEL: true + #KC_HOSTNAME_DEBUG: true + KC_HTTP_RELATIVE_PATH: /auth/ + KC_HTTP_ENABLED: true + KC_HEALTH_ENABLED: true + KC_METRICS_ENABLED: true + #KC_LOG_LEVEL: DEBUG + extra_hosts: + - "host.docker.internal:host-gateway" + - "LOCALHOST_NAME:host-gateway" + healthcheck: + test : ['CMD-SHELL', '[ -f /tmp/HealthCheck.java ] || echo "public class HealthCheck { public static void main(String[] args) throws java.lang.Throwable { System.exit(java.net.HttpURLConnection.HTTP_OK == ((java.net.HttpURLConnection)new java.net.URL (args[0]).openConnection().getResponseCode() ? 0 : 1); } }" > /tmp/HealthCheck.java && java /tmp/HealthCheck.java http://LOCALHOST_NAME/auth/health/live '] + interval: 5s + timeout: 5s + retries: 3 + + angular-ui: + container_name: quiz.ui + image: quiz/ui + ports: + - 4200:80 + extra_hosts: + - "host.docker.internal:host-gateway" + - "LOCALHOST_NAME:host-gateway" + + quiz-db: + container_name: quiz.api.postgres + image: postgres:latest + ports: + - 5432:5432 + environment: + - POSTGRES_DB=quiz + - POSTGRES_PASSWORD=${POSTGRES_QUIZ_PASSWORD} + - POSTGRES_USER=quiz + volumes: + - quiz_api_postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready", "-d", "quiz", "-U", "quiz"] + interval: 3s + timeout: 60s + retries: 20 + + quiz-api: + container_name: quiz.api + image: quiz/api + ports: + - 7084:7084 + environment: + HOSTNAME: LOCALHOST_NAME + SERVER_ADDRESS: 0.0.0.0 + depends_on: + keycloak: + condition: service_healthy + quiz-db: + condition: service_healthy + extra_hosts: + - "host.docker.internal:host-gateway" + - "LOCALHOST_NAME:host-gateway" + + bff: + container_name: quiz.bff + image: quiz/bff + ports: + - 7080:7080 + environment: + HOSTNAME: LOCALHOST_NAME + SERVER_ADDRESS: 0.0.0.0 + CLIENT_SECRET: secret + depends_on: + keycloak: + condition: service_healthy + extra_hosts: + - "host.docker.internal:host-gateway" + - "LOCALHOST_NAME:host-gateway" diff --git a/delete-eclipse-project-files.sh b/delete-eclipse-project-files.sh new file mode 100644 index 0000000..bcbcc41 --- /dev/null +++ b/delete-eclipse-project-files.sh @@ -0,0 +1,3 @@ +#!/bin/bash +rm -f ./*/.classpath ./*/.factorypath ./*/.project ./*/*/.classpath ./*/*/.factorypath ./*/*/.project +rm -Rf ./*/.settings ./*/target ./*/*/.settings ./*/*/target diff --git a/keycloak/.env b/keycloak/.env new file mode 100644 index 0000000..c4c692a --- /dev/null +++ b/keycloak/.env @@ -0,0 +1 @@ +KEYCLOAK_ADMIN_PASSWORD=admin \ No newline at end of file diff --git a/keycloak/compose.yml b/keycloak/compose.yml new file mode 100644 index 0000000..56ebef4 --- /dev/null +++ b/keycloak/compose.yml @@ -0,0 +1,42 @@ +name: quiz_auth-only + +services: + + nginx-reverse-proxy: + container_name: quiz.auth-only.nginx-reverse-proxy + image: quiz/nginx-reverse-proxy + ports: + - 80:80 + extra_hosts: + - "host.docker.internal:host-gateway" + + keycloak: + container_name: quiz.auth-only.keycloak + image: quay.io/keycloak/keycloak:latest + command: + - start-dev + - --import-realm + ports: + - 8080:8080 + volumes: + - ./import/:/opt/keycloak/data/import/ + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} + KC_HTTP_PORT: 8080 + KC_HOSTNAME_URL: http://localhost/auth + KC_HOSTNAME_ADMIN_URL: http://localhost/auth + KC_HOSTNAME_STRICT_BACKCHANNEL: true + #KC_HOSTNAME_DEBUG: true + KC_HTTP_RELATIVE_PATH: /auth/ + KC_HTTP_ENABLED: true + KC_HEALTH_ENABLED: true + KC_METRICS_ENABLED: true + #KC_LOG_LEVEL: DEBUG + extra_hosts: + - "host.docker.internal:host-gateway" + healthcheck: + test: ['CMD-SHELL', '[ -f /tmp/HealthCheck.java ] || echo "public class HealthCheck { public static void main(String[] args) throws java.lang.Throwable { System.exit(java.net.HttpURLConnection.HTTP_OK == ((java.net.HttpURLConnection)new java.net.URL(args[0]).openConnection()).getResponseCode() ? 0 : 1); } }" > /tmp/HealthCheck.java && java /tmp/HealthCheck.java http://localhost/auth/health/live'] + interval: 5s + timeout: 5s + retries: 20 diff --git a/keycloak/import/quiz-realm.json b/keycloak/import/quiz-realm.json new file mode 100644 index 0000000..caf56f4 --- /dev/null +++ b/keycloak/import/quiz-realm.json @@ -0,0 +1,1941 @@ +{ + "id" : "76d4a142-1721-46e0-8074-c36907b64f89", + "realm" : "quiz", + "notBefore" : 0, + "defaultSignatureAlgorithm" : "RS256", + "revokeRefreshToken" : false, + "refreshTokenMaxReuse" : 0, + "accessTokenLifespan" : 300, + "accessTokenLifespanForImplicitFlow" : 900, + "ssoSessionIdleTimeout" : 1800, + "ssoSessionMaxLifespan" : 36000, + "ssoSessionIdleTimeoutRememberMe" : 0, + "ssoSessionMaxLifespanRememberMe" : 0, + "offlineSessionIdleTimeout" : 2592000, + "offlineSessionMaxLifespanEnabled" : false, + "offlineSessionMaxLifespan" : 5184000, + "clientSessionIdleTimeout" : 0, + "clientSessionMaxLifespan" : 0, + "clientOfflineSessionIdleTimeout" : 0, + "clientOfflineSessionMaxLifespan" : 0, + "accessCodeLifespan" : 60, + "accessCodeLifespanUserAction" : 300, + "accessCodeLifespanLogin" : 1800, + "actionTokenGeneratedByAdminLifespan" : 43200, + "actionTokenGeneratedByUserLifespan" : 300, + "oauth2DeviceCodeLifespan" : 600, + "oauth2DevicePollingInterval" : 5, + "enabled" : true, + "sslRequired" : "external", + "registrationAllowed" : false, + "registrationEmailAsUsername" : false, + "rememberMe" : false, + "verifyEmail" : false, + "loginWithEmailAllowed" : true, + "duplicateEmailsAllowed" : false, + "resetPasswordAllowed" : false, + "editUsernameAllowed" : false, + "bruteForceProtected" : false, + "permanentLockout" : false, + "maxTemporaryLockouts" : 0, + "maxFailureWaitSeconds" : 900, + "minimumQuickLoginWaitSeconds" : 60, + "waitIncrementSeconds" : 60, + "quickLoginCheckMilliSeconds" : 1000, + "maxDeltaTimeSeconds" : 43200, + "failureFactor" : 30, + "roles" : { + "realm" : [ { + "id" : "9d37e22b-ea36-4535-9434-5fd44026f841", + "name" : "default-roles-quiz", + "description" : "${role_default-roles}", + "composite" : true, + "composites" : { + "realm" : [ "offline_access", "uma_authorization" ], + "client" : { + "account" : [ "manage-account", "view-profile" ] + } + }, + "clientRole" : false, + "containerId" : "76d4a142-1721-46e0-8074-c36907b64f89", + "attributes" : { } + }, { + "id" : "bd117839-cec3-4350-ab28-6a54ee7fa56c", + "name" : "moderator", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "76d4a142-1721-46e0-8074-c36907b64f89", + "attributes" : { } + }, { + "id" : "4eb904cc-a109-4742-8b73-36a2b5f45f2f", + "name" : "uma_authorization", + "description" : "${role_uma_authorization}", + "composite" : false, + "clientRole" : false, + "containerId" : "76d4a142-1721-46e0-8074-c36907b64f89", + "attributes" : { } + }, { + "id" : "675618c2-75eb-4716-8dbb-eac7fe763b6b", + "name" : "offline_access", + "description" : "${role_offline-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "76d4a142-1721-46e0-8074-c36907b64f89", + "attributes" : { } + }, { + "id" : "25b69071-61f4-435d-9b1f-3271068f162d", + "name" : "trainer", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "76d4a142-1721-46e0-8074-c36907b64f89", + "attributes" : { } + } ], + "client" : { + "realm-management" : [ { + "id" : "d444308b-e618-4d62-adfe-92db269ba2ff", + "name" : "view-realm", + "description" : "${role_view-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "76cd936f-87db-43c5-903a-e0436cb6f37c", + "name" : "query-users", + "description" : "${role_query-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "db7e75ed-f76f-4ebb-a250-68639b23f3c2", + "name" : "view-events", + "description" : "${role_view-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "1d5b2477-2697-4d7b-b516-3358616117b5", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "6f9e010f-81f0-489c-808c-d53ef53f6358", + "name" : "manage-events", + "description" : "${role_manage-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "ac4104a0-3e7b-4db4-a7c9-1740fc359f54", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "7414f0d7-b557-4aeb-9359-693cc3fa6a08", + "name" : "query-clients", + "description" : "${role_query-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "11a8b26f-c1b8-42f0-9551-05528f43deca", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "44b7b388-790d-4a8d-a502-e99fc009ae96", + "name" : "query-realms", + "description" : "${role_query-realms}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "aaece596-37c0-4f83-a4b9-fd210f8279ba", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "6dc2ec92-009b-4b83-b277-60ff77808087", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "6776512b-4e88-4ad8-a891-ad9384923cf1", + "name" : "realm-admin", + "description" : "${role_realm-admin}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "view-realm", "query-users", "view-events", "manage-identity-providers", "manage-events", "manage-realm", "query-clients", "view-authorization", "query-realms", "manage-authorization", "manage-clients", "view-identity-providers", "view-clients", "impersonation", "create-client", "view-users", "manage-users", "query-groups" ] + } + }, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "ff5af959-b24e-48b5-b601-6686957d2a4a", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "6d247874-cfc0-4292-b991-2472af00bf1a", + "name" : "view-clients", + "description" : "${role_view-clients}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-clients" ] + } + }, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "8246a3c9-5342-4e2a-9f6e-d28fb2c7470b", + "name" : "impersonation", + "description" : "${role_impersonation}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "261313c2-e3e7-4c13-b4f3-e901967de70f", + "name" : "create-client", + "description" : "${role_create-client}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "19d6c3ae-4cf5-4906-a4b1-047cde6e1c4f", + "name" : "view-users", + "description" : "${role_view-users}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-users", "query-groups" ] + } + }, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "70f598ca-ad42-49ad-bff5-ae8f1466d9aa", + "name" : "manage-users", + "description" : "${role_manage-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "afea140a-1e86-4e7f-acb8-8307ef78251a", + "name" : "query-groups", + "description" : "${role_query-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + } ], + "security-admin-console" : [ ], + "admin-cli" : [ ], + "account-console" : [ ], + "broker" : [ { + "id" : "f46c10f5-8fcb-404a-a366-7707368493ba", + "name" : "read-token", + "description" : "${role_read-token}", + "composite" : false, + "clientRole" : true, + "containerId" : "30d9e8ec-6bd9-45e7-a1e4-bd3fbbcfdd58", + "attributes" : { } + } ], + "quiz-bff" : [ ], + "account" : [ { + "id" : "dfafd224-116f-42c1-a315-af921786ec1e", + "name" : "view-groups", + "description" : "${role_view-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "ab912702-9ceb-42d5-b561-0f669aa354ab", + "attributes" : { } + }, { + "id" : "02866871-b7a4-4034-94d9-641c3d6074a3", + "name" : "manage-consent", + "description" : "${role_manage-consent}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "view-consent" ] + } + }, + "clientRole" : true, + "containerId" : "ab912702-9ceb-42d5-b561-0f669aa354ab", + "attributes" : { } + }, { + "id" : "032161ca-550c-45c9-bb52-440111a58cf1", + "name" : "manage-account-links", + "description" : "${role_manage-account-links}", + "composite" : false, + "clientRole" : true, + "containerId" : "ab912702-9ceb-42d5-b561-0f669aa354ab", + "attributes" : { } + }, { + "id" : "6f520b85-63c6-4b3d-b037-51549275ae38", + "name" : "view-applications", + "description" : "${role_view-applications}", + "composite" : false, + "clientRole" : true, + "containerId" : "ab912702-9ceb-42d5-b561-0f669aa354ab", + "attributes" : { } + }, { + "id" : "fb9b0727-7f09-4dc3-8d2c-f8db7cf2090d", + "name" : "manage-account", + "description" : "${role_manage-account}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "manage-account-links" ] + } + }, + "clientRole" : true, + "containerId" : "ab912702-9ceb-42d5-b561-0f669aa354ab", + "attributes" : { } + }, { + "id" : "553624de-0e8f-46cf-aada-8e48723a5ddb", + "name" : "view-consent", + "description" : "${role_view-consent}", + "composite" : false, + "clientRole" : true, + "containerId" : "ab912702-9ceb-42d5-b561-0f669aa354ab", + "attributes" : { } + }, { + "id" : "afa0893f-9346-4a76-b006-8d52f6794984", + "name" : "delete-account", + "description" : "${role_delete-account}", + "composite" : false, + "clientRole" : true, + "containerId" : "ab912702-9ceb-42d5-b561-0f669aa354ab", + "attributes" : { } + }, { + "id" : "b8f21063-15e7-4921-9f9f-ed405f398687", + "name" : "view-profile", + "description" : "${role_view-profile}", + "composite" : false, + "clientRole" : true, + "containerId" : "ab912702-9ceb-42d5-b561-0f669aa354ab", + "attributes" : { } + } ] + } + }, + "groups" : [ ], + "defaultRole" : { + "id" : "9d37e22b-ea36-4535-9434-5fd44026f841", + "name" : "default-roles-quiz", + "description" : "${role_default-roles}", + "composite" : true, + "clientRole" : false, + "containerId" : "76d4a142-1721-46e0-8074-c36907b64f89" + }, + "requiredCredentials" : [ "password" ], + "otpPolicyType" : "totp", + "otpPolicyAlgorithm" : "HmacSHA1", + "otpPolicyInitialCounter" : 0, + "otpPolicyDigits" : 6, + "otpPolicyLookAheadWindow" : 1, + "otpPolicyPeriod" : 30, + "otpPolicyCodeReusable" : false, + "otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppGoogleName", "totpAppMicrosoftAuthenticatorName" ], + "localizationTexts" : { }, + "webAuthnPolicyRpEntityName" : "keycloak", + "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyRpId" : "", + "webAuthnPolicyAttestationConveyancePreference" : "not specified", + "webAuthnPolicyAuthenticatorAttachment" : "not specified", + "webAuthnPolicyRequireResidentKey" : "not specified", + "webAuthnPolicyUserVerificationRequirement" : "not specified", + "webAuthnPolicyCreateTimeout" : 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyAcceptableAaguids" : [ ], + "webAuthnPolicyExtraOrigins" : [ ], + "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyPasswordlessRpId" : "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", + "webAuthnPolicyPasswordlessCreateTimeout" : 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], + "webAuthnPolicyPasswordlessExtraOrigins" : [ ], + "users" : [ { + "id" : "b6d2f174-1bb7-46f7-9d47-bf8d8bedbfce", + "username" : "ch4mp", + "firstName" : "Jérôme", + "lastName" : "Wacongne", + "email" : "ch4mp@c4-soft.com", + "emailVerified" : true, + "createdTimestamp" : 1718396064665, + "enabled" : true, + "totp" : false, + "credentials" : [ { + "id" : "84894e4f-fc06-4f5b-ab0f-f7e3f648f19b", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1718396081974, + "secretData" : "{\"value\":\"5rCVWjX334a2DKeZaFwg2BDtff8rsXkzdUeoRxwoCJO7B2fzmak7mKkixk0R4Uh47ZxiEyvCf2ZBC13KzYRx9w==\",\"salt\":\"hU1AB+YrdKX58YlR5Mpksw==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":210000,\"algorithm\":\"pbkdf2-sha512\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-quiz", "moderator", "trainer" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "5d77e1e1-d918-40a4-aa30-40e5f9e669d7", + "username" : "moderator", + "firstName" : "quiz", + "lastName" : "moderator", + "email" : "moderator@c4-soft.com", + "emailVerified" : true, + "createdTimestamp" : 1718396200989, + "enabled" : true, + "totp" : false, + "credentials" : [ { + "id" : "aec883cc-680c-43c9-b4a4-4afd82f64c42", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1718396214335, + "secretData" : "{\"value\":\"spqpVpRg3nk/Oyzihxs5mKocM8xCijEt1a51kYLDH4Qn2LqDmMg5UCfOaftpqZsFuG0Lpm7ULds/Om+0hdB+LQ==\",\"salt\":\"g2HJChUBARDbzp/tCklx0Q==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":210000,\"algorithm\":\"pbkdf2-sha512\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-quiz", "moderator" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "c0d9b57f-ad81-47e4-b2b8-b47e1de12a0b", + "username" : "trainee", + "firstName" : "quiz", + "lastName" : "trainee", + "email" : "trainee@c4-soft.com", + "emailVerified" : true, + "createdTimestamp" : 1718396252988, + "enabled" : true, + "totp" : false, + "credentials" : [ { + "id" : "d43886a1-96e4-4561-838d-b91b8f671e3b", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1718396312762, + "secretData" : "{\"value\":\"d4DuesXjEXLNFBPKtMLvgGRwD29/Qve9g6WKCCMoGGvYEMLjNdFs0C2Lowwu8iyZL3xQ2dqO50uNRY+Tuu9WqA==\",\"salt\":\"3BdvKQH6CtUKHCL9r8N7cg==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":210000,\"algorithm\":\"pbkdf2-sha512\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-quiz" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "836d5dc9-6537-4414-8b97-9c82b13aca70", + "username" : "trainer", + "firstName" : "quiz", + "lastName" : "trainer", + "email" : "trainer@c4-soft.com", + "emailVerified" : true, + "createdTimestamp" : 1718396125143, + "enabled" : true, + "totp" : false, + "credentials" : [ { + "id" : "ae7d8c6f-f897-4408-ac42-7982159140ea", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1718396163326, + "secretData" : "{\"value\":\"8qyocDPlK0LwuxxoOjEmvzfGrJN0rbv4785485EqwIfZfA1q6Y3hxdHP4K4jMDFG+QP+wAi6bMPjLvj3YwgfEQ==\",\"salt\":\"3krsBkjCIMlcDPDu5o0j1g==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":210000,\"algorithm\":\"pbkdf2-sha512\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-quiz", "trainer" ], + "notBefore" : 0, + "groups" : [ ] + } ], + "scopeMappings" : [ { + "clientScope" : "offline_access", + "roles" : [ "offline_access" ] + } ], + "clientScopeMappings" : { + "account" : [ { + "client" : "account-console", + "roles" : [ "manage-account", "view-groups" ] + } ] + }, + "clients" : [ { + "id" : "ab912702-9ceb-42d5-b561-0f669aa354ab", + "clientId" : "account", + "name" : "${client_account}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/quiz/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/quiz/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "1dbf212a-b451-42ad-9833-85a71ade77ee", + "clientId" : "account-console", + "name" : "${client_account-console}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/quiz/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/quiz/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "19c7f669-7251-4dad-aacf-90c07d31e3be", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "8897aa23-421f-4f6f-b75e-da6b30092a4e", + "clientId" : "admin-cli", + "name" : "${client_admin-cli}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : false, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "30d9e8ec-6bd9-45e7-a1e4-bd3fbbcfdd58", + "clientId" : "broker", + "name" : "${client_broker}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "c0511d4c-a116-4030-8957-576a059d049b", + "clientId" : "quiz-bff", + "name" : "", + "description" : "", + "rootUrl" : "http://mc-ch4mp/bff", + "adminUrl" : "http://mc-ch4mp/bff", + "baseUrl" : "", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "secret", + "redirectUris" : [ + "http://localhost/bff/login/oauth2/code/quiz-bff", + "http://127.0.0.1/bff/login/oauth2/code/quiz-bff", + "http://host.docker.internal/bff/login/oauth2/code/quiz-bff", + "http://mc-ch4mp/bff/login/oauth2/code/quiz-bff", + "https://localhost/bff/login/oauth2/code/quiz-bff", + "https://127.0.0.1/bff/login/oauth2/code/quiz-bff", + "https://host.docker.internal/bff/login/oauth2/code/quiz-bff", + "https://mc-ch4mp/bff/login/oauth2/code/quiz-bff" + ], + "webOrigins" : [ "+" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "oidc.ciba.grant.enabled" : "false", + "client.secret.creation.time" : "1718395789", + "backchannel.logout.session.required" : "true", + "backchannel.logout.url" : "http://mc-ch4mp/bff/logout/connect/back-channel/quiz-bff", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", + "display.on.consent.screen" : "false", + "backchannel.logout.revoke.offline.tokens" : "true" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "clientId" : "realm-management", + "name" : "${client_realm-management}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "ca2f7520-4658-4401-aa9b-ce013aba30f1", + "clientId" : "security-admin-console", + "name" : "${client_security-admin-console}", + "rootUrl" : "${authAdminUrl}", + "baseUrl" : "/admin/quiz/console/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/admin/quiz/console/*" ], + "webOrigins" : [ "+" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "3fd9fe45-9dcc-4231-9138-c8d64d117111", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + } ], + "clientScopes" : [ { + "id" : "99dbb9c5-4d72-47d5-a4c7-a7fe481cd420", + "name" : "profile", + "description" : "OpenID Connect built-in scope: profile", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${profileScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "3cbab277-3c50-4edf-b650-87be63363968", + "name" : "updated at", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "updatedAt", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "updated_at", + "jsonType.label" : "long" + } + }, { + "id" : "8425c199-042e-4ece-9c72-2a31f02c3017", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + }, { + "id" : "71c19900-4675-4c1b-8c40-20d127492263", + "name" : "nickname", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "nickname", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "nickname", + "jsonType.label" : "String" + } + }, { + "id" : "5189656b-92e2-46b0-ae21-adaf8dcee669", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "ed9a01e0-2ad6-4909-b7f1-229a341dedf1", + "name" : "website", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "website", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "website", + "jsonType.label" : "String" + } + }, { + "id" : "676080d9-6192-4517-99cb-6b69ab2d1b83", + "name" : "birthdate", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "birthdate", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "birthdate", + "jsonType.label" : "String" + } + }, { + "id" : "f0fac647-6b9d-4202-af89-3230aa8c1114", + "name" : "zoneinfo", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "zoneinfo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "zoneinfo", + "jsonType.label" : "String" + } + }, { + "id" : "5fb4a1ae-b4d7-4321-8dbf-d602f11a4110", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "b004cae7-e41d-4fc3-998f-54798e255942", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "introspection.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + }, { + "id" : "0f4e547f-2e0d-4b36-8268-44de3baec468", + "name" : "picture", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "picture", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "picture", + "jsonType.label" : "String" + } + }, { + "id" : "d6a6d2c5-b336-499d-acd2-0e77e9582423", + "name" : "middle name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "middleName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "middle_name", + "jsonType.label" : "String" + } + }, { + "id" : "13014abe-4b35-4bc8-834a-0fd62a7ec861", + "name" : "gender", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "gender", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "gender", + "jsonType.label" : "String" + } + }, { + "id" : "2f85838d-4542-4968-a4ee-5141b1388452", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "d0fb56cd-5c9a-4a3b-a24b-25af4e69cf89", + "name" : "profile", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "profile", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "profile", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "76362470-7974-42ed-857e-7834000a6426", + "name" : "acr", + "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "fb1879f4-a587-461b-a9d0-83514354d77e", + "name" : "acr loa level", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-acr-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "introspection.token.claim" : "true", + "access.token.claim" : "true" + } + } ] + }, { + "id" : "ed00af57-3e31-43f2-b5c1-c28c9f3ab7b9", + "name" : "role_list", + "description" : "SAML role list", + "protocol" : "saml", + "attributes" : { + "consent.screen.text" : "${samlRoleListScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "241df7cf-5245-4fae-9529-d8ddd071e195", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + } ] + }, { + "id" : "bf321dc1-190d-4924-b990-a27191467f19", + "name" : "address", + "description" : "OpenID Connect built-in scope: address", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${addressScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "96f86cf5-6ccb-4e6f-8dd3-7bb186ba5723", + "name" : "address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-address-mapper", + "consentRequired" : false, + "config" : { + "user.attribute.formatted" : "formatted", + "user.attribute.country" : "country", + "introspection.token.claim" : "true", + "user.attribute.postal_code" : "postal_code", + "userinfo.token.claim" : "true", + "user.attribute.street" : "street", + "id.token.claim" : "true", + "user.attribute.region" : "region", + "access.token.claim" : "true", + "user.attribute.locality" : "locality" + } + } ] + }, { + "id" : "3fbd86f0-2a7e-4870-868a-b7be2ccaac68", + "name" : "offline_access", + "description" : "OpenID Connect built-in scope: offline_access", + "protocol" : "openid-connect", + "attributes" : { + "consent.screen.text" : "${offlineAccessScopeConsentText}", + "display.on.consent.screen" : "true" + } + }, { + "id" : "ca9e835c-a297-43d3-9cb9-f5c480161cfc", + "name" : "email", + "description" : "OpenID Connect built-in scope: email", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${emailScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "b92539ca-d028-48eb-80c9-dbe1ed4f588b", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + }, { + "id" : "8d72b099-2ab3-43c5-9174-b4a5325c6840", + "name" : "email verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "emailVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email_verified", + "jsonType.label" : "boolean" + } + } ] + }, { + "id" : "71924996-41a4-4e9c-b674-e59d7b7557fa", + "name" : "web-origins", + "description" : "OpenID Connect scope for add allowed web origins to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false", + "consent.screen.text" : "" + }, + "protocolMappers" : [ { + "id" : "bcd87c60-b873-40b3-bd80-f9525c66d508", + "name" : "allowed web origins", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-allowed-origins-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "access.token.claim" : "true" + } + } ] + }, { + "id" : "4c26130a-a8e5-4dc2-971f-30f8fde200fc", + "name" : "phone", + "description" : "OpenID Connect built-in scope: phone", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${phoneScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "e1e4a4a2-8a43-4943-9beb-c0c892c023d5", + "name" : "phone number", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumber", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number", + "jsonType.label" : "String" + } + }, { + "id" : "c0c15b77-e330-48ef-9c1b-bb0ad0f751c8", + "name" : "phone number verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumberVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number_verified", + "jsonType.label" : "boolean" + } + } ] + }, { + "id" : "14ca39e6-3045-4013-86be-343912bfa2b2", + "name" : "roles", + "description" : "OpenID Connect scope for add user roles to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${rolesScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "03260206-f8bf-43d9-84dd-46967983a02e", + "name" : "realm roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "multivalued" : "true", + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "realm_access.roles", + "jsonType.label" : "String" + } + }, { + "id" : "87b834c9-afe6-4ced-8fb7-516351289c66", + "name" : "client roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-client-role-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "multivalued" : "true", + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "resource_access.${client_id}.roles", + "jsonType.label" : "String" + } + }, { + "id" : "97bed3cf-38a5-4cde-ad71-0fe909c72eb7", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "access.token.claim" : "true" + } + } ] + }, { + "id" : "27ac15a4-ba92-4aa1-839d-7e54d71f8f56", + "name" : "microprofile-jwt", + "description" : "Microprofile - JWT built-in scope", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "32a2dcd2-9149-4fd3-9f70-eb33a4a9f673", + "name" : "upn", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "upn", + "jsonType.label" : "String" + } + }, { + "id" : "f78a669b-d9a4-4936-a02c-fa2162b00686", + "name" : "groups", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "multivalued" : "true", + "user.attribute" : "foo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "groups", + "jsonType.label" : "String" + } + } ] + } ], + "defaultDefaultClientScopes" : [ "role_list", "profile", "email", "roles", "web-origins", "acr" ], + "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt" ], + "browserSecurityHeaders" : { + "contentSecurityPolicyReportOnly" : "", + "xContentTypeOptions" : "nosniff", + "referrerPolicy" : "no-referrer", + "xRobotsTag" : "none", + "xFrameOptions" : "SAMEORIGIN", + "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection" : "1; mode=block", + "strictTransportSecurity" : "max-age=31536000; includeSubDomains" + }, + "smtpServer" : { }, + "eventsEnabled" : false, + "eventsListeners" : [ "jboss-logging" ], + "enabledEventTypes" : [ ], + "adminEventsEnabled" : false, + "adminEventsDetailsEnabled" : false, + "identityProviders" : [ ], + "identityProviderMappers" : [ ], + "components" : { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { + "id" : "50206cfc-9ae8-4ba5-b20e-8cc800668abf", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "b3908f5f-759c-4fa8-a043-64f02554d5ec", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "662260df-4b8d-4bb9-87c5-c03ea587d915", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-usermodel-property-mapper", "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-property-mapper", "oidc-full-name-mapper", "saml-role-list-mapper" ] + } + }, { + "id" : "f6dacc02-7e0f-43ef-b80c-4a342881b6fe", + "name" : "Consent Required", + "providerId" : "consent-required", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "681b41eb-e84d-4ce1-b281-c0e313c993c4", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-usermodel-property-mapper", "oidc-address-mapper", "oidc-usermodel-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "saml-user-property-mapper", "oidc-full-name-mapper", "saml-user-attribute-mapper" ] + } + }, { + "id" : "179b3fbf-e09e-4798-8db8-ee7689838de6", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "452f130f-72fa-4623-a9e3-0bb385a54bde", + "name" : "Max Clients Limit", + "providerId" : "max-clients", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "max-clients" : [ "200" ] + } + }, { + "id" : "1898a372-a35a-42ae-88e9-27328cadaf91", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] + } + } ], + "org.keycloak.keys.KeyProvider" : [ { + "id" : "46666735-1edd-4c61-a4c9-e51a2ecba86f", + "name" : "rsa-generated", + "providerId" : "rsa-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEowIBAAKCAQEAkttcoFvNp1uhMCz9X0Tl3ZLo+uvCn65PjmrWSLUUXUX5nEMOsZyETCfCEaX8zaqkks3ybHE0soPTJJggPNqN9Bg8gFhVHDF4Nd0xHlg9cFL1E/UBFowb/7ZjLXbHTLDG1P0IluAmd+ijV9NGXFnGi1/08FGJiTQCnRU87/P2RqGwbh+S2Oqkt/P/fdHdHoUv6/iVTDsFCxxAhXQnZQtPkbjgf+RAONijGRVv6FCNwsAXl589I/qM+GuTXne9b14qJZxFs5ULQPZU3ewlGHsG/BGdljH+wnYMruLzGpm4rJkA/U994/fSl2xYnaqk67UKvCm6YGhYqVYKKEm3ROTRqwIDAQABAoIBABPZfuNqioQImw4HbrcjTrc3EF2QuJgxDS4bRlvCef5XeHMaDzuLg8hFXB0Fj1jUgUpvIslsjb+5Q19ACU91slfgChbGRGQQqFu4yixR/KVDg+kO0HcAjheZ9T15FLF1eBKOzEvpqoQXl4hwlU3jO+ECdyQfvpQjHwf7YxFYgMewBlwXPOIrmYyedUGzVb018mt37HdrS5y80UPe0ZYKJyDN1tE3+JYBZtkeuh5XTK7G/ral8IPrx2TjrDRbkkR9eiy2K/wtJGUsW0jYCV39juHYNXz8ONd3PTf68lPmLa6e5CS8sGU3Bgf0DxqSxqny0RBatKnZQQ0/ncwekiv7/O0CgYEAxSRXOTOkQDiM2OSi5TP63FSrqCjc0JcV8dqixgG4w8LU3yf9h1Xnp6xG8ZT1aI8w6DG0QcoqMgREzVFxxaCYVWUy+I0bQiWTEtyOTuOn0j3lFwwInyQtvPHR8sksrcKkszM0p06yl+NRv9yfnJeQH6OesG0e0NdkDp7n7QqQbx0CgYEAvrO1H3QaMExTtaXx55sOMp+eWUuzKPbtv0PyQpbwvAOxEgz3FJOtzv7iPB0vgXx0Qqaju+ynxBO2iI0Q38QbQR2rvwPyQess6a52d8GEQpHH9DbGSR5+lb86qAYUNjAIz3HyHUyPh1PSQjWkpdiI/4/Ka621VHq4dL2jBKltAWcCgYEArbY79Xd/v+cUlPnCX9zRq9wf9ekrydGX0KkqzPcva6/+CzUdvWcA3Up8qNrs89lY6wEBKQeO7su8JH8jlVTRZk3qAAG2hy68zd3yt6j6c9N+TjSYVEJ42wjqStjhuarNnen893/s9tGjc0BglIcow/hVGHjOso9Y51k2XDT22DECgYBEGJOymZjIaljx4kHRPDMDrK+0ny1okVQwGikl0hRAbukJ0wtfk6Ra5/UNFjbie/hf3HVXI5h8kbV6aTT/Ul2REPFFEjmthDbTcvLmrHl38+p8sBp5/aOiAVkGqrUI7HoY6U1VXQNbhXjtH46dkgbJiyTy8mJvuiqntRfDEGTWPQKBgG9tbfw+UNK0SKluAIWtzEbH8f1mAwCfBUJDX9S3XW/WeTSuzOLVAq5sg2FFC/XZXNqQI3cHo39BrvaLNmJWtQI+GTYm2qFlWUKPTEfRoNu2+vYtUlrqlqUMiupvEELZNKxjUxdv/NaXhE+ApzsZNirYYdy5t3QxF3GcsaDamXAO" ], + "keyUse" : [ "SIG" ], + "certificate" : [ "MIIClzCCAX8CBgGQGFwk6TANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARxdWl6MB4XDTI0MDYxNDIwMDUxMFoXDTM0MDYxNDIwMDY1MFowDzENMAsGA1UEAwwEcXVpejCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJLbXKBbzadboTAs/V9E5d2S6Prrwp+uT45q1ki1FF1F+ZxDDrGchEwnwhGl/M2qpJLN8mxxNLKD0ySYIDzajfQYPIBYVRwxeDXdMR5YPXBS9RP1ARaMG/+2Yy12x0ywxtT9CJbgJnfoo1fTRlxZxotf9PBRiYk0Ap0VPO/z9kahsG4fktjqpLfz/33R3R6FL+v4lUw7BQscQIV0J2ULT5G44H/kQDjYoxkVb+hQjcLAF5efPSP6jPhrk153vW9eKiWcRbOVC0D2VN3sJRh7BvwRnZYx/sJ2DK7i8xqZuKyZAP1PfeP30pdsWJ2qpOu1CrwpumBoWKlWCihJt0Tk0asCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAOb/edBv8SuU07QDWFre0atpZtpq277xfXBSkGth29jrzQbcejy1fdMgpETsqHg1YE1hjb0DyW3KEEShyXCkwRze25KSAhhXmTwIQ+zVli8i6OGtBxQ2sYP0lIBXLUQ15eRiIBJaBNoBKQ2jV30Q6aWh9l0n9lEr+xl68KPOAfrZ4/6G47/CRueBvwAfgJfke73zwuIBx6AEXAnUSCF2yj3uIR7pBAg8ktvkLex1MRqtJhHsFYPuOdMNz+eV3eq4R1MsahvCc0g20e+0wsN05KraDOlxAZ+Ja0iC8B0/LSilOnydBbamlMK4NArum2m+YkaWdwlQhcn/ysARfn9GcWQ==" ], + "priority" : [ "100" ] + } + }, { + "id" : "a096fc73-c90e-405c-a930-982750821c4e", + "name" : "aes-generated", + "providerId" : "aes-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "55f06aba-f2d6-4924-be6f-b67b79859dc5" ], + "secret" : [ "QBL58JCjftUDn2p884-oNw" ], + "priority" : [ "100" ] + } + }, { + "id" : "7770f487-ea13-4f5c-8998-ee8431d75a7c", + "name" : "hmac-generated-hs512", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "630f9486-b381-4680-ae30-6c3ca02ce944" ], + "secret" : [ "RZOq_xUFuq9h6PwJN06i-toKBJeXEHZlG3qUNZjQ4YE_tgn69fUvhw0lU5GZ0pf2WmSVb0ChJQayyX9KP79xN-tTOw3oeAwY9yzGdcAqR3Wy3BFnC9QrPsJJzvJDQPt4-H1cS18CABgdaCvlpJmxDSd91PApnMtpRlkuLahlhrU" ], + "priority" : [ "100" ], + "algorithm" : [ "HS512" ] + } + }, { + "id" : "e59712c3-805c-4947-93da-444057081de9", + "name" : "rsa-enc-generated", + "providerId" : "rsa-enc-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEpAIBAAKCAQEA1jA9OAUr098HBxh3B5Caa7nZSCCtM9YLIoXVNf0KRUDXq1SIgU3lzqG93y6rQhh1hhUQENcmoZIeyR6wseFOqE5XSmPV2qHDChtkPwAVe5TduXdaHeLO/JDZKpOlJNsrxLSEqX6I47bIN/zzpIoghaboo3X94cRbExePcfUPM8zB+Na8lBAeZ0denb53k09geuzOPxZc0Cr9bqCBUQalf5WANCozi29OgUDHuV2aR3tUZIrlBzIhQuOd8zH+rMg2L1RODWsRk62U0VBRzvtWypoTKrqWLgtroDAQH5slBj5wf9wxWyhiwatefOC47I0qfElZGMgvKFor3GY5umAbBQIDAQABAoIBABJ8J0xmZdnrLZgXLG6itDEH4RCwgUIv/tYD5tq58+D2L6bBrOY76tFidBiG1pDlu37O4owRhWv8SFvjIiy33Sn5WN86roIn48VDh+tH/pKjzl7rRgO+R07U0WkzRNVhdO2NBsX5/aZpZ+4tDnns0dG3DezNihOp9pI3PlH/bEh0Au/j60ojLKlIBrjvUr4eisjM4YVD0KGgOP/RPXDQJMu1ZnfyV04djvJvAqy3g18aVgLM3bCy5uHH096gmvD+zFkb0Y1EQitG3pe9UybqZkYBUogKcw+OJkvd0sTFUTbpqG+Ww11z3gz+7jRn+wNTO0qGY0TAvTyKGxsxacL4agECgYEA9u+IG0Bl+F4IEE9UwIkMbfbGiGgaioy3d8WyNjO59YDSmvIK3n41+PI34B1DhOhhbYUdzDAJ0yJZJmKcFe0IrqMDwe9jfqulKD3z7NFdC7w0G0VVMZuDwDelZQJzmhk7ls/Ce5f1EqmaOQe/Fs9OFab05yaVUPsJkRpyDh9EL5UCgYEA3gz617qRlKKQDXK180NqAzPHqVF96HeN1gr072ly0MVzf6FQ+Q8tJ+9xrjr2O3Q008+51rAGSZHvPEBcQ+yRqfOA8oplBPygDn6LVfPz39u5ObxBCwtqEzznbRQZJm88Fy/0pI2lNrVaJPHGDMg74pqYQaVA5kQoEOwdOYd1IbECgYEAixOHdcnEZKkZI456rmHBxWE6mfMAF85/Oo72+z4Q2yut8iSmaH9vCIHrZGc28vEqhlN1392ieu6ahVr+i/7PaARaBm08fUhRIaQa8ONN42Ehe8aF4AW1o5nzpjtGPqE9hYgIAjIojlVFjh9FyaQCV4GL53A8orpwve/GExwa/C0CgYAdxZnAYPhx+FXeJ7ozkp9dLI0hq5Z5G8Z9o1xx2S6WxVKWEzf7HMjOKPGSDb/D+vG2UIy3N0SwiouMBYkZZleldMueOWYQBBsltlvSO3JAWoJ8Njx2UaJ4T9srqd0xZQqmhTyE76TuL+SVPYFzZ0l4OYgejRylgw/oLPBCLWJMEQKBgQC2JPXpo2sO2NgIY6rRJnPe8vZwZStHEOhuXtb8xXqjAUpxlM09JvFIjVoRr5qdIdzBIZGxNMzd1osVquLtQV7yWYI51NYIRoJBJ7rn1cp96tiRgttkD+Ruw16vO6sNkpF/v/LOQmZjGhsMKH231+MZa+3m3LqLvD5IgmP/lB7WEw==" ], + "keyUse" : [ "ENC" ], + "certificate" : [ "MIIClzCCAX8CBgGQGFwlLDANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARxdWl6MB4XDTI0MDYxNDIwMDUxMFoXDTM0MDYxNDIwMDY1MFowDzENMAsGA1UEAwwEcXVpejCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANYwPTgFK9PfBwcYdweQmmu52UggrTPWCyKF1TX9CkVA16tUiIFN5c6hvd8uq0IYdYYVEBDXJqGSHskesLHhTqhOV0pj1dqhwwobZD8AFXuU3bl3Wh3izvyQ2SqTpSTbK8S0hKl+iOO2yDf886SKIIWm6KN1/eHEWxMXj3H1DzPMwfjWvJQQHmdHXp2+d5NPYHrszj8WXNAq/W6ggVEGpX+VgDQqM4tvToFAx7ldmkd7VGSK5QcyIULjnfMx/qzINi9UTg1rEZOtlNFQUc77VsqaEyq6li4La6AwEB+bJQY+cH/cMVsoYsGrXnzguOyNKnxJWRjILyhaK9xmObpgGwUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAhiPtCFXWKkwaro5ux7CFw+XPXnjoT3pAV0ydEoaunwuMR3EGGhXpWETn/bJx+9Oa5DzftYlqr7jdysKY3ighiftb6aFuUidFy37ejmF+2MxYuM+BvI70BoT5pHrU0mVB+AzawHttLhWDDLa3/pdk2N7XA6qkfsr0iXBoHeiJX1945aELJB2lJZwuyTfZbsX35uPiOiJD18Sg8yM7p4C8qSFNhuGl0kfJMjFReTcGCfoz93xppECuYtNR5e1o9tIdhmejZ+HOO+llDWOKpLw8zGuSgAgrtxuzlXP0dML7MDhxNn7iK1RH+2emC501hxqCyYUDpbwlOtr3Hr9i3WJctA==" ], + "priority" : [ "100" ], + "algorithm" : [ "RSA-OAEP" ] + } + } ] + }, + "internationalizationEnabled" : false, + "supportedLocales" : [ ], + "authenticationFlows" : [ { + "id" : "ed0cae0a-f42b-4f2d-9f96-c68b8275c9a2", + "alias" : "Account verification options", + "description" : "Method with which to verity the existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-email-verification", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Verify Existing Account by Re-authentication", + "userSetupAllowed" : false + } ] + }, { + "id" : "f758ff41-c07c-449f-b057-0f9190a6c134", + "alias" : "Browser - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "cdb3c1e0-d616-4db2-abe8-c880d104c48f", + "alias" : "Direct Grant - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "9fe9ce9b-1b53-4506-9967-d48be8534ef8", + "alias" : "First broker login - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "e2446c85-33f2-4c6d-8179-7d01ae5ac6de", + "alias" : "Handle Existing Account", + "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-confirm-link", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Account verification options", + "userSetupAllowed" : false + } ] + }, { + "id" : "3a1a7ac6-0c5d-48ee-8d35-74fc93549ea7", + "alias" : "Reset - Conditional OTP", + "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "cb7b84aa-4cd9-41fe-81d2-fb69e0b7c8ab", + "alias" : "User creation or linking", + "description" : "Flow for the existing/non-existing user alternatives", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "create unique user config", + "authenticator" : "idp-create-user-if-unique", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Handle Existing Account", + "userSetupAllowed" : false + } ] + }, { + "id" : "f3a7517a-4d3b-4b4c-b5cd-f5f426eee616", + "alias" : "Verify Existing Account by Re-authentication", + "description" : "Reauthentication of existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "First broker login - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "3dffca90-b32d-4e34-86d6-354a7bfc26ef", + "alias" : "browser", + "description" : "browser based authentication", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-cookie", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-spnego", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "identity-provider-redirector", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 25, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "forms", + "userSetupAllowed" : false + } ] + }, { + "id" : "02c4b846-004d-48ba-b56d-a8a17da937d8", + "alias" : "clients", + "description" : "Base authentication for clients", + "providerId" : "client-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "client-secret", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-secret-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-x509", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "9dc48d44-4120-49ce-b9db-0eab8d67e0f3", + "alias" : "direct grant", + "description" : "OpenID Connect Resource Owner Grant", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "direct-grant-validate-username", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "Direct Grant - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "cfb61ad1-4fcd-4726-9c38-1b93eca19a4f", + "alias" : "docker auth", + "description" : "Used by Docker clients to authenticate against the IDP", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "docker-http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "b58455a5-c796-4214-8ead-606db3bd46d1", + "alias" : "first broker login", + "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "review profile config", + "authenticator" : "idp-review-profile", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "User creation or linking", + "userSetupAllowed" : false + } ] + }, { + "id" : "04c03f92-ea11-4167-a450-69a1a191c86c", + "alias" : "forms", + "description" : "Username, password, otp and other auth forms.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Browser - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "e7d608bf-8562-4198-b0ba-45eca045eb65", + "alias" : "registration", + "description" : "registration flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-page-form", + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : true, + "flowAlias" : "registration form", + "userSetupAllowed" : false + } ] + }, { + "id" : "0835bc5d-74ff-4ae8-af2e-5680a8aa7939", + "alias" : "registration form", + "description" : "registration form", + "providerId" : "form-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-user-creation", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-password-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 50, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-recaptcha-action", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 60, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-terms-and-conditions", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 70, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "2b2e97f0-6061-4f99-8b5e-348e8671869d", + "alias" : "reset credentials", + "description" : "Reset credentials for a user if they forgot their password or something", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "reset-credentials-choose-user", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-credential-email", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 40, + "autheticatorFlow" : true, + "flowAlias" : "Reset - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "44827891-86ff-4038-873e-ec8e123edd70", + "alias" : "saml ecp", + "description" : "SAML ECP Profile Authentication Flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + } ], + "authenticatorConfig" : [ { + "id" : "9f951dda-f317-439b-af6a-d3f437770a66", + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "id" : "2886e0a1-3c7a-47ab-a648-72a1da0c291a", + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" + } + } ], + "requiredActions" : [ { + "alias" : "CONFIGURE_TOTP", + "name" : "Configure OTP", + "providerId" : "CONFIGURE_TOTP", + "enabled" : true, + "defaultAction" : false, + "priority" : 10, + "config" : { } + }, { + "alias" : "TERMS_AND_CONDITIONS", + "name" : "Terms and Conditions", + "providerId" : "TERMS_AND_CONDITIONS", + "enabled" : false, + "defaultAction" : false, + "priority" : 20, + "config" : { } + }, { + "alias" : "UPDATE_PASSWORD", + "name" : "Update Password", + "providerId" : "UPDATE_PASSWORD", + "enabled" : true, + "defaultAction" : false, + "priority" : 30, + "config" : { } + }, { + "alias" : "UPDATE_PROFILE", + "name" : "Update Profile", + "providerId" : "UPDATE_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 40, + "config" : { } + }, { + "alias" : "VERIFY_EMAIL", + "name" : "Verify Email", + "providerId" : "VERIFY_EMAIL", + "enabled" : true, + "defaultAction" : false, + "priority" : 50, + "config" : { } + }, { + "alias" : "delete_account", + "name" : "Delete Account", + "providerId" : "delete_account", + "enabled" : false, + "defaultAction" : false, + "priority" : 60, + "config" : { } + }, { + "alias" : "webauthn-register", + "name" : "Webauthn Register", + "providerId" : "webauthn-register", + "enabled" : true, + "defaultAction" : false, + "priority" : 70, + "config" : { } + }, { + "alias" : "webauthn-register-passwordless", + "name" : "Webauthn Register Passwordless", + "providerId" : "webauthn-register-passwordless", + "enabled" : true, + "defaultAction" : false, + "priority" : 80, + "config" : { } + }, { + "alias" : "VERIFY_PROFILE", + "name" : "Verify Profile", + "providerId" : "VERIFY_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 90, + "config" : { } + }, { + "alias" : "delete_credential", + "name" : "Delete Credential", + "providerId" : "delete_credential", + "enabled" : true, + "defaultAction" : false, + "priority" : 100, + "config" : { } + }, { + "alias" : "update_user_locale", + "name" : "Update User Locale", + "providerId" : "update_user_locale", + "enabled" : true, + "defaultAction" : false, + "priority" : 1000, + "config" : { } + } ], + "browserFlow" : "browser", + "registrationFlow" : "registration", + "directGrantFlow" : "direct grant", + "resetCredentialsFlow" : "reset credentials", + "clientAuthenticationFlow" : "clients", + "dockerAuthenticationFlow" : "docker auth", + "firstBrokerLoginFlow" : "first broker login", + "attributes" : { + "cibaBackchannelTokenDeliveryMode" : "poll", + "cibaExpiresIn" : "120", + "cibaAuthRequestedUserHint" : "login_hint", + "oauth2DeviceCodeLifespan" : "600", + "oauth2DevicePollingInterval" : "5", + "parRequestUriLifespan" : "60", + "cibaInterval" : "5", + "realmReusableOtpCode" : "false" + }, + "keycloakVersion" : "24.0.5", + "userManagedAccessAllowed" : false, + "clientProfiles" : { + "profiles" : [ ] + }, + "clientPolicies" : { + "policies" : [ ] + } +} \ No newline at end of file diff --git a/nginx-reverse-proxy/404.html b/nginx-reverse-proxy/404.html new file mode 100644 index 0000000..ad55ee8 --- /dev/null +++ b/nginx-reverse-proxy/404.html @@ -0,0 +1,10 @@ + + + Page Not Found + + + +

Proxy Backend Not Found +

+ + \ No newline at end of file diff --git a/nginx-reverse-proxy/Dockerfile b/nginx-reverse-proxy/Dockerfile new file mode 100644 index 0000000..0ac01aa --- /dev/null +++ b/nginx-reverse-proxy/Dockerfile @@ -0,0 +1,2 @@ +FROM nginx:stable +COPY nginx.conf /etc/nginx/nginx.conf diff --git a/nginx-reverse-proxy/nginx.conf b/nginx-reverse-proxy/nginx.conf new file mode 100644 index 0000000..c3f9ffa --- /dev/null +++ b/nginx-reverse-proxy/nginx.conf @@ -0,0 +1,67 @@ +worker_processes 1; +error_log /var/log/nginx/error.log warn; +pid /tmp/nginx.pid; +events { + worker_connections 1024; +} +http { + client_body_temp_path /tmp/client_temp; + proxy_temp_path /tmp/proxy_temp_path; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + include /etc/nginx/mime.types; + default_type application/octet-stream; + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + access_log /var/log/nginx/access.log main; + sendfile on; + keepalive_timeout 65; + + gzip on; + gzip_static on; + gzip_vary on; + gzip_proxied no-cache no-store private expired auth; + gzip_min_length 10240; + gzip_types + application/javascript + application/json + font/woff2 + text/css + text/plain; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + proxy_request_buffering off; + proxy_http_version 1.1; + proxy_intercept_errors on; + + server { + listen 80; + server_name localhost; + error_page 404 /404.html; + location = /404.html { + root /usr/share/nginx/html; + } + location / { + rewrite ^/$ /ui/ permanent; + } + location /ui { + proxy_pass http://host.docker.internal:4200/ui; + } + location /bff { + rewrite /bff/(.*) /$1 break; + proxy_pass http://host.docker.internal:7080; + } + location /resource-server { + rewrite /resource-server/(.*) /$1 break; + proxy_pass http://host.docker.internal:7084; + } + location /auth { + proxy_pass http://host.docker.internal:8080/auth; + } + } +} diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..d7e8e91 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,66 @@ +worker_processes 1; +error_log /var/log/nginx/error.log warn; +pid /tmp/nginx.pid; +events { + worker_connections 1024; +} +http { + client_body_temp_path /tmp/client_temp; + proxy_temp_path /tmp/proxy_temp_path; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + include /etc/nginx/mime.types; + default_type application/octet-stream; + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + access_log /var/log/nginx/access.log main; + sendfile on; + keepalive_timeout 65; + + gzip on; + gzip_static on; + gzip_vary on; + gzip_proxied no-cache no-store private expired auth; + gzip_min_length 10240; + gzip_types + application/javascript + application/json + font/woff2 + text/css + text/plain; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + proxy_request_buffering off; + proxy_http_version 1.1; + proxy_intercept_errors on; + + server { + listen 80; + server_name LOCALHOST_NAME; + error_page 404 /404.html; + location = /404.html { + root /usr/share/nginx/html; + } + location / { + rewrite ^/$ /ui/ permanent; + } + location /ui { + proxy_pass http://LOCALHOST_NAME:4200/ui; + } + location /bff { + rewrite /bff/(.*) /$1 break; + proxy_pass http://LOCALHOST_NAME:7080; + } + location /resource-server { + rewrite /resource-server/(.*) /$1 break; + proxy_pass http://LOCALHOST_NAME:7084; + } + location /auth { + proxy_pass http://LOCALHOST_NAME:8080/auth; + } +} diff --git a/quiz-realm.json b/quiz-realm.json new file mode 100644 index 0000000..1ebb791 --- /dev/null +++ b/quiz-realm.json @@ -0,0 +1,2051 @@ +{ + "id" : "76d4a142-1721-46e0-8074-c36907b64f89", + "realm" : "quiz", + "notBefore" : 0, + "defaultSignatureAlgorithm" : "RS256", + "revokeRefreshToken" : false, + "refreshTokenMaxReuse" : 0, + "accessTokenLifespan" : 300, + "accessTokenLifespanForImplicitFlow" : 900, + "ssoSessionIdleTimeout" : 1800, + "ssoSessionMaxLifespan" : 36000, + "ssoSessionIdleTimeoutRememberMe" : 0, + "ssoSessionMaxLifespanRememberMe" : 0, + "offlineSessionIdleTimeout" : 2592000, + "offlineSessionMaxLifespanEnabled" : false, + "offlineSessionMaxLifespan" : 5184000, + "clientSessionIdleTimeout" : 0, + "clientSessionMaxLifespan" : 0, + "clientOfflineSessionIdleTimeout" : 0, + "clientOfflineSessionMaxLifespan" : 0, + "accessCodeLifespan" : 60, + "accessCodeLifespanUserAction" : 300, + "accessCodeLifespanLogin" : 1800, + "actionTokenGeneratedByAdminLifespan" : 43200, + "actionTokenGeneratedByUserLifespan" : 300, + "oauth2DeviceCodeLifespan" : 600, + "oauth2DevicePollingInterval" : 5, + "enabled" : true, + "sslRequired" : "external", + "registrationAllowed" : false, + "registrationEmailAsUsername" : false, + "rememberMe" : false, + "verifyEmail" : false, + "loginWithEmailAllowed" : true, + "duplicateEmailsAllowed" : false, + "resetPasswordAllowed" : false, + "editUsernameAllowed" : false, + "bruteForceProtected" : false, + "permanentLockout" : false, + "maxTemporaryLockouts" : 0, + "maxFailureWaitSeconds" : 900, + "minimumQuickLoginWaitSeconds" : 60, + "waitIncrementSeconds" : 60, + "quickLoginCheckMilliSeconds" : 1000, + "maxDeltaTimeSeconds" : 43200, + "failureFactor" : 30, + "roles" : { + "realm" : [ { + "id" : "9d37e22b-ea36-4535-9434-5fd44026f841", + "name" : "default-roles-quiz", + "description" : "${role_default-roles}", + "composite" : true, + "composites" : { + "realm" : [ "offline_access", "uma_authorization" ], + "client" : { + "account" : [ "manage-account", "view-profile" ] + } + }, + "clientRole" : false, + "containerId" : "76d4a142-1721-46e0-8074-c36907b64f89", + "attributes" : { } + }, { + "id" : "bd117839-cec3-4350-ab28-6a54ee7fa56c", + "name" : "moderator", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "76d4a142-1721-46e0-8074-c36907b64f89", + "attributes" : { } + }, { + "id" : "4eb904cc-a109-4742-8b73-36a2b5f45f2f", + "name" : "uma_authorization", + "description" : "${role_uma_authorization}", + "composite" : false, + "clientRole" : false, + "containerId" : "76d4a142-1721-46e0-8074-c36907b64f89", + "attributes" : { } + }, { + "id" : "675618c2-75eb-4716-8dbb-eac7fe763b6b", + "name" : "offline_access", + "description" : "${role_offline-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "76d4a142-1721-46e0-8074-c36907b64f89", + "attributes" : { } + }, { + "id" : "25b69071-61f4-435d-9b1f-3271068f162d", + "name" : "trainer", + "description" : "", + "composite" : false, + "clientRole" : false, + "containerId" : "76d4a142-1721-46e0-8074-c36907b64f89", + "attributes" : { } + } ], + "client" : { + "quiz-admin" : [ ], + "realm-management" : [ { + "id" : "d444308b-e618-4d62-adfe-92db269ba2ff", + "name" : "view-realm", + "description" : "${role_view-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "76cd936f-87db-43c5-903a-e0436cb6f37c", + "name" : "query-users", + "description" : "${role_query-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "db7e75ed-f76f-4ebb-a250-68639b23f3c2", + "name" : "view-events", + "description" : "${role_view-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "1d5b2477-2697-4d7b-b516-3358616117b5", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "6f9e010f-81f0-489c-808c-d53ef53f6358", + "name" : "manage-events", + "description" : "${role_manage-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "ac4104a0-3e7b-4db4-a7c9-1740fc359f54", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "7414f0d7-b557-4aeb-9359-693cc3fa6a08", + "name" : "query-clients", + "description" : "${role_query-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "11a8b26f-c1b8-42f0-9551-05528f43deca", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "44b7b388-790d-4a8d-a502-e99fc009ae96", + "name" : "query-realms", + "description" : "${role_query-realms}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "aaece596-37c0-4f83-a4b9-fd210f8279ba", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "6dc2ec92-009b-4b83-b277-60ff77808087", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "6776512b-4e88-4ad8-a891-ad9384923cf1", + "name" : "realm-admin", + "description" : "${role_realm-admin}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "view-realm", "query-users", "view-events", "manage-identity-providers", "manage-events", "manage-realm", "query-clients", "view-authorization", "query-realms", "manage-authorization", "manage-clients", "view-identity-providers", "view-clients", "impersonation", "create-client", "view-users", "manage-users", "query-groups" ] + } + }, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "ff5af959-b24e-48b5-b601-6686957d2a4a", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "6d247874-cfc0-4292-b991-2472af00bf1a", + "name" : "view-clients", + "description" : "${role_view-clients}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-clients" ] + } + }, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "8246a3c9-5342-4e2a-9f6e-d28fb2c7470b", + "name" : "impersonation", + "description" : "${role_impersonation}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "261313c2-e3e7-4c13-b4f3-e901967de70f", + "name" : "create-client", + "description" : "${role_create-client}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "19d6c3ae-4cf5-4906-a4b1-047cde6e1c4f", + "name" : "view-users", + "description" : "${role_view-users}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-users", "query-groups" ] + } + }, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "70f598ca-ad42-49ad-bff5-ae8f1466d9aa", + "name" : "manage-users", + "description" : "${role_manage-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + }, { + "id" : "afea140a-1e86-4e7f-acb8-8307ef78251a", + "name" : "query-groups", + "description" : "${role_query-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "attributes" : { } + } ], + "security-admin-console" : [ ], + "admin-cli" : [ ], + "account-console" : [ ], + "broker" : [ { + "id" : "f46c10f5-8fcb-404a-a366-7707368493ba", + "name" : "read-token", + "description" : "${role_read-token}", + "composite" : false, + "clientRole" : true, + "containerId" : "30d9e8ec-6bd9-45e7-a1e4-bd3fbbcfdd58", + "attributes" : { } + } ], + "quiz-bff" : [ ], + "account" : [ { + "id" : "dfafd224-116f-42c1-a315-af921786ec1e", + "name" : "view-groups", + "description" : "${role_view-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "ab912702-9ceb-42d5-b561-0f669aa354ab", + "attributes" : { } + }, { + "id" : "02866871-b7a4-4034-94d9-641c3d6074a3", + "name" : "manage-consent", + "description" : "${role_manage-consent}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "view-consent" ] + } + }, + "clientRole" : true, + "containerId" : "ab912702-9ceb-42d5-b561-0f669aa354ab", + "attributes" : { } + }, { + "id" : "032161ca-550c-45c9-bb52-440111a58cf1", + "name" : "manage-account-links", + "description" : "${role_manage-account-links}", + "composite" : false, + "clientRole" : true, + "containerId" : "ab912702-9ceb-42d5-b561-0f669aa354ab", + "attributes" : { } + }, { + "id" : "6f520b85-63c6-4b3d-b037-51549275ae38", + "name" : "view-applications", + "description" : "${role_view-applications}", + "composite" : false, + "clientRole" : true, + "containerId" : "ab912702-9ceb-42d5-b561-0f669aa354ab", + "attributes" : { } + }, { + "id" : "fb9b0727-7f09-4dc3-8d2c-f8db7cf2090d", + "name" : "manage-account", + "description" : "${role_manage-account}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "manage-account-links" ] + } + }, + "clientRole" : true, + "containerId" : "ab912702-9ceb-42d5-b561-0f669aa354ab", + "attributes" : { } + }, { + "id" : "553624de-0e8f-46cf-aada-8e48723a5ddb", + "name" : "view-consent", + "description" : "${role_view-consent}", + "composite" : false, + "clientRole" : true, + "containerId" : "ab912702-9ceb-42d5-b561-0f669aa354ab", + "attributes" : { } + }, { + "id" : "afa0893f-9346-4a76-b006-8d52f6794984", + "name" : "delete-account", + "description" : "${role_delete-account}", + "composite" : false, + "clientRole" : true, + "containerId" : "ab912702-9ceb-42d5-b561-0f669aa354ab", + "attributes" : { } + }, { + "id" : "b8f21063-15e7-4921-9f9f-ed405f398687", + "name" : "view-profile", + "description" : "${role_view-profile}", + "composite" : false, + "clientRole" : true, + "containerId" : "ab912702-9ceb-42d5-b561-0f669aa354ab", + "attributes" : { } + } ] + } + }, + "groups" : [ ], + "defaultRole" : { + "id" : "9d37e22b-ea36-4535-9434-5fd44026f841", + "name" : "default-roles-quiz", + "description" : "${role_default-roles}", + "composite" : true, + "clientRole" : false, + "containerId" : "76d4a142-1721-46e0-8074-c36907b64f89" + }, + "requiredCredentials" : [ "password" ], + "otpPolicyType" : "totp", + "otpPolicyAlgorithm" : "HmacSHA1", + "otpPolicyInitialCounter" : 0, + "otpPolicyDigits" : 6, + "otpPolicyLookAheadWindow" : 1, + "otpPolicyPeriod" : 30, + "otpPolicyCodeReusable" : false, + "otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppGoogleName", "totpAppMicrosoftAuthenticatorName" ], + "localizationTexts" : { }, + "webAuthnPolicyRpEntityName" : "keycloak", + "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyRpId" : "", + "webAuthnPolicyAttestationConveyancePreference" : "not specified", + "webAuthnPolicyAuthenticatorAttachment" : "not specified", + "webAuthnPolicyRequireResidentKey" : "not specified", + "webAuthnPolicyUserVerificationRequirement" : "not specified", + "webAuthnPolicyCreateTimeout" : 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyAcceptableAaguids" : [ ], + "webAuthnPolicyExtraOrigins" : [ ], + "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyPasswordlessRpId" : "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", + "webAuthnPolicyPasswordlessCreateTimeout" : 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], + "webAuthnPolicyPasswordlessExtraOrigins" : [ ], + "users" : [ { + "id" : "b6d2f174-1bb7-46f7-9d47-bf8d8bedbfce", + "username" : "ch4mp", + "firstName" : "Jérôme", + "lastName" : "Wacongne", + "email" : "ch4mp@c4-soft.com", + "emailVerified" : true, + "createdTimestamp" : 1718396064665, + "enabled" : true, + "totp" : false, + "credentials" : [ { + "id" : "84894e4f-fc06-4f5b-ab0f-f7e3f648f19b", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1718396081974, + "secretData" : "{\"value\":\"5rCVWjX334a2DKeZaFwg2BDtff8rsXkzdUeoRxwoCJO7B2fzmak7mKkixk0R4Uh47ZxiEyvCf2ZBC13KzYRx9w==\",\"salt\":\"hU1AB+YrdKX58YlR5Mpksw==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":210000,\"algorithm\":\"pbkdf2-sha512\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-quiz", "moderator", "trainer" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "5d77e1e1-d918-40a4-aa30-40e5f9e669d7", + "username" : "moderator", + "firstName" : "quiz", + "lastName" : "moderator", + "email" : "moderator@c4-soft.com", + "emailVerified" : true, + "createdTimestamp" : 1718396200989, + "enabled" : true, + "totp" : false, + "credentials" : [ { + "id" : "aec883cc-680c-43c9-b4a4-4afd82f64c42", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1718396214335, + "secretData" : "{\"value\":\"spqpVpRg3nk/Oyzihxs5mKocM8xCijEt1a51kYLDH4Qn2LqDmMg5UCfOaftpqZsFuG0Lpm7ULds/Om+0hdB+LQ==\",\"salt\":\"g2HJChUBARDbzp/tCklx0Q==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":210000,\"algorithm\":\"pbkdf2-sha512\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-quiz", "moderator" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "d62d7428-704e-4aeb-bfec-fc277ed75cd9", + "username" : "service-account-quiz-admin", + "emailVerified" : false, + "createdTimestamp" : 1718654181092, + "enabled" : true, + "totp" : false, + "serviceAccountClientId" : "quiz-admin", + "credentials" : [ ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-quiz" ], + "clientRoles" : { + "realm-management" : [ "view-users" ], + "account" : [ "view-profile" ] + }, + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "c0d9b57f-ad81-47e4-b2b8-b47e1de12a0b", + "username" : "trainee", + "firstName" : "quiz", + "lastName" : "trainee", + "email" : "trainee@c4-soft.com", + "emailVerified" : true, + "createdTimestamp" : 1718396252988, + "enabled" : true, + "totp" : false, + "credentials" : [ { + "id" : "d43886a1-96e4-4561-838d-b91b8f671e3b", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1718396312762, + "secretData" : "{\"value\":\"d4DuesXjEXLNFBPKtMLvgGRwD29/Qve9g6WKCCMoGGvYEMLjNdFs0C2Lowwu8iyZL3xQ2dqO50uNRY+Tuu9WqA==\",\"salt\":\"3BdvKQH6CtUKHCL9r8N7cg==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":210000,\"algorithm\":\"pbkdf2-sha512\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-quiz" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "836d5dc9-6537-4414-8b97-9c82b13aca70", + "username" : "trainer", + "firstName" : "quiz", + "lastName" : "trainer", + "email" : "trainer@c4-soft.com", + "emailVerified" : true, + "createdTimestamp" : 1718396125143, + "enabled" : true, + "totp" : false, + "credentials" : [ { + "id" : "ae7d8c6f-f897-4408-ac42-7982159140ea", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1718396163326, + "secretData" : "{\"value\":\"8qyocDPlK0LwuxxoOjEmvzfGrJN0rbv4785485EqwIfZfA1q6Y3hxdHP4K4jMDFG+QP+wAi6bMPjLvj3YwgfEQ==\",\"salt\":\"3krsBkjCIMlcDPDu5o0j1g==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":210000,\"algorithm\":\"pbkdf2-sha512\",\"additionalParameters\":{}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-quiz", "trainer" ], + "notBefore" : 0, + "groups" : [ ] + } ], + "scopeMappings" : [ { + "clientScope" : "offline_access", + "roles" : [ "offline_access" ] + } ], + "clientScopeMappings" : { + "account" : [ { + "client" : "account-console", + "roles" : [ "manage-account", "view-groups" ] + } ] + }, + "clients" : [ { + "id" : "ab912702-9ceb-42d5-b561-0f669aa354ab", + "clientId" : "account", + "name" : "${client_account}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/quiz/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/quiz/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "1dbf212a-b451-42ad-9833-85a71ade77ee", + "clientId" : "account-console", + "name" : "${client_account-console}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/quiz/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/quiz/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "19c7f669-7251-4dad-aacf-90c07d31e3be", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "8897aa23-421f-4f6f-b75e-da6b30092a4e", + "clientId" : "admin-cli", + "name" : "${client_admin-cli}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : false, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "30d9e8ec-6bd9-45e7-a1e4-bd3fbbcfdd58", + "clientId" : "broker", + "name" : "${client_broker}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "a8e552e1-0cb9-4688-9619-c2aadeb89d7b", + "clientId" : "quiz-admin", + "name" : "", + "description" : "", + "rootUrl" : "", + "adminUrl" : "", + "baseUrl" : "", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "secret", + "redirectUris" : [ "/*" ], + "webOrigins" : [ "/*" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : false, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : true, + "publicClient" : false, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "oidc.ciba.grant.enabled" : "false", + "oauth2.device.authorization.grant.enabled" : "false", + "client.secret.creation.time" : "1718654181", + "backchannel.logout.session.required" : "true", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "protocolMappers" : [ { + "id" : "abe43a09-3f43-48fd-b1bd-89a3c5efd856", + "name" : "Client IP Address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "clientAddress", + "introspection.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "clientAddress", + "jsonType.label" : "String" + } + }, { + "id" : "a63a64c3-8415-4ed8-8979-d33c79b922c3", + "name" : "Client ID", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "client_id", + "introspection.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "client_id", + "jsonType.label" : "String" + } + }, { + "id" : "1a9f3f9d-332c-460d-8494-4a9f0d259248", + "name" : "Client Host", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "clientHost", + "introspection.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "clientHost", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "c0511d4c-a116-4030-8957-576a059d049b", + "clientId" : "quiz-bff", + "name" : "", + "description" : "", + "rootUrl" : "http://LOCALHOST_NAME/bff", + "adminUrl" : "http://LOCALHOST_NAME/bff", + "baseUrl" : "", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "secret", + "redirectUris" : [ + "http://127.0.0.1/bff/login/oauth2/code/quiz-bff", + "https://127.0.0.1/bff/login/oauth2/code/quiz-bff", + "http://localhost/bff/login/oauth2/code/quiz-bff", + "https://localhost/bff/login/oauth2/code/quiz-bff", + "http://LOCALHOST_NAME/bff/login/oauth2/code/quiz-bff", + "https://LOCALHOST_NAME/bff/login/oauth2/code/quiz-bff", + "http://host.docker.internal/bff/login/oauth2/code/quiz-bff", + "https://host.docker.internal/bff/login/oauth2/code/quiz-bff" ], + "webOrigins" : [ "+" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "oidc.ciba.grant.enabled" : "false", + "client.secret.creation.time" : "1718395789", + "backchannel.logout.session.required" : "true", + "backchannel.logout.url" : "http://LOCALHOST_NAME/bff/logout/connect/back-channel/quiz-bff", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", + "display.on.consent.screen" : "false", + "backchannel.logout.revoke.offline.tokens" : "true" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "a20241cf-c1b3-4297-8709-f50a44fc8468", + "clientId" : "realm-management", + "name" : "${client_realm-management}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "ca2f7520-4658-4401-aa9b-ce013aba30f1", + "clientId" : "security-admin-console", + "name" : "${client_security-admin-console}", + "rootUrl" : "${authAdminUrl}", + "baseUrl" : "/admin/quiz/console/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/admin/quiz/console/*" ], + "webOrigins" : [ "+" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "3fd9fe45-9dcc-4231-9138-c8d64d117111", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + } ], + "clientScopes" : [ { + "id" : "99dbb9c5-4d72-47d5-a4c7-a7fe481cd420", + "name" : "profile", + "description" : "OpenID Connect built-in scope: profile", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${profileScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "3cbab277-3c50-4edf-b650-87be63363968", + "name" : "updated at", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "updatedAt", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "updated_at", + "jsonType.label" : "long" + } + }, { + "id" : "8425c199-042e-4ece-9c72-2a31f02c3017", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + }, { + "id" : "71c19900-4675-4c1b-8c40-20d127492263", + "name" : "nickname", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "nickname", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "nickname", + "jsonType.label" : "String" + } + }, { + "id" : "5189656b-92e2-46b0-ae21-adaf8dcee669", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "ed9a01e0-2ad6-4909-b7f1-229a341dedf1", + "name" : "website", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "website", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "website", + "jsonType.label" : "String" + } + }, { + "id" : "676080d9-6192-4517-99cb-6b69ab2d1b83", + "name" : "birthdate", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "birthdate", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "birthdate", + "jsonType.label" : "String" + } + }, { + "id" : "f0fac647-6b9d-4202-af89-3230aa8c1114", + "name" : "zoneinfo", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "zoneinfo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "zoneinfo", + "jsonType.label" : "String" + } + }, { + "id" : "5fb4a1ae-b4d7-4321-8dbf-d602f11a4110", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "b004cae7-e41d-4fc3-998f-54798e255942", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "introspection.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + }, { + "id" : "0f4e547f-2e0d-4b36-8268-44de3baec468", + "name" : "picture", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "picture", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "picture", + "jsonType.label" : "String" + } + }, { + "id" : "d6a6d2c5-b336-499d-acd2-0e77e9582423", + "name" : "middle name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "middleName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "middle_name", + "jsonType.label" : "String" + } + }, { + "id" : "13014abe-4b35-4bc8-834a-0fd62a7ec861", + "name" : "gender", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "gender", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "gender", + "jsonType.label" : "String" + } + }, { + "id" : "2f85838d-4542-4968-a4ee-5141b1388452", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "d0fb56cd-5c9a-4a3b-a24b-25af4e69cf89", + "name" : "profile", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "profile", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "profile", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "76362470-7974-42ed-857e-7834000a6426", + "name" : "acr", + "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "fb1879f4-a587-461b-a9d0-83514354d77e", + "name" : "acr loa level", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-acr-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "introspection.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + } ] + }, { + "id" : "ed00af57-3e31-43f2-b5c1-c28c9f3ab7b9", + "name" : "role_list", + "description" : "SAML role list", + "protocol" : "saml", + "attributes" : { + "consent.screen.text" : "${samlRoleListScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "241df7cf-5245-4fae-9529-d8ddd071e195", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + } ] + }, { + "id" : "bf321dc1-190d-4924-b990-a27191467f19", + "name" : "address", + "description" : "OpenID Connect built-in scope: address", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${addressScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "96f86cf5-6ccb-4e6f-8dd3-7bb186ba5723", + "name" : "address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-address-mapper", + "consentRequired" : false, + "config" : { + "user.attribute.formatted" : "formatted", + "user.attribute.country" : "country", + "introspection.token.claim" : "true", + "user.attribute.postal_code" : "postal_code", + "userinfo.token.claim" : "true", + "user.attribute.street" : "street", + "id.token.claim" : "true", + "user.attribute.region" : "region", + "access.token.claim" : "true", + "user.attribute.locality" : "locality" + } + } ] + }, { + "id" : "3fbd86f0-2a7e-4870-868a-b7be2ccaac68", + "name" : "offline_access", + "description" : "OpenID Connect built-in scope: offline_access", + "protocol" : "openid-connect", + "attributes" : { + "consent.screen.text" : "${offlineAccessScopeConsentText}", + "display.on.consent.screen" : "true" + } + }, { + "id" : "ca9e835c-a297-43d3-9cb9-f5c480161cfc", + "name" : "email", + "description" : "OpenID Connect built-in scope: email", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${emailScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "b92539ca-d028-48eb-80c9-dbe1ed4f588b", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + }, { + "id" : "8d72b099-2ab3-43c5-9174-b4a5325c6840", + "name" : "email verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "emailVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email_verified", + "jsonType.label" : "boolean" + } + } ] + }, { + "id" : "71924996-41a4-4e9c-b674-e59d7b7557fa", + "name" : "web-origins", + "description" : "OpenID Connect scope for add allowed web origins to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false", + "consent.screen.text" : "" + }, + "protocolMappers" : [ { + "id" : "bcd87c60-b873-40b3-bd80-f9525c66d508", + "name" : "allowed web origins", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-allowed-origins-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "access.token.claim" : "true" + } + } ] + }, { + "id" : "4c26130a-a8e5-4dc2-971f-30f8fde200fc", + "name" : "phone", + "description" : "OpenID Connect built-in scope: phone", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${phoneScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "e1e4a4a2-8a43-4943-9beb-c0c892c023d5", + "name" : "phone number", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumber", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number", + "jsonType.label" : "String" + } + }, { + "id" : "c0c15b77-e330-48ef-9c1b-bb0ad0f751c8", + "name" : "phone number verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumberVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number_verified", + "jsonType.label" : "boolean" + } + } ] + }, { + "id" : "14ca39e6-3045-4013-86be-343912bfa2b2", + "name" : "roles", + "description" : "OpenID Connect scope for add user roles to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${rolesScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "03260206-f8bf-43d9-84dd-46967983a02e", + "name" : "realm roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "multivalued" : "true", + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "realm_access.roles", + "jsonType.label" : "String" + } + }, { + "id" : "87b834c9-afe6-4ced-8fb7-516351289c66", + "name" : "client roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-client-role-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "multivalued" : "true", + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "resource_access.${client_id}.roles", + "jsonType.label" : "String" + } + }, { + "id" : "97bed3cf-38a5-4cde-ad71-0fe909c72eb7", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "access.token.claim" : "true" + } + } ] + }, { + "id" : "27ac15a4-ba92-4aa1-839d-7e54d71f8f56", + "name" : "microprofile-jwt", + "description" : "Microprofile - JWT built-in scope", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "32a2dcd2-9149-4fd3-9f70-eb33a4a9f673", + "name" : "upn", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "upn", + "jsonType.label" : "String" + } + }, { + "id" : "f78a669b-d9a4-4936-a02c-fa2162b00686", + "name" : "groups", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "multivalued" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "foo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "groups", + "jsonType.label" : "String" + } + } ] + } ], + "defaultDefaultClientScopes" : [ "role_list", "profile", "email", "roles", "web-origins", "acr" ], + "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt" ], + "browserSecurityHeaders" : { + "contentSecurityPolicyReportOnly" : "", + "xContentTypeOptions" : "nosniff", + "referrerPolicy" : "no-referrer", + "xRobotsTag" : "none", + "xFrameOptions" : "SAMEORIGIN", + "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection" : "1; mode=block", + "strictTransportSecurity" : "max-age=31536000; includeSubDomains" + }, + "smtpServer" : { }, + "eventsEnabled" : false, + "eventsListeners" : [ "jboss-logging" ], + "enabledEventTypes" : [ ], + "adminEventsEnabled" : false, + "adminEventsDetailsEnabled" : false, + "identityProviders" : [ ], + "identityProviderMappers" : [ ], + "components" : { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { + "id" : "50206cfc-9ae8-4ba5-b20e-8cc800668abf", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "b3908f5f-759c-4fa8-a043-64f02554d5ec", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "662260df-4b8d-4bb9-87c5-c03ea587d915", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper", "saml-user-property-mapper", "oidc-address-mapper" ] + } + }, { + "id" : "f6dacc02-7e0f-43ef-b80c-4a342881b6fe", + "name" : "Consent Required", + "providerId" : "consent-required", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "681b41eb-e84d-4ce1-b281-c0e313c993c4", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper", "oidc-address-mapper", "saml-user-property-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper", "saml-user-attribute-mapper" ] + } + }, { + "id" : "179b3fbf-e09e-4798-8db8-ee7689838de6", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "452f130f-72fa-4623-a9e3-0bb385a54bde", + "name" : "Max Clients Limit", + "providerId" : "max-clients", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "max-clients" : [ "200" ] + } + }, { + "id" : "1898a372-a35a-42ae-88e9-27328cadaf91", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] + } + } ], + "org.keycloak.keys.KeyProvider" : [ { + "id" : "46666735-1edd-4c61-a4c9-e51a2ecba86f", + "name" : "rsa-generated", + "providerId" : "rsa-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEowIBAAKCAQEAkttcoFvNp1uhMCz9X0Tl3ZLo+uvCn65PjmrWSLUUXUX5nEMOsZyETCfCEaX8zaqkks3ybHE0soPTJJggPNqN9Bg8gFhVHDF4Nd0xHlg9cFL1E/UBFowb/7ZjLXbHTLDG1P0IluAmd+ijV9NGXFnGi1/08FGJiTQCnRU87/P2RqGwbh+S2Oqkt/P/fdHdHoUv6/iVTDsFCxxAhXQnZQtPkbjgf+RAONijGRVv6FCNwsAXl589I/qM+GuTXne9b14qJZxFs5ULQPZU3ewlGHsG/BGdljH+wnYMruLzGpm4rJkA/U994/fSl2xYnaqk67UKvCm6YGhYqVYKKEm3ROTRqwIDAQABAoIBABPZfuNqioQImw4HbrcjTrc3EF2QuJgxDS4bRlvCef5XeHMaDzuLg8hFXB0Fj1jUgUpvIslsjb+5Q19ACU91slfgChbGRGQQqFu4yixR/KVDg+kO0HcAjheZ9T15FLF1eBKOzEvpqoQXl4hwlU3jO+ECdyQfvpQjHwf7YxFYgMewBlwXPOIrmYyedUGzVb018mt37HdrS5y80UPe0ZYKJyDN1tE3+JYBZtkeuh5XTK7G/ral8IPrx2TjrDRbkkR9eiy2K/wtJGUsW0jYCV39juHYNXz8ONd3PTf68lPmLa6e5CS8sGU3Bgf0DxqSxqny0RBatKnZQQ0/ncwekiv7/O0CgYEAxSRXOTOkQDiM2OSi5TP63FSrqCjc0JcV8dqixgG4w8LU3yf9h1Xnp6xG8ZT1aI8w6DG0QcoqMgREzVFxxaCYVWUy+I0bQiWTEtyOTuOn0j3lFwwInyQtvPHR8sksrcKkszM0p06yl+NRv9yfnJeQH6OesG0e0NdkDp7n7QqQbx0CgYEAvrO1H3QaMExTtaXx55sOMp+eWUuzKPbtv0PyQpbwvAOxEgz3FJOtzv7iPB0vgXx0Qqaju+ynxBO2iI0Q38QbQR2rvwPyQess6a52d8GEQpHH9DbGSR5+lb86qAYUNjAIz3HyHUyPh1PSQjWkpdiI/4/Ka621VHq4dL2jBKltAWcCgYEArbY79Xd/v+cUlPnCX9zRq9wf9ekrydGX0KkqzPcva6/+CzUdvWcA3Up8qNrs89lY6wEBKQeO7su8JH8jlVTRZk3qAAG2hy68zd3yt6j6c9N+TjSYVEJ42wjqStjhuarNnen893/s9tGjc0BglIcow/hVGHjOso9Y51k2XDT22DECgYBEGJOymZjIaljx4kHRPDMDrK+0ny1okVQwGikl0hRAbukJ0wtfk6Ra5/UNFjbie/hf3HVXI5h8kbV6aTT/Ul2REPFFEjmthDbTcvLmrHl38+p8sBp5/aOiAVkGqrUI7HoY6U1VXQNbhXjtH46dkgbJiyTy8mJvuiqntRfDEGTWPQKBgG9tbfw+UNK0SKluAIWtzEbH8f1mAwCfBUJDX9S3XW/WeTSuzOLVAq5sg2FFC/XZXNqQI3cHo39BrvaLNmJWtQI+GTYm2qFlWUKPTEfRoNu2+vYtUlrqlqUMiupvEELZNKxjUxdv/NaXhE+ApzsZNirYYdy5t3QxF3GcsaDamXAO" ], + "keyUse" : [ "SIG" ], + "certificate" : [ "MIIClzCCAX8CBgGQGFwk6TANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARxdWl6MB4XDTI0MDYxNDIwMDUxMFoXDTM0MDYxNDIwMDY1MFowDzENMAsGA1UEAwwEcXVpejCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJLbXKBbzadboTAs/V9E5d2S6Prrwp+uT45q1ki1FF1F+ZxDDrGchEwnwhGl/M2qpJLN8mxxNLKD0ySYIDzajfQYPIBYVRwxeDXdMR5YPXBS9RP1ARaMG/+2Yy12x0ywxtT9CJbgJnfoo1fTRlxZxotf9PBRiYk0Ap0VPO/z9kahsG4fktjqpLfz/33R3R6FL+v4lUw7BQscQIV0J2ULT5G44H/kQDjYoxkVb+hQjcLAF5efPSP6jPhrk153vW9eKiWcRbOVC0D2VN3sJRh7BvwRnZYx/sJ2DK7i8xqZuKyZAP1PfeP30pdsWJ2qpOu1CrwpumBoWKlWCihJt0Tk0asCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAOb/edBv8SuU07QDWFre0atpZtpq277xfXBSkGth29jrzQbcejy1fdMgpETsqHg1YE1hjb0DyW3KEEShyXCkwRze25KSAhhXmTwIQ+zVli8i6OGtBxQ2sYP0lIBXLUQ15eRiIBJaBNoBKQ2jV30Q6aWh9l0n9lEr+xl68KPOAfrZ4/6G47/CRueBvwAfgJfke73zwuIBx6AEXAnUSCF2yj3uIR7pBAg8ktvkLex1MRqtJhHsFYPuOdMNz+eV3eq4R1MsahvCc0g20e+0wsN05KraDOlxAZ+Ja0iC8B0/LSilOnydBbamlMK4NArum2m+YkaWdwlQhcn/ysARfn9GcWQ==" ], + "priority" : [ "100" ] + } + }, { + "id" : "a096fc73-c90e-405c-a930-982750821c4e", + "name" : "aes-generated", + "providerId" : "aes-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "55f06aba-f2d6-4924-be6f-b67b79859dc5" ], + "secret" : [ "QBL58JCjftUDn2p884-oNw" ], + "priority" : [ "100" ] + } + }, { + "id" : "7770f487-ea13-4f5c-8998-ee8431d75a7c", + "name" : "hmac-generated-hs512", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "630f9486-b381-4680-ae30-6c3ca02ce944" ], + "secret" : [ "RZOq_xUFuq9h6PwJN06i-toKBJeXEHZlG3qUNZjQ4YE_tgn69fUvhw0lU5GZ0pf2WmSVb0ChJQayyX9KP79xN-tTOw3oeAwY9yzGdcAqR3Wy3BFnC9QrPsJJzvJDQPt4-H1cS18CABgdaCvlpJmxDSd91PApnMtpRlkuLahlhrU" ], + "priority" : [ "100" ], + "algorithm" : [ "HS512" ] + } + }, { + "id" : "e59712c3-805c-4947-93da-444057081de9", + "name" : "rsa-enc-generated", + "providerId" : "rsa-enc-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEpAIBAAKCAQEA1jA9OAUr098HBxh3B5Caa7nZSCCtM9YLIoXVNf0KRUDXq1SIgU3lzqG93y6rQhh1hhUQENcmoZIeyR6wseFOqE5XSmPV2qHDChtkPwAVe5TduXdaHeLO/JDZKpOlJNsrxLSEqX6I47bIN/zzpIoghaboo3X94cRbExePcfUPM8zB+Na8lBAeZ0denb53k09geuzOPxZc0Cr9bqCBUQalf5WANCozi29OgUDHuV2aR3tUZIrlBzIhQuOd8zH+rMg2L1RODWsRk62U0VBRzvtWypoTKrqWLgtroDAQH5slBj5wf9wxWyhiwatefOC47I0qfElZGMgvKFor3GY5umAbBQIDAQABAoIBABJ8J0xmZdnrLZgXLG6itDEH4RCwgUIv/tYD5tq58+D2L6bBrOY76tFidBiG1pDlu37O4owRhWv8SFvjIiy33Sn5WN86roIn48VDh+tH/pKjzl7rRgO+R07U0WkzRNVhdO2NBsX5/aZpZ+4tDnns0dG3DezNihOp9pI3PlH/bEh0Au/j60ojLKlIBrjvUr4eisjM4YVD0KGgOP/RPXDQJMu1ZnfyV04djvJvAqy3g18aVgLM3bCy5uHH096gmvD+zFkb0Y1EQitG3pe9UybqZkYBUogKcw+OJkvd0sTFUTbpqG+Ww11z3gz+7jRn+wNTO0qGY0TAvTyKGxsxacL4agECgYEA9u+IG0Bl+F4IEE9UwIkMbfbGiGgaioy3d8WyNjO59YDSmvIK3n41+PI34B1DhOhhbYUdzDAJ0yJZJmKcFe0IrqMDwe9jfqulKD3z7NFdC7w0G0VVMZuDwDelZQJzmhk7ls/Ce5f1EqmaOQe/Fs9OFab05yaVUPsJkRpyDh9EL5UCgYEA3gz617qRlKKQDXK180NqAzPHqVF96HeN1gr072ly0MVzf6FQ+Q8tJ+9xrjr2O3Q008+51rAGSZHvPEBcQ+yRqfOA8oplBPygDn6LVfPz39u5ObxBCwtqEzznbRQZJm88Fy/0pI2lNrVaJPHGDMg74pqYQaVA5kQoEOwdOYd1IbECgYEAixOHdcnEZKkZI456rmHBxWE6mfMAF85/Oo72+z4Q2yut8iSmaH9vCIHrZGc28vEqhlN1392ieu6ahVr+i/7PaARaBm08fUhRIaQa8ONN42Ehe8aF4AW1o5nzpjtGPqE9hYgIAjIojlVFjh9FyaQCV4GL53A8orpwve/GExwa/C0CgYAdxZnAYPhx+FXeJ7ozkp9dLI0hq5Z5G8Z9o1xx2S6WxVKWEzf7HMjOKPGSDb/D+vG2UIy3N0SwiouMBYkZZleldMueOWYQBBsltlvSO3JAWoJ8Njx2UaJ4T9srqd0xZQqmhTyE76TuL+SVPYFzZ0l4OYgejRylgw/oLPBCLWJMEQKBgQC2JPXpo2sO2NgIY6rRJnPe8vZwZStHEOhuXtb8xXqjAUpxlM09JvFIjVoRr5qdIdzBIZGxNMzd1osVquLtQV7yWYI51NYIRoJBJ7rn1cp96tiRgttkD+Ruw16vO6sNkpF/v/LOQmZjGhsMKH231+MZa+3m3LqLvD5IgmP/lB7WEw==" ], + "keyUse" : [ "ENC" ], + "certificate" : [ "MIIClzCCAX8CBgGQGFwlLDANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARxdWl6MB4XDTI0MDYxNDIwMDUxMFoXDTM0MDYxNDIwMDY1MFowDzENMAsGA1UEAwwEcXVpejCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANYwPTgFK9PfBwcYdweQmmu52UggrTPWCyKF1TX9CkVA16tUiIFN5c6hvd8uq0IYdYYVEBDXJqGSHskesLHhTqhOV0pj1dqhwwobZD8AFXuU3bl3Wh3izvyQ2SqTpSTbK8S0hKl+iOO2yDf886SKIIWm6KN1/eHEWxMXj3H1DzPMwfjWvJQQHmdHXp2+d5NPYHrszj8WXNAq/W6ggVEGpX+VgDQqM4tvToFAx7ldmkd7VGSK5QcyIULjnfMx/qzINi9UTg1rEZOtlNFQUc77VsqaEyq6li4La6AwEB+bJQY+cH/cMVsoYsGrXnzguOyNKnxJWRjILyhaK9xmObpgGwUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAhiPtCFXWKkwaro5ux7CFw+XPXnjoT3pAV0ydEoaunwuMR3EGGhXpWETn/bJx+9Oa5DzftYlqr7jdysKY3ighiftb6aFuUidFy37ejmF+2MxYuM+BvI70BoT5pHrU0mVB+AzawHttLhWDDLa3/pdk2N7XA6qkfsr0iXBoHeiJX1945aELJB2lJZwuyTfZbsX35uPiOiJD18Sg8yM7p4C8qSFNhuGl0kfJMjFReTcGCfoz93xppECuYtNR5e1o9tIdhmejZ+HOO+llDWOKpLw8zGuSgAgrtxuzlXP0dML7MDhxNn7iK1RH+2emC501hxqCyYUDpbwlOtr3Hr9i3WJctA==" ], + "priority" : [ "100" ], + "algorithm" : [ "RSA-OAEP" ] + } + } ] + }, + "internationalizationEnabled" : false, + "supportedLocales" : [ ], + "authenticationFlows" : [ { + "id" : "ed0cae0a-f42b-4f2d-9f96-c68b8275c9a2", + "alias" : "Account verification options", + "description" : "Method with which to verity the existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-email-verification", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Verify Existing Account by Re-authentication", + "userSetupAllowed" : false + } ] + }, { + "id" : "f758ff41-c07c-449f-b057-0f9190a6c134", + "alias" : "Browser - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "cdb3c1e0-d616-4db2-abe8-c880d104c48f", + "alias" : "Direct Grant - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "9fe9ce9b-1b53-4506-9967-d48be8534ef8", + "alias" : "First broker login - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "e2446c85-33f2-4c6d-8179-7d01ae5ac6de", + "alias" : "Handle Existing Account", + "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-confirm-link", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Account verification options", + "userSetupAllowed" : false + } ] + }, { + "id" : "3a1a7ac6-0c5d-48ee-8d35-74fc93549ea7", + "alias" : "Reset - Conditional OTP", + "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "cb7b84aa-4cd9-41fe-81d2-fb69e0b7c8ab", + "alias" : "User creation or linking", + "description" : "Flow for the existing/non-existing user alternatives", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "create unique user config", + "authenticator" : "idp-create-user-if-unique", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Handle Existing Account", + "userSetupAllowed" : false + } ] + }, { + "id" : "f3a7517a-4d3b-4b4c-b5cd-f5f426eee616", + "alias" : "Verify Existing Account by Re-authentication", + "description" : "Reauthentication of existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "First broker login - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "3dffca90-b32d-4e34-86d6-354a7bfc26ef", + "alias" : "browser", + "description" : "browser based authentication", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-cookie", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-spnego", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "identity-provider-redirector", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 25, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "forms", + "userSetupAllowed" : false + } ] + }, { + "id" : "02c4b846-004d-48ba-b56d-a8a17da937d8", + "alias" : "clients", + "description" : "Base authentication for clients", + "providerId" : "client-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "client-secret", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-secret-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-x509", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "9dc48d44-4120-49ce-b9db-0eab8d67e0f3", + "alias" : "direct grant", + "description" : "OpenID Connect Resource Owner Grant", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "direct-grant-validate-username", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "Direct Grant - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "cfb61ad1-4fcd-4726-9c38-1b93eca19a4f", + "alias" : "docker auth", + "description" : "Used by Docker clients to authenticate against the IDP", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "docker-http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "b58455a5-c796-4214-8ead-606db3bd46d1", + "alias" : "first broker login", + "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "review profile config", + "authenticator" : "idp-review-profile", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "User creation or linking", + "userSetupAllowed" : false + } ] + }, { + "id" : "04c03f92-ea11-4167-a450-69a1a191c86c", + "alias" : "forms", + "description" : "Username, password, otp and other auth forms.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Browser - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "e7d608bf-8562-4198-b0ba-45eca045eb65", + "alias" : "registration", + "description" : "registration flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-page-form", + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : true, + "flowAlias" : "registration form", + "userSetupAllowed" : false + } ] + }, { + "id" : "0835bc5d-74ff-4ae8-af2e-5680a8aa7939", + "alias" : "registration form", + "description" : "registration form", + "providerId" : "form-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-user-creation", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-password-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 50, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-recaptcha-action", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 60, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-terms-and-conditions", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 70, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "2b2e97f0-6061-4f99-8b5e-348e8671869d", + "alias" : "reset credentials", + "description" : "Reset credentials for a user if they forgot their password or something", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "reset-credentials-choose-user", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-credential-email", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 40, + "autheticatorFlow" : true, + "flowAlias" : "Reset - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "44827891-86ff-4038-873e-ec8e123edd70", + "alias" : "saml ecp", + "description" : "SAML ECP Profile Authentication Flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + } ], + "authenticatorConfig" : [ { + "id" : "9f951dda-f317-439b-af6a-d3f437770a66", + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "id" : "2886e0a1-3c7a-47ab-a648-72a1da0c291a", + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" + } + } ], + "requiredActions" : [ { + "alias" : "CONFIGURE_TOTP", + "name" : "Configure OTP", + "providerId" : "CONFIGURE_TOTP", + "enabled" : true, + "defaultAction" : false, + "priority" : 10, + "config" : { } + }, { + "alias" : "TERMS_AND_CONDITIONS", + "name" : "Terms and Conditions", + "providerId" : "TERMS_AND_CONDITIONS", + "enabled" : false, + "defaultAction" : false, + "priority" : 20, + "config" : { } + }, { + "alias" : "UPDATE_PASSWORD", + "name" : "Update Password", + "providerId" : "UPDATE_PASSWORD", + "enabled" : true, + "defaultAction" : false, + "priority" : 30, + "config" : { } + }, { + "alias" : "UPDATE_PROFILE", + "name" : "Update Profile", + "providerId" : "UPDATE_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 40, + "config" : { } + }, { + "alias" : "VERIFY_EMAIL", + "name" : "Verify Email", + "providerId" : "VERIFY_EMAIL", + "enabled" : true, + "defaultAction" : false, + "priority" : 50, + "config" : { } + }, { + "alias" : "delete_account", + "name" : "Delete Account", + "providerId" : "delete_account", + "enabled" : false, + "defaultAction" : false, + "priority" : 60, + "config" : { } + }, { + "alias" : "webauthn-register", + "name" : "Webauthn Register", + "providerId" : "webauthn-register", + "enabled" : true, + "defaultAction" : false, + "priority" : 70, + "config" : { } + }, { + "alias" : "webauthn-register-passwordless", + "name" : "Webauthn Register Passwordless", + "providerId" : "webauthn-register-passwordless", + "enabled" : true, + "defaultAction" : false, + "priority" : 80, + "config" : { } + }, { + "alias" : "VERIFY_PROFILE", + "name" : "Verify Profile", + "providerId" : "VERIFY_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 90, + "config" : { } + }, { + "alias" : "delete_credential", + "name" : "Delete Credential", + "providerId" : "delete_credential", + "enabled" : true, + "defaultAction" : false, + "priority" : 100, + "config" : { } + }, { + "alias" : "update_user_locale", + "name" : "Update User Locale", + "providerId" : "update_user_locale", + "enabled" : true, + "defaultAction" : false, + "priority" : 1000, + "config" : { } + } ], + "browserFlow" : "browser", + "registrationFlow" : "registration", + "directGrantFlow" : "direct grant", + "resetCredentialsFlow" : "reset credentials", + "clientAuthenticationFlow" : "clients", + "dockerAuthenticationFlow" : "docker auth", + "firstBrokerLoginFlow" : "first broker login", + "attributes" : { + "cibaBackchannelTokenDeliveryMode" : "poll", + "cibaExpiresIn" : "120", + "cibaAuthRequestedUserHint" : "login_hint", + "oauth2DeviceCodeLifespan" : "600", + "clientOfflineSessionMaxLifespan" : "0", + "oauth2DevicePollingInterval" : "5", + "clientSessionIdleTimeout" : "0", + "parRequestUriLifespan" : "60", + "clientSessionMaxLifespan" : "0", + "clientOfflineSessionIdleTimeout" : "0", + "cibaInterval" : "5", + "realmReusableOtpCode" : "false" + }, + "keycloakVersion" : "24.0.0", + "userManagedAccessAllowed" : false, + "clientProfiles" : { + "profiles" : [ ] + }, + "clientPolicies" : { + "policies" : [ ] + } +} \ No newline at end of file