From 34ac303f0260475fcb7f004759c47e0a7205aff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Barto=C5=A1?= Date: Tue, 22 Jul 2025 15:48:48 +0200 Subject: [PATCH] [admin-api-v2] Provide simple validation with Jakarta/Hibernate Validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Martin Bartoš --- core/pom.xml | 4 ++ .../admin/v2/ClientRepresentation.java | 21 +++++--- .../admin/v2/validation/CreateClient.java | 5 ++ pom.xml | 17 +++++++ quarkus/deployment/pom.xml | 4 ++ .../quarkus/deployment/KeycloakProcessor.java | 2 + quarkus/runtime/pom.xml | 6 +++ .../keycloak/admin/api/client/ClientApi.java | 7 ++- .../keycloak/admin/api/client/ClientsApi.java | 7 ++- .../admin/api/client/DefaultClientsApi.java | 6 ++- .../jakarta/JakartaValidatorProvider.java | 9 ++++ .../JakartaValidatorProviderFactory.java | 6 +++ .../jakarta/JakartaValidatorSpi.java | 27 ++++++++++ .../services/org.keycloak.provider.Spi | 3 +- .../services/client/ClientService.java | 8 +-- services/pom.xml | 10 ++++ .../services/client/DefaultClientService.java | 14 +++++- .../services/error/KeycloakErrorHandler.java | 3 ++ .../jakarta/HibernateValidatorProvider.java | 21 ++++++++ .../HibernateValidatorProviderFactory.java | 40 +++++++++++++++ ...on.jakarta.JakartaValidatorProviderFactory | 1 + .../org/keycloak/tests/admin/AdminV2Test.java | 49 ++++++++++++++----- 22 files changed, 242 insertions(+), 28 deletions(-) create mode 100644 core/src/main/java/org/keycloak/representations/admin/v2/validation/CreateClient.java create mode 100644 server-spi-private/src/main/java/org/keycloak/validation/jakarta/JakartaValidatorProvider.java create mode 100644 server-spi-private/src/main/java/org/keycloak/validation/jakarta/JakartaValidatorProviderFactory.java create mode 100644 server-spi-private/src/main/java/org/keycloak/validation/jakarta/JakartaValidatorSpi.java create mode 100644 services/src/main/java/org/keycloak/validation/jakarta/HibernateValidatorProvider.java create mode 100644 services/src/main/java/org/keycloak/validation/jakarta/HibernateValidatorProviderFactory.java create mode 100644 services/src/main/resources/META-INF/services/org.keycloak.validation.jakarta.JakartaValidatorProviderFactory diff --git a/core/pom.xml b/core/pom.xml index 8363871a6732..c187bc8b46c9 100755 --- a/core/pom.xml +++ b/core/pom.xml @@ -68,6 +68,10 @@ org.eclipse.microprofile.openapi microprofile-openapi-api + + org.hibernate.validator + hibernate-validator + junit junit diff --git a/core/src/main/java/org/keycloak/representations/admin/v2/ClientRepresentation.java b/core/src/main/java/org/keycloak/representations/admin/v2/ClientRepresentation.java index 4d4aba00ffd4..f2ee3cd63abb 100644 --- a/core/src/main/java/org/keycloak/representations/admin/v2/ClientRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/admin/v2/ClientRepresentation.java @@ -3,8 +3,11 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import org.hibernate.validator.constraints.URL; +import org.keycloak.representations.admin.v2.validation.CreateClient; import java.util.LinkedHashSet; import java.util.Set; @@ -13,6 +16,7 @@ public class ClientRepresentation extends BaseRepresentation { public static final String OIDC = "openid-connect"; + @NotBlank(groups = CreateClient.class) @JsonPropertyDescription("ID uniquely identifying this client") private String clientId; @@ -29,28 +33,31 @@ public class ClientRepresentation extends BaseRepresentation { @JsonPropertyDescription("Whether this client is enabled") private Boolean enabled; + @URL @JsonPropertyDescription("URL to the application's homepage that is represented by this client") private String appUrl; @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonPropertyDescription("URLs that the browser can redirect to after login") - private Set appRedirectUrls = new LinkedHashSet(); + private Set<@NotBlank @URL(message = "Each redirect URL must be valid") String> appRedirectUrls = new LinkedHashSet(); @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonPropertyDescription("Login flows that are enabled for this client") - private Set loginFlows = new LinkedHashSet(); + private Set<@NotBlank String> loginFlows = new LinkedHashSet(); + @Valid @JsonPropertyDescription("Authentication configuration for this client") private Auth auth; @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonPropertyDescription("Web origins that are allowed to make requests to this client") - private Set webOrigins = new LinkedHashSet(); + private Set<@NotBlank String> webOrigins = new LinkedHashSet(); @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonPropertyDescription("Roles associated with this client") - private Set roles = new LinkedHashSet(); + private Set<@NotBlank String> roles = new LinkedHashSet(); + @Valid @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonPropertyDescription("Service account configuration for this client") private ServiceAccount serviceAccount; @@ -160,6 +167,7 @@ public void setServiceAccount(ServiceAccount serviceAccount) { @JsonInclude(JsonInclude.Include.NON_ABSENT) public static class Auth { + @NotNull @JsonPropertyDescription("Whether authentication is enabled for this client") private Boolean enabled; @@ -208,6 +216,7 @@ public void setCertificate(String certificate) { @JsonInclude(JsonInclude.Include.NON_ABSENT) public static class ServiceAccount { + @NotNull @JsonPropertyDescription("Whether the service account is enabled") private Boolean enabled; diff --git a/core/src/main/java/org/keycloak/representations/admin/v2/validation/CreateClient.java b/core/src/main/java/org/keycloak/representations/admin/v2/validation/CreateClient.java new file mode 100644 index 000000000000..28f9333de6f2 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/admin/v2/validation/CreateClient.java @@ -0,0 +1,5 @@ +package org.keycloak.representations.admin.v2.validation; + +// Jakarta Validation Group - validation is done only when creating a client +public interface CreateClient { +} diff --git a/pom.xml b/pom.xml index 5dc607705a6c..e419b0bf59a6 100644 --- a/pom.xml +++ b/pom.xml @@ -90,6 +90,8 @@ 2.3.230 6.2.13.Final 6.2.13.Final + 9.0.1.Final + 6.0.0 15.0.18.Final 5.0.14.Final ${protostream.version} @@ -579,6 +581,21 @@ h2 ${h2.version} + + org.hibernate.validator + hibernate-validator + ${hibernate-validator.version} + + + org.hibernate.validator + hibernate-validator-cdi + ${hibernate-validator.version} + + + org.glassfish.expressly + expressly + ${expressly.version} + org.hibernate.orm hibernate-c3p0 diff --git a/quarkus/deployment/pom.xml b/quarkus/deployment/pom.xml index 981102203d60..8b51d30587a7 100644 --- a/quarkus/deployment/pom.xml +++ b/quarkus/deployment/pom.xml @@ -136,6 +136,10 @@ io.quarkus quarkus-rest-jackson-deployment + + io.quarkus + quarkus-hibernate-validator-deployment + io.quarkus quarkus-hibernate-orm-deployment diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java index d1750eabdb86..52afba0ced52 100644 --- a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java +++ b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java @@ -134,6 +134,7 @@ import org.keycloak.userprofile.config.UPConfigUtils; import org.keycloak.util.JsonSerialization; import org.keycloak.utils.StringUtil; +import org.keycloak.validation.jakarta.HibernateValidatorProviderFactory; import org.keycloak.vault.FilesKeystoreVaultProviderFactory; import org.keycloak.vault.FilesPlainTextVaultProviderFactory; @@ -189,6 +190,7 @@ class KeycloakProcessor { JBossJtaTransactionManagerLookup.class, DefaultJpaConnectionProviderFactory.class, DefaultLiquibaseConnectionProvider.class, + //HibernateValidatorProviderFactory.class, FolderThemeProviderFactory.class, LiquibaseJpaUpdaterProviderFactory.class, FilesKeystoreVaultProviderFactory.class, diff --git a/quarkus/runtime/pom.xml b/quarkus/runtime/pom.xml index dcb248e49e4c..0c6452d51afe 100644 --- a/quarkus/runtime/pom.xml +++ b/quarkus/runtime/pom.xml @@ -136,6 +136,12 @@ mapstruct + + + io.quarkus + quarkus-hibernate-validator + + io.smallrye.config diff --git a/rest/admin-api/src/main/java/org/keycloak/admin/api/client/ClientApi.java b/rest/admin-api/src/main/java/org/keycloak/admin/api/client/ClientApi.java index bfefe2591963..aca21bf6eda2 100644 --- a/rest/admin-api/src/main/java/org/keycloak/admin/api/client/ClientApi.java +++ b/rest/admin-api/src/main/java/org/keycloak/admin/api/client/ClientApi.java @@ -1,11 +1,13 @@ package org.keycloak.admin.api.client; +import jakarta.validation.Valid; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.PATCH; import jakarta.ws.rs.PUT; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import org.keycloak.admin.api.FieldValidation; @@ -25,11 +27,12 @@ public interface ClientApi { @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - ClientRepresentation createOrUpdateClient(ClientRepresentation client, @PathParam("fieldValidation") FieldValidation fieldValidation); + ClientRepresentation createOrUpdateClient(@Valid ClientRepresentation client, + @QueryParam("fieldValidation") FieldValidation fieldValidation); @PATCH @Consumes({MediaType.APPLICATION_JSON_PATCH_JSON, CONENT_TYPE_MERGE_PATCH}) @Produces(MediaType.APPLICATION_JSON) - ClientRepresentation patchClient(JsonNode patch, @PathParam("fieldValidation") FieldValidation fieldValidation); + ClientRepresentation patchClient(JsonNode patch, @QueryParam("fieldValidation") FieldValidation fieldValidation); } diff --git a/rest/admin-api/src/main/java/org/keycloak/admin/api/client/ClientsApi.java b/rest/admin-api/src/main/java/org/keycloak/admin/api/client/ClientsApi.java index c8c49f83ce79..ea5d4214a050 100644 --- a/rest/admin-api/src/main/java/org/keycloak/admin/api/client/ClientsApi.java +++ b/rest/admin-api/src/main/java/org/keycloak/admin/api/client/ClientsApi.java @@ -2,6 +2,9 @@ import java.util.stream.Stream; +import jakarta.validation.Valid; +import jakarta.validation.groups.ConvertGroup; +import jakarta.ws.rs.QueryParam; import org.keycloak.admin.api.FieldValidation; import org.keycloak.provider.Provider; import org.keycloak.representations.admin.v2.ClientRepresentation; @@ -13,6 +16,7 @@ import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; +import org.keycloak.representations.admin.v2.validation.CreateClient; public interface ClientsApi extends Provider { @@ -24,7 +28,8 @@ public interface ClientsApi extends Provider { @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - ClientRepresentation createClient(ClientRepresentation client, @PathParam("fieldValidation") FieldValidation fieldValidation); + ClientRepresentation createClient(@Valid @ConvertGroup(to = CreateClient.class) ClientRepresentation client, + @QueryParam("fieldValidation") FieldValidation fieldValidation); @Path("{id}") ClientApi client(@PathParam("id") String id); diff --git a/rest/admin-api/src/main/java/org/keycloak/admin/api/client/DefaultClientsApi.java b/rest/admin-api/src/main/java/org/keycloak/admin/api/client/DefaultClientsApi.java index 18deae8c4e08..bafb9c36c5dd 100644 --- a/rest/admin-api/src/main/java/org/keycloak/admin/api/client/DefaultClientsApi.java +++ b/rest/admin-api/src/main/java/org/keycloak/admin/api/client/DefaultClientsApi.java @@ -2,11 +2,14 @@ import java.util.stream.Stream; +import jakarta.validation.Valid; +import jakarta.validation.groups.ConvertGroup; import org.keycloak.admin.api.FieldValidation; import org.keycloak.http.HttpResponse; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.representations.admin.v2.ClientRepresentation; +import org.keycloak.representations.admin.v2.validation.CreateClient; import org.keycloak.services.ServiceException; import org.keycloak.services.client.ClientService; @@ -35,7 +38,8 @@ public Stream getClients() { } @Override - public ClientRepresentation createClient(ClientRepresentation client, FieldValidation fieldValidation) { + public ClientRepresentation createClient(@Valid @ConvertGroup(to = CreateClient.class) ClientRepresentation client, + FieldValidation fieldValidation) { try { response.setStatus(Response.Status.CREATED.getStatusCode()); return clientService.createOrUpdate(realm, client, false).representation(); diff --git a/server-spi-private/src/main/java/org/keycloak/validation/jakarta/JakartaValidatorProvider.java b/server-spi-private/src/main/java/org/keycloak/validation/jakarta/JakartaValidatorProvider.java new file mode 100644 index 000000000000..792b9b2d463e --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/validation/jakarta/JakartaValidatorProvider.java @@ -0,0 +1,9 @@ +package org.keycloak.validation.jakarta; + +import jakarta.validation.Validator; +import org.keycloak.provider.Provider; + +public interface JakartaValidatorProvider extends Provider { + + Validator getValidator(); +} diff --git a/server-spi-private/src/main/java/org/keycloak/validation/jakarta/JakartaValidatorProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/validation/jakarta/JakartaValidatorProviderFactory.java new file mode 100644 index 000000000000..3c2365ea9dde --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/validation/jakarta/JakartaValidatorProviderFactory.java @@ -0,0 +1,6 @@ +package org.keycloak.validation.jakarta; + +import org.keycloak.provider.ProviderFactory; + +public interface JakartaValidatorProviderFactory extends ProviderFactory { +} diff --git a/server-spi-private/src/main/java/org/keycloak/validation/jakarta/JakartaValidatorSpi.java b/server-spi-private/src/main/java/org/keycloak/validation/jakarta/JakartaValidatorSpi.java new file mode 100644 index 000000000000..3d56fc1731b2 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/validation/jakarta/JakartaValidatorSpi.java @@ -0,0 +1,27 @@ +package org.keycloak.validation.jakarta; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +public class JakartaValidatorSpi implements Spi { + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return "jakarta-validator"; + } + + @Override + public Class getProviderClass() { + return JakartaValidatorProvider.class; + } + + @Override + public Class> getProviderFactoryClass() { + return JakartaValidatorProviderFactory.class; + } +} diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi index a1df9eea3e03..a2b0769cd303 100755 --- a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -107,4 +107,5 @@ org.keycloak.securityprofile.SecurityProfileSpi org.keycloak.logging.MappedDiagnosticContextSpi org.keycloak.services.KeycloakServicesSpi org.keycloak.services.client.ClientServiceSpi -org.keycloak.models.mapper.ModelMapperSpi \ No newline at end of file +org.keycloak.models.mapper.ModelMapperSpi +org.keycloak.validation.jakarta.JakartaValidatorSpi \ No newline at end of file diff --git a/server-spi/src/main/java/org/keycloak/services/client/ClientService.java b/server-spi/src/main/java/org/keycloak/services/client/ClientService.java index 18a28b763557..df90ee0301ae 100644 --- a/server-spi/src/main/java/org/keycloak/services/client/ClientService.java +++ b/server-spi/src/main/java/org/keycloak/services/client/ClientService.java @@ -10,22 +10,22 @@ public interface ClientService extends Service { - public static class ClientSearchOptions { + class ClientSearchOptions { // TODO } - public static class ClientProjectionOptions { + class ClientProjectionOptions { // TODO } - public static class ClientSortAndSliceOptions { + class ClientSortAndSliceOptions { // order by // offset // limit // NOTE: this is not always the most desirable way to do pagination } - public record CreateOrUpdateResult(ClientRepresentation representation, boolean created) {} + record CreateOrUpdateResult(ClientRepresentation representation, boolean created) {} Optional getClient(RealmModel realm, String clientId, ClientProjectionOptions projectionOptions); diff --git a/services/pom.xml b/services/pom.xml index ac78b085baef..6c3867f98503 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -86,6 +86,16 @@ mapstruct ${org.mapstruct.version} + + org.hibernate.validator + hibernate-validator-cdi + ${hibernate-validator.version} + + + org.glassfish.expressly + expressly + ${expressly.version} + org.jboss.logging jboss-logging diff --git a/services/src/main/java/org/keycloak/services/client/DefaultClientService.java b/services/src/main/java/org/keycloak/services/client/DefaultClientService.java index 6bc24c61c898..d6ccbff30b1c 100644 --- a/services/src/main/java/org/keycloak/services/client/DefaultClientService.java +++ b/services/src/main/java/org/keycloak/services/client/DefaultClientService.java @@ -1,14 +1,23 @@ package org.keycloak.services.client; +import jakarta.enterprise.inject.spi.CDI; +import jakarta.inject.Inject; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; import jakarta.ws.rs.core.Response; - +import org.hibernate.validator.HibernateValidator; +import org.hibernate.validator.HibernateValidatorFactory; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.mapper.ClientModelMapper; import org.keycloak.models.mapper.ModelMapper; import org.keycloak.representations.admin.v2.ClientRepresentation; +import org.keycloak.representations.admin.v2.validation.CreateClient; import org.keycloak.services.ServiceException; +import org.keycloak.validation.jakarta.HibernateValidatorProvider; +import org.keycloak.validation.jakarta.JakartaValidatorProvider; import java.util.Optional; import java.util.stream.Stream; @@ -17,10 +26,12 @@ public class DefaultClientService implements ClientService { private final KeycloakSession session; private final ClientModelMapper mapper; + private final Validator validator; public DefaultClientService(KeycloakSession session) { this.session = session; this.mapper = session.getProvider(ModelMapper.class).clients(); + this.validator = session.getProvider(JakartaValidatorProvider.class).getValidator(); } @Override @@ -45,6 +56,7 @@ public CreateOrUpdateResult createOrUpdate(RealmModel realm, ClientRepresentatio throw new ServiceException("Client already exists", Response.Status.CONFLICT); } } else { + validator.validate(client, CreateClient.class); // TODO improve it to avoid second validation when we know it is create and not update model = realm.addClient(client.getClientId()); created = true; } diff --git a/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java b/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java index 4f5decdb18fd..abe70519237f 100644 --- a/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java +++ b/services/src/main/java/org/keycloak/services/error/KeycloakErrorHandler.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.core.JsonProcessingException; +import jakarta.validation.ValidationException; import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.OAuthErrorException; @@ -100,6 +101,8 @@ public static Response getResponse(KeycloakSession session, Throwable throwable) error.setErrorDescription("Cannot parse the JSON"); } else if (isServerError) { error.setErrorDescription("For more on this error consult the server log."); + } else if (throwable instanceof ValidationException) { + error.setErrorDescription(throwable.getMessage()); } return Response.status(responseStatus) diff --git a/services/src/main/java/org/keycloak/validation/jakarta/HibernateValidatorProvider.java b/services/src/main/java/org/keycloak/validation/jakarta/HibernateValidatorProvider.java new file mode 100644 index 000000000000..e7150d0cb6cb --- /dev/null +++ b/services/src/main/java/org/keycloak/validation/jakarta/HibernateValidatorProvider.java @@ -0,0 +1,21 @@ +package org.keycloak.validation.jakarta; + +import jakarta.validation.Validator; + +public class HibernateValidatorProvider implements JakartaValidatorProvider { + private final Validator validator; + + public HibernateValidatorProvider(Validator validator) { + this.validator = validator; + } + + @Override + public Validator getValidator() { + return validator; + } + + @Override + public void close() { + + } +} diff --git a/services/src/main/java/org/keycloak/validation/jakarta/HibernateValidatorProviderFactory.java b/services/src/main/java/org/keycloak/validation/jakarta/HibernateValidatorProviderFactory.java new file mode 100644 index 000000000000..e50f9c5dbba8 --- /dev/null +++ b/services/src/main/java/org/keycloak/validation/jakarta/HibernateValidatorProviderFactory.java @@ -0,0 +1,40 @@ +package org.keycloak.validation.jakarta; + +import jakarta.enterprise.inject.spi.CDI; +import jakarta.validation.Validator; +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +public class HibernateValidatorProviderFactory implements JakartaValidatorProviderFactory { + public static final String PROVIDER_ID = "default"; + private static HibernateValidatorProvider SINGLETON; + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public JakartaValidatorProvider create(KeycloakSession session) { + if (SINGLETON == null) { + SINGLETON = new HibernateValidatorProvider(CDI.current().select(Validator.class).get()); + } + return SINGLETON; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } +} diff --git a/services/src/main/resources/META-INF/services/org.keycloak.validation.jakarta.JakartaValidatorProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.validation.jakarta.JakartaValidatorProviderFactory new file mode 100644 index 000000000000..c40b9ff7d964 --- /dev/null +++ b/services/src/main/resources/META-INF/services/org.keycloak.validation.jakarta.JakartaValidatorProviderFactory @@ -0,0 +1 @@ +org.keycloak.validation.jakarta.HibernateValidatorProviderFactory \ No newline at end of file diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/AdminV2Test.java b/tests/base/src/test/java/org/keycloak/tests/admin/AdminV2Test.java index 91547c4ea71b..5f4fbf7c1b8b 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/AdminV2Test.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/AdminV2Test.java @@ -17,14 +17,17 @@ package org.keycloak.tests.admin; -import static org.junit.jupiter.api.Assertions.assertEquals; - +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPatch; +import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.util.EntityUtils; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.keycloak.admin.api.client.ClientApi; @@ -32,31 +35,38 @@ import org.keycloak.testframework.annotations.InjectHttpClient; import org.keycloak.testframework.annotations.KeycloakIntegrationTest; -import com.fasterxml.jackson.databind.ObjectMapper; +import java.nio.charset.StandardCharsets; -import jakarta.ws.rs.core.HttpHeaders; -import jakarta.ws.rs.core.MediaType; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; @KeycloakIntegrationTest() public class AdminV2Test { private static final String HOSTNAME_LOCAL_ADMIN = "http://localhost:8080/admin/api/v2"; + private static ObjectMapper mapper; @InjectHttpClient private HttpClient client; + @BeforeAll + public static void setupMapper() { + mapper = new ObjectMapper(); + } + @Test - public void testGetClient() throws Exception { + public void getClient() throws Exception { HttpGet request = new HttpGet(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/account"); HttpResponse response = client.execute(request); assertEquals(200, response.getStatusLine().getStatusCode()); - ObjectMapper mapper = new ObjectMapper(); ClientRepresentation client = mapper.createParser(response.getEntity().getContent()).readValueAs(ClientRepresentation.class); assertEquals("account", client.getClientId()); } @Test - public void testJsonPatchClient() throws Exception { + public void jsonPatchClient() throws Exception { HttpPatch request = new HttpPatch(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/account"); request.setEntity(new StringEntity("not json")); request.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_PATCH_JSON); @@ -72,22 +82,19 @@ public void testJsonPatchClient() throws Exception { response = client.execute(request); assertEquals(200, response.getStatusLine().getStatusCode()); - ObjectMapper mapper = new ObjectMapper(); ClientRepresentation client = mapper.createParser(response.getEntity().getContent()).readValueAs(ClientRepresentation.class); assertEquals("I'm a description", client.getDescription()); } @Disabled @Test - public void testJsonMergePatchClient() throws Exception { + public void jsonMergePatchClient() throws Exception { HttpPatch request = new HttpPatch(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/account"); request.setHeader(HttpHeaders.CONTENT_TYPE, ClientApi.CONENT_TYPE_MERGE_PATCH); ClientRepresentation patch = new ClientRepresentation(); patch.setDescription("I'm also a description"); - ObjectMapper mapper = new ObjectMapper(); - request.setEntity(new StringEntity(mapper.writeValueAsString(patch))); HttpResponse response = client.execute(request); @@ -97,4 +104,22 @@ public void testJsonMergePatchClient() throws Exception { assertEquals("I'm also a description", client.getDescription()); } + @Test + public void clientRepresentationValidation() throws Exception { + HttpPost request = new HttpPost(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients"); + request.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); + + request.setEntity(new StringEntity(""" + { + "displayName": "something", + "appUrl": "notUrl" + } + """)); + + var response = client.execute(request); + assertThat(response, notNullValue()); + System.err.println(EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)); + assertThat(response.getStatusLine().getStatusCode(), is(400)); + } + }