diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml index 11c8f780374f3..8aa72a7d053d6 100644 --- a/bom/runtime/pom.xml +++ b/bom/runtime/pom.xml @@ -389,6 +389,11 @@ quarkus-keycloak-authorization ${project.version} + + io.quarkus + quarkus-keycloak-admin-client + ${project.version} + io.quarkus quarkus-flyway @@ -2884,6 +2889,17 @@ keycloak-core ${keycloak.version} + + org.keycloak + keycloak-admin-client + ${keycloak.version} + + + org.checkerframework + checker-qual + + + org.keycloak keycloak-adapter-spi diff --git a/docs/src/main/asciidoc/security-openid-connect.adoc b/docs/src/main/asciidoc/security-openid-connect.adoc index 420e6753ed5a3..d1f6d3927d712 100644 --- a/docs/src/main/asciidoc/security-openid-connect.adoc +++ b/docs/src/main/asciidoc/security-openid-connect.adoc @@ -174,6 +174,9 @@ Log in as the `admin` user to access the Keycloak Administration Console. Userna Import the {quickstarts-tree-url}/security-openid-connect-quickstart/config/quarkus-realm.json[realm configuration file] to create a new realm. For more details, see the Keycloak documentation about how to https://www.keycloak.org/docs/latest/server_admin/index.html#_create-realm[create a new realm]. +NOTE: If you want to use the Keycloak Admin Client to configure your server from your application you need to include the +`quarkus-keycloak-admin-client` extension. + == Running and Using the Application === Running in Developer Mode diff --git a/extensions/keycloak-admin-client/deployment/pom.xml b/extensions/keycloak-admin-client/deployment/pom.xml new file mode 100644 index 0000000000000..e13a93ba4c636 --- /dev/null +++ b/extensions/keycloak-admin-client/deployment/pom.xml @@ -0,0 +1,59 @@ + + + + quarkus-keycloak-admin-client-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-keycloak-admin-client-deployment + Quarkus - Keycloak Admin Client - Deployment + + + + io.quarkus + quarkus-keycloak-admin-client + + + io.quarkus + quarkus-rest-client-deployment + + + io.quarkus + quarkus-jackson-deployment + + + + io.quarkus + quarkus-junit5-internal + test + + + io.rest-assured + rest-assured + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + diff --git a/extensions/keycloak-admin-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java b/extensions/keycloak-admin-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java new file mode 100644 index 0000000000000..099861e898ea0 --- /dev/null +++ b/extensions/keycloak-admin-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java @@ -0,0 +1,37 @@ +package io.quarkus.keycloak.adminclient.deployment; + +import org.jboss.jandex.DotName; +import org.jboss.resteasy.client.jaxrs.internal.ResteasyClientBuilderImpl; +import org.jboss.resteasy.client.jaxrs.internal.proxy.ProxyBuilderImpl; +import org.keycloak.admin.client.JacksonProvider; +import org.keycloak.common.util.MultivaluedHashMap; +import org.keycloak.json.StringListMapDeserializer; +import org.keycloak.json.StringOrArrayDeserializer; +import org.keycloak.json.StringOrArraySerializer; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.AdditionalApplicationArchiveMarkerBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyIgnoreWarningBuildItem; + +public class KeycloakAdminClientProcessor { + + @BuildStep + ReflectiveHierarchyIgnoreWarningBuildItem marker(BuildProducer prod) { + prod.produce(new AdditionalApplicationArchiveMarkerBuildItem("org/keycloak/admin/client/")); + prod.produce(new AdditionalApplicationArchiveMarkerBuildItem("org/keycloak/representations")); + return new ReflectiveHierarchyIgnoreWarningBuildItem(DotName.createSimple(MultivaluedHashMap.class.getName())); + } + + @BuildStep + ReflectiveClassBuildItem reflect() { + return ReflectiveClassBuildItem.builder(ResteasyClientBuilderImpl.class, JacksonProvider.class, ProxyBuilderImpl.class, + StringListMapDeserializer.class, + StringOrArrayDeserializer.class, + StringOrArraySerializer.class) + .constructors(true) + .methods(true) + .build(); + } +} diff --git a/extensions/keycloak-admin-client/pom.xml b/extensions/keycloak-admin-client/pom.xml new file mode 100644 index 0000000000000..9a50c88c021b8 --- /dev/null +++ b/extensions/keycloak-admin-client/pom.xml @@ -0,0 +1,20 @@ + + + + quarkus-build-parent + io.quarkus + 999-SNAPSHOT + ../../build-parent/pom.xml + + 4.0.0 + + quarkus-keycloak-admin-client-parent + Quarkus - Keycloak Admin Client + pom + + deployment + runtime + + diff --git a/extensions/keycloak-admin-client/runtime/pom.xml b/extensions/keycloak-admin-client/runtime/pom.xml new file mode 100644 index 0000000000000..fc18926b83f68 --- /dev/null +++ b/extensions/keycloak-admin-client/runtime/pom.xml @@ -0,0 +1,93 @@ + + + + quarkus-keycloak-admin-client-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-keycloak-admin-client + Quarkus - Keycloak Admin Client - Runtime + Administer a Keycloak Instance + + + + io.quarkus + quarkus-oidc + + + io.quarkus + quarkus-rest-client + + + org.keycloak + keycloak-adapter-core + + + org.keycloak + keycloak-core + + + org.keycloak + keycloak-admin-client + + + io.quarkus + quarkus-jackson + + + org.apache.httpcomponents + httpasyncclient + + + commons-logging + commons-logging + + + + + org.keycloak + keycloak-authz-client + + + commons-logging + commons-logging + + + + + org.jboss.logging + commons-logging-jboss-logging + + + junit + junit + test + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/keycloak-admin-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/keycloak-admin-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..1d7184fc22ffb --- /dev/null +++ b/extensions/keycloak-admin-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,11 @@ +--- +name: "Keycloak Admin Client" +metadata: + keywords: + - "oauth2" + - "openid-connect" + - "keycloak" + - "authorization-services" + categories: + - "security" + status: "stable" diff --git a/extensions/pom.xml b/extensions/pom.xml index f7395ba709218..9e4763a2500a7 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -114,6 +114,7 @@ smallrye-jwt oidc keycloak-authorization + keycloak-admin-client infinispan-client diff --git a/integration-tests/keycloak-authorization/pom.xml b/integration-tests/keycloak-authorization/pom.xml index fe05d488acaef..7bfc0e0fa163d 100644 --- a/integration-tests/keycloak-authorization/pom.xml +++ b/integration-tests/keycloak-authorization/pom.xml @@ -23,6 +23,10 @@ io.quarkus quarkus-keycloak-authorization + + io.quarkus + quarkus-keycloak-admin-client + io.quarkus quarkus-resteasy-jsonb diff --git a/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/KeycloakTestResource.java b/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/KeycloakTestResource.java index 262547191937e..ae6d6687636d0 100644 --- a/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/KeycloakTestResource.java +++ b/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/KeycloakTestResource.java @@ -1,6 +1,5 @@ package io.quarkus.it.keycloak; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -9,7 +8,8 @@ import java.util.List; import java.util.Map; -import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.KeycloakBuilder; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.RealmRepresentation; @@ -19,16 +19,16 @@ import org.keycloak.representations.idm.authorization.PolicyRepresentation; import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.keycloak.representations.idm.authorization.ResourceServerRepresentation; -import org.keycloak.util.JsonSerialization; import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; -import io.restassured.RestAssured; public class KeycloakTestResource implements QuarkusTestResourceLifecycleManager { private static final String KEYCLOAK_SERVER_URL = System.getProperty("keycloak.url", "http://localhost:8180/auth"); private static final String KEYCLOAK_REALM = "quarkus"; + private Keycloak keycloak; + @Override public Map start() { @@ -39,38 +39,16 @@ public Map start() { realm.getUsers().add(createUser("admin", "user", "admin")); realm.getUsers().add(createUser("jdoe", "user", "confidential")); - try { - RestAssured - .given() - .auth().oauth2(getAdminAccessToken()) - .contentType("application/json") - .body(JsonSerialization.writeValueAsBytes(realm)) - .when() - .post(KEYCLOAK_SERVER_URL + "/admin/realms").then() - .statusCode(201); - } catch (IOException e) { - throw new RuntimeException(e); - } - - HashMap map = new HashMap<>(); - - // a workaround to set system properties defined when executing tests. Looks like this commit introduced an - // unexpected behavior: 3ca0b323dd1c6d80edb66136eb42be7f9bde3310 - map.put("keycloak.url", System.getProperty("keycloak.url")); - - return map; - } + keycloak = KeycloakBuilder.builder() + .serverUrl(KEYCLOAK_SERVER_URL) + .realm("master") + .clientId("admin-cli") + .username("admin") + .password("admin") + .build(); + keycloak.realms().create(realm); - private static String getAdminAccessToken() { - return RestAssured - .given() - .param("grant_type", "password") - .param("username", "admin") - .param("password", "admin") - .param("client_id", "admin-cli") - .when() - .post(KEYCLOAK_SERVER_URL + "/realms/master/protocol/openid-connect/token") - .as(AccessTokenResponse.class).getToken(); + return Collections.emptyMap(); } private static RealmRepresentation createRealm(String name) { @@ -234,11 +212,7 @@ private static UserRepresentation createUser(String username, String... realmRol @Override public void stop() { - RestAssured - .given() - .auth().oauth2(getAdminAccessToken()) - .when() - .delete(KEYCLOAK_SERVER_URL + "/admin/realms/" + KEYCLOAK_REALM).then().statusCode(204); + keycloak.realm(KEYCLOAK_REALM).remove(); } } diff --git a/integration-tests/oidc-code-flow/pom.xml b/integration-tests/oidc-code-flow/pom.xml index f3867b720ee6a..ddbd3c11ef8a6 100644 --- a/integration-tests/oidc-code-flow/pom.xml +++ b/integration-tests/oidc-code-flow/pom.xml @@ -23,6 +23,10 @@ io.quarkus quarkus-oidc + + io.quarkus + quarkus-keycloak-admin-client + io.quarkus quarkus-resteasy-jackson @@ -42,7 +46,6 @@ org.keycloak keycloak-core - test io.rest-assured diff --git a/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/AdminClientResource.java b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/AdminClientResource.java new file mode 100644 index 0000000000000..f6690d0a2bb9f --- /dev/null +++ b/integration-tests/oidc-code-flow/src/main/java/io/quarkus/it/keycloak/AdminClientResource.java @@ -0,0 +1,33 @@ +package io.quarkus.it.keycloak; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.KeycloakBuilder; + +@Path("/admin-client") +public class AdminClientResource { + + private static final Logger LOG = Logger.getLogger(AdminClientResource.class); + + @ConfigProperty(name = "admin-url") + String url; + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + LOG.info("Hello invoked"); + + Keycloak keycloak = KeycloakBuilder.builder().serverUrl(url) + .realm("master") + .clientId("admin-cli") + .username("admin") + .password("admin").build(); + return keycloak.realm("quarkus").toRepresentation().getRealm(); + } +} diff --git a/integration-tests/oidc-code-flow/src/main/resources/application.properties b/integration-tests/oidc-code-flow/src/main/resources/application.properties index d7a58cc8d5435..05b6e013dc98c 100644 --- a/integration-tests/oidc-code-flow/src/main/resources/application.properties +++ b/integration-tests/oidc-code-flow/src/main/resources/application.properties @@ -1,6 +1,7 @@ # Configuration file # Default tenant configuration +admin-url=${keycloak.url} quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus quarkus.oidc.client-id=quarkus-app quarkus.oidc.credentials.secret=secret diff --git a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/AdminClientTestCase.java b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/AdminClientTestCase.java new file mode 100644 index 0000000000000..9d04d016d0ae0 --- /dev/null +++ b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/AdminClientTestCase.java @@ -0,0 +1,22 @@ +package io.quarkus.it.keycloak; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class AdminClientTestCase { + + @Test + public void testHelloEndpoint() { + given() + .when().get("/admin-client") + .then() + .statusCode(200) + .body(is("quarkus")); + } + +} \ No newline at end of file diff --git a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java index 9ff0201d96acc..a97400f585e7c 100644 --- a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java +++ b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/KeycloakRealmResourceManager.java @@ -1,65 +1,49 @@ package io.quarkus.it.keycloak; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; -import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.KeycloakBuilder; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RolesRepresentation; import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.util.JsonSerialization; import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; -import io.restassured.RestAssured; public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycleManager { private static final String KEYCLOAK_SERVER_URL = System.getProperty("keycloak.url", "http://localhost:8180/auth"); private static final String KEYCLOAK_REALM = "quarkus"; + private Keycloak keycloak; + @Override public Map start() { - try { - - RealmRepresentation realm = createRealm(KEYCLOAK_REALM); - - realm.getClients().add(createClient("quarkus-app")); - realm.getUsers().add(createUser("alice", "user")); - realm.getUsers().add(createUser("admin", "user", "admin")); - realm.getUsers().add(createUser("jdoe", "user", "confidential")); - - RestAssured - .given() - .auth().oauth2(getAdminAccessToken()) - .contentType("application/json") - .body(JsonSerialization.writeValueAsBytes(realm)) - .when() - .post(KEYCLOAK_SERVER_URL + "/admin/realms").then() - .statusCode(201); - } catch (IOException e) { - throw new RuntimeException(e); - } - return Collections.emptyMap(); - } + RealmRepresentation realm = createRealm(KEYCLOAK_REALM); + + realm.getClients().add(createClient("quarkus-app")); + realm.getUsers().add(createUser("alice", "user")); + realm.getUsers().add(createUser("admin", "user", "admin")); + realm.getUsers().add(createUser("jdoe", "user", "confidential")); - private static String getAdminAccessToken() { - return RestAssured - .given() - .param("grant_type", "password") - .param("username", "admin") - .param("password", "admin") - .param("client_id", "admin-cli") - .when() - .post(KEYCLOAK_SERVER_URL + "/realms/master/protocol/openid-connect/token") - .as(AccessTokenResponse.class).getToken(); + keycloak = KeycloakBuilder.builder() + .serverUrl(KEYCLOAK_SERVER_URL) + .realm("master") + .clientId("admin-cli") + .username("admin") + .password("admin") + .build(); + keycloak.realms().create(realm); + + return Collections.emptyMap(); } private static RealmRepresentation createRealm(String name) { @@ -118,11 +102,6 @@ private static UserRepresentation createUser(String username, String... realmRol @Override public void stop() { - - RestAssured - .given() - .auth().oauth2(getAdminAccessToken()) - .when() - .delete(KEYCLOAK_SERVER_URL + "/admin/realms/" + KEYCLOAK_REALM).thenReturn().prettyPrint(); + keycloak.realm(KEYCLOAK_REALM).remove(); } } diff --git a/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/NativeAdminClientITCase.java b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/NativeAdminClientITCase.java new file mode 100644 index 0000000000000..7a6eb73b22666 --- /dev/null +++ b/integration-tests/oidc-code-flow/src/test/java/io/quarkus/it/keycloak/NativeAdminClientITCase.java @@ -0,0 +1,9 @@ +package io.quarkus.it.keycloak; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +public class NativeAdminClientITCase extends AdminClientTestCase { + + // Execute the same tests but in native mode. +} \ No newline at end of file