mirror of
https://github.com/keycloak/keycloak.git
synced 2026-05-26 13:50:48 +00:00
feat(admin-api-v2): automatically update openapi file used by JS client (#46472)
* Closes: https://github.com/keycloak/keycloak/issues/46388 Signed-off-by: Michal Vavřík <michal.vavrik@aol.com>
This commit is contained in:
+19
-57
@@ -11,17 +11,10 @@ components:
|
||||
type: string
|
||||
certificate:
|
||||
type: string
|
||||
BaseRepresentation:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Resource identifier
|
||||
additionalProperties: true
|
||||
BaseClientRepresentation:
|
||||
description: Base client representation with common properties for all client
|
||||
types
|
||||
type: object
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/BaseRepresentation"
|
||||
properties:
|
||||
clientId:
|
||||
type: string
|
||||
@@ -43,9 +36,13 @@ components:
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: string
|
||||
additionalFields:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
protocol:
|
||||
type: string
|
||||
discriminator:
|
||||
propertyName: protocol
|
||||
mapping:
|
||||
openid-connect: "#/components/schemas/OIDCClientRepresentation"
|
||||
saml: "#/components/schemas/SAMLClientRepresentation"
|
||||
Flow:
|
||||
type: string
|
||||
enum:
|
||||
@@ -58,8 +55,6 @@ components:
|
||||
- CIBA
|
||||
OIDCClientRepresentation:
|
||||
type: object
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/BaseClientRepresentation"
|
||||
properties:
|
||||
loginFlows:
|
||||
type: array
|
||||
@@ -80,11 +75,11 @@ components:
|
||||
type: string
|
||||
protocol:
|
||||
type: string
|
||||
SAMLClientRepresentation:
|
||||
type: object
|
||||
description: SAML Client configuration
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/BaseClientRepresentation"
|
||||
SAMLClientRepresentation:
|
||||
description: SAML Client configuration
|
||||
type: object
|
||||
properties:
|
||||
nameIdFormat:
|
||||
type: string
|
||||
@@ -112,6 +107,8 @@ components:
|
||||
type: boolean
|
||||
protocol:
|
||||
type: string
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/BaseClientRepresentation"
|
||||
tags:
|
||||
- name: Clients (v2)
|
||||
x-smallrye-profile-admin: ""
|
||||
@@ -131,14 +128,7 @@ paths:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/OIDCClientRepresentation"
|
||||
- $ref: "#/components/schemas/SAMLClientRepresentation"
|
||||
discriminator:
|
||||
propertyName: protocol
|
||||
mapping:
|
||||
openid-connect: "#/components/schemas/OIDCClientRepresentation"
|
||||
saml: "#/components/schemas/SAMLClientRepresentation"
|
||||
$ref: "#/components/schemas/BaseClientRepresentation"
|
||||
post:
|
||||
summary: Create a new client
|
||||
description: Creates a new client in the realm
|
||||
@@ -149,14 +139,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/OIDCClientRepresentation"
|
||||
- $ref: "#/components/schemas/SAMLClientRepresentation"
|
||||
discriminator:
|
||||
propertyName: protocol
|
||||
mapping:
|
||||
openid-connect: "#/components/schemas/OIDCClientRepresentation"
|
||||
saml: "#/components/schemas/SAMLClientRepresentation"
|
||||
$ref: "#/components/schemas/BaseClientRepresentation"
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
@@ -187,14 +170,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/OIDCClientRepresentation"
|
||||
- $ref: "#/components/schemas/SAMLClientRepresentation"
|
||||
discriminator:
|
||||
propertyName: protocol
|
||||
mapping:
|
||||
openid-connect: "#/components/schemas/OIDCClientRepresentation"
|
||||
saml: "#/components/schemas/SAMLClientRepresentation"
|
||||
$ref: "#/components/schemas/BaseClientRepresentation"
|
||||
put:
|
||||
operationId: createOrUpdateClient
|
||||
tags:
|
||||
@@ -203,14 +179,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/OIDCClientRepresentation"
|
||||
- $ref: "#/components/schemas/SAMLClientRepresentation"
|
||||
discriminator:
|
||||
propertyName: protocol
|
||||
mapping:
|
||||
openid-connect: "#/components/schemas/OIDCClientRepresentation"
|
||||
saml: "#/components/schemas/SAMLClientRepresentation"
|
||||
$ref: "#/components/schemas/BaseClientRepresentation"
|
||||
required: true
|
||||
responses:
|
||||
"200":
|
||||
@@ -232,14 +201,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/OIDCClientRepresentation"
|
||||
- $ref: "#/components/schemas/SAMLClientRepresentation"
|
||||
discriminator:
|
||||
propertyName: protocol
|
||||
mapping:
|
||||
openid-connect: "#/components/schemas/OIDCClientRepresentation"
|
||||
saml: "#/components/schemas/SAMLClientRepresentation"
|
||||
$ref: "#/components/schemas/BaseClientRepresentation"
|
||||
delete:
|
||||
operationId: deleteClient
|
||||
tags:
|
||||
@@ -37,9 +37,9 @@
|
||||
]
|
||||
},
|
||||
"generate:openapi": {
|
||||
"command": "kiota generate -l typescript -d api.yml -c AdminClient -o ./src/generated",
|
||||
"command": "kiota generate -l typescript -d openapi.yaml -c AdminClient -o ./src/generated",
|
||||
"files": [
|
||||
"api.yml"
|
||||
"openapi.yaml"
|
||||
],
|
||||
"output": [
|
||||
"src/generated"
|
||||
|
||||
+15
@@ -23,6 +23,21 @@
|
||||
<module>themes-vendor</module>
|
||||
</modules>
|
||||
|
||||
<dependencies>
|
||||
<!-- Enforce build order: openapi.yaml must be generated before JS build -->
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-admin-v2-rest</artifactId>
|
||||
<scope>provided</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>*</groupId>
|
||||
<artifactId>*</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<properties>
|
||||
<node.version>v24.9.0</node.version>
|
||||
<pnpm.version>10.14.0</pnpm.version>
|
||||
|
||||
@@ -91,18 +91,10 @@ kc.log-file=${kc.home.dir:default}${file.separator}data${file.separator}log${fil
|
||||
|
||||
#OpenAPI defaults
|
||||
quarkus.smallrye-openapi.path=/openapi
|
||||
quarkus.smallrye-openapi.store-schema-directory=${openapi.schema.target}
|
||||
quarkus.swagger-ui.path=${quarkus.smallrye-openapi.path}/ui
|
||||
quarkus.swagger-ui.always-include=true
|
||||
quarkus.swagger-ui.filter=true
|
||||
mp.openapi.scan.packages=org.keycloak.representations.admin.v2,org.keycloak.admin.api,org.keycloak.quarkus.runtime.oas,io.quarkus.smallrye.openapi
|
||||
mp.openapi.extensions.smallrye.auto-inheritance=PARENT_ONLY
|
||||
mp.openapi.extensions.smallrye.operationIdStrategy=METHOD
|
||||
mp.openapi.extensions.smallrye.duplicateOperationIdBehavior=FAIL
|
||||
|
||||
# Disable Error messages from smallrye.openapi
|
||||
# related issue: https://github.com/keycloak/keycloak/issues/41871
|
||||
quarkus.log.category."io.smallrye.openapi.runtime.scanner.dataobject".level=off
|
||||
mp.openapi.scan.disable=true
|
||||
# 3.31.1 appears to have a hibernate bug
|
||||
quarkus.log.category."org.hibernate.orm.sql.exec".level=info
|
||||
# Enable shutdown delay by default as it is a built-time property, and timeout are then configured at runtime
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
<kc.home.dir>${kc.home.dir}</kc.home.dir>
|
||||
<kc.db>dev-file</kc.db>
|
||||
<java.util.concurrent.ForkJoinPool.common.threadFactory>io.quarkus.bootstrap.forkjoin.QuarkusForkJoinWorkerThreadFactory</java.util.concurrent.ForkJoinPool.common.threadFactory>
|
||||
<openapi.schema.target>${project.build.directory}</openapi.schema.target>
|
||||
</systemProperties>
|
||||
</configuration>
|
||||
<executions>
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<maven.compiler.release>17</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<!-- Use OpenAPI 4.x compatible plugin version -->
|
||||
<smallrye.openapi.generator.plugin.version>4.2.4</smallrye.openapi.generator.plugin.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -40,5 +42,86 @@
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-services</artifactId>
|
||||
</dependency>
|
||||
<!-- used by OAS Filter during OpenAPI spec generation -->
|
||||
<dependency>
|
||||
<groupId>io.smallrye</groupId>
|
||||
<artifactId>jandex</artifactId>
|
||||
<scope>provided</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>io.smallrye</groupId>
|
||||
<artifactId>smallrye-open-api-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<scanPackages>org.keycloak.representations.admin.v2,org.keycloak.admin.api</scanPackages>
|
||||
<filter>org.keycloak.admin.internal.openapi.OASModelFilter</filter>
|
||||
<outputDirectory>${project.build.directory}</outputDirectory>
|
||||
<outputFileTypeFilter>ALL</outputFileTypeFilter>
|
||||
<infoTitle>Keycloak API</infoTitle>
|
||||
<infoVersion>${project.version}</infoVersion>
|
||||
<systemPropertyVariables>
|
||||
<mp.openapi.extensions.smallrye.auto-inheritance>PARENT_ONLY</mp.openapi.extensions.smallrye.auto-inheritance>
|
||||
<mp.openapi.extensions.smallrye.operationIdStrategy>METHOD</mp.openapi.extensions.smallrye.operationIdStrategy>
|
||||
<mp.openapi.extensions.smallrye.duplicateOperationIdBehavior>FAIL</mp.openapi.extensions.smallrye.duplicateOperationIdBehavior>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>process-classes</phase>
|
||||
<goals>
|
||||
<goal>generate-schema</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-openapi-to-jar</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<!-- include generated openapi documents in JAR, so that Quarkus server can see and serve them -->
|
||||
<outputDirectory>${project.build.directory}/classes/META-INF</outputDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>${project.build.directory}</directory>
|
||||
<includes>
|
||||
<include>openapi.json</include>
|
||||
<include>openapi.yaml</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>copy-openapi-to-js-client</id>
|
||||
<phase>process-classes</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${maven.multiModuleProjectDirectory}/js/libs/keycloak-admin-client</outputDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>${project.build.directory}</directory>
|
||||
<includes>
|
||||
<include>openapi.yaml</include>
|
||||
</includes>
|
||||
<filtering>false</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
+62
-5
@@ -1,4 +1,5 @@
|
||||
package org.keycloak.quarkus.runtime.oas;
|
||||
package org.keycloak.admin.internal.openapi;
|
||||
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@@ -12,7 +13,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import io.quarkus.smallrye.openapi.OpenApiFilter;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.eclipse.microprofile.openapi.OASFactory;
|
||||
import org.eclipse.microprofile.openapi.OASFilter;
|
||||
import org.eclipse.microprofile.openapi.models.OpenAPI;
|
||||
@@ -22,20 +23,23 @@ import org.eclipse.microprofile.openapi.models.media.Schema;
|
||||
import org.jboss.jandex.AnnotationInstance;
|
||||
import org.jboss.jandex.AnnotationValue;
|
||||
import org.jboss.jandex.ClassInfo;
|
||||
import org.jboss.jandex.DotName;
|
||||
import org.jboss.jandex.IndexView;
|
||||
import org.jboss.jandex.MethodInfo;
|
||||
import org.jboss.jandex.Type;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
@OpenApiFilter(OpenApiFilter.RunStage.BUILD)
|
||||
import static org.keycloak.services.PatchTypeNames.JSON_MERGE;
|
||||
|
||||
public class OASModelFilter implements OASFilter {
|
||||
|
||||
private final IndexView index;
|
||||
private final Logger log = Logger.getLogger(OASModelFilter.class);
|
||||
private final Map<String, ClassInfo> simpleNameToClassInfoMap = new HashMap<>();
|
||||
|
||||
public static final String REF_PREFIX = "#/components/schemas/";
|
||||
private static final DotName JSON_NODE = DotName.createSimple(JsonNode.class);
|
||||
|
||||
public OASModelFilter(IndexView indexView) {
|
||||
this.index = indexView;
|
||||
log.debug("Index size: " + indexView.getKnownClasses().size());
|
||||
|
||||
indexView.getKnownClasses().forEach(classInfo -> {
|
||||
@@ -59,6 +63,8 @@ public class OASModelFilter implements OASFilter {
|
||||
|
||||
removeSchemaAndRefs(openAPI, "BaseRepresentation");
|
||||
|
||||
fixJsonMergePatchRequestObject(openAPI);
|
||||
|
||||
Map<String, Set<Schema>> discriminatorPropertiesToBeAdded = new HashMap<>();
|
||||
|
||||
// Follows https://swagger.io/docs/specification/v3_0/data-models/inheritance-and-polymorphism/
|
||||
@@ -113,6 +119,57 @@ public class OASModelFilter implements OASFilter {
|
||||
setter.accept(filtered.isEmpty() ? null : filtered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently, if endpoint consumes 'application/merge-patch+json' and the request object is 'JsonNode',
|
||||
* SmallRye OpenAPI generates array type schema for the endpoint.
|
||||
* See <a href="https://github.com/smallrye/smallrye-open-api/issues/2494">issue 2494</a> for more context.
|
||||
* What we need is either no schema, or an object with additional properties, so that the generated client
|
||||
* doesn't have empty body. This method removes schema.
|
||||
*/
|
||||
private void fixJsonMergePatchRequestObject(OpenAPI openAPI) {
|
||||
if (openAPI.getPaths() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
openAPI.getPaths().getPathItems().forEach((path, pathItem) -> {
|
||||
if (pathItem.getPATCH() != null && pathItem.getPATCH().getRequestBody() != null) {
|
||||
var patchOp = pathItem.getPATCH();
|
||||
var requestBody = patchOp.getRequestBody();
|
||||
|
||||
if (requestBody.getContent() != null && requestBody.getContent().getMediaType(JSON_MERGE) != null
|
||||
&& hasJsonNodeParameter(patchOp.getOperationId())) {
|
||||
var mediaTypeObject = requestBody.getContent().getMediaType(JSON_MERGE);
|
||||
mediaTypeObject.setSchema(null);
|
||||
log.debugf("Removed request body schema from PATCH path '%s' operation '%s' using content type '%s'", path, patchOp.getOperationId(), JSON_MERGE);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects REST interface method which name matches the operation name.
|
||||
*/
|
||||
private boolean hasJsonNodeParameter(String operationId) {
|
||||
if (operationId == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (ClassInfo classInfo : simpleNameToClassInfoMap.values()) {
|
||||
for (MethodInfo method : classInfo.methods()) {
|
||||
if (method.name().equals(operationId)) {
|
||||
for (Type paramType : method.parameterTypes()) {
|
||||
if (JSON_NODE.equals(paramType.name())) {
|
||||
log.debugf("Method '%s#%s' has parameter with type '%s'", classInfo.name(), method.name());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds discriminator and oneOf references to parent schemas that have Jackson @JsonTypeInfo
|
||||
* and @JsonSubTypes annotations. This enables OpenAPI generators to create proper class
|
||||
Reference in New Issue
Block a user