Skip to content

Commit

Permalink
Merge pull request quarkusio#44389 from michalvavrik/feature/kc-dev-s…
Browse files Browse the repository at this point in the history
…vc-for-kc-admin-client

Start Keycloak Dev Services for standalone Keycloak Admin REST/RESTEasy clients
  • Loading branch information
sberyozkin authored Nov 9, 2024
2 parents 3321050 + f983e20 commit ee2100b
Show file tree
Hide file tree
Showing 15 changed files with 239 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

public interface KeycloakDevServicesConfigurator {

record ConfigPropertiesContext(String authServerInternalUrl, String oidcClientId, String oidcClientSecret) {
record ConfigPropertiesContext(String authServerInternalUrl, String oidcClientId, String oidcClientSecret,
String authServerInternalBaseUrl) {
}

Map<String, String> createProperties(ConfigPropertiesContext context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,14 +287,22 @@ private static void closeDevService() {
capturedDevServicesConfiguration = null;
}

private static String getBaseURL(String scheme, String host, Integer port) {
return scheme + host + ":" + port;
}

private static String startURL(String scheme, String host, Integer port, boolean isKeycloakX) {
return scheme + host + ":" + port + (isKeycloakX ? "" : "/auth");
return getBaseURL(scheme, host, port) + (isKeycloakX ? "" : "/auth");
}

private static String startURL(String baseUrl, boolean isKeycloakX) {
return baseUrl + (isKeycloakX ? "" : "/auth");
}

private static Map<String, String> prepareConfiguration(
BuildProducer<KeycloakDevServicesConfigBuildItem> keycloakBuildItemBuildProducer, String internalURL,
String hostURL, List<RealmRepresentation> realmReps, List<String> errors,
KeycloakDevServicesConfigurator devServicesConfigurator) {
KeycloakDevServicesConfigurator devServicesConfigurator, String internalBaseUrl) {
final String realmName = realmReps != null && !realmReps.isEmpty() ? realmReps.iterator().next().getRealm()
: getDefaultRealmName();
final String authServerInternalUrl = realmsURL(internalURL, realmName);
Expand Down Expand Up @@ -338,7 +346,8 @@ private static Map<String, String> prepareConfiguration(
}

Map<String, String> configProperties = new HashMap<>();
var configPropertiesContext = new ConfigPropertiesContext(authServerInternalUrl, oidcClientId, oidcClientSecret);
var configPropertiesContext = new ConfigPropertiesContext(authServerInternalUrl, oidcClientId, oidcClientSecret,
internalBaseUrl);
configProperties.putAll(devServicesConfigurator.createProperties(configPropertiesContext));
configProperties.put(KEYCLOAK_URL_KEY, internalURL);
configProperties.put(CLIENT_AUTH_SERVER_URL_CONFIG_KEY, clientAuthServerUrl);
Expand Down Expand Up @@ -397,8 +406,9 @@ private static RunningDevService startContainer(
oidcContainer.withEnv(capturedDevServicesConfiguration.containerEnv());
oidcContainer.start();

String internalUrl = startURL((oidcContainer.isHttps() ? "https://" : "http://"), oidcContainer.getHost(),
oidcContainer.getPort(), oidcContainer.keycloakX);
String internalBaseUrl = getBaseURL((oidcContainer.isHttps() ? "https://" : "http://"), oidcContainer.getHost(),
oidcContainer.getPort());
String internalUrl = startURL(internalBaseUrl, oidcContainer.keycloakX);
String hostUrl = oidcContainer.useSharedNetwork
// we need to use auto-detected host and port, so it works when docker host != localhost
? startURL("http://", oidcContainer.getSharedNetworkExternalHost(),
Expand All @@ -407,17 +417,17 @@ private static RunningDevService startContainer(
: null;

Map<String, String> configs = prepareConfiguration(keycloakBuildItemBuildProducer, internalUrl, hostUrl,
oidcContainer.realmReps, errors, devServicesConfigurator);
oidcContainer.realmReps, errors, devServicesConfigurator, internalBaseUrl);
return new RunningDevService(KEYCLOAK_CONTAINER_NAME, oidcContainer.getContainerId(),
oidcContainer::close, configs);
};

return maybeContainerAddress
.map(containerAddress -> {
// TODO: this probably needs to be addressed
Map<String, String> configs = prepareConfiguration(keycloakBuildItemBuildProducer,
getSharedContainerUrl(containerAddress),
getSharedContainerUrl(containerAddress), null, errors, devServicesConfigurator);
String sharedContainerUrl = getSharedContainerUrl(containerAddress);
Map<String, String> configs = prepareConfiguration(keycloakBuildItemBuildProducer, sharedContainerUrl,
sharedContainerUrl, null, errors, devServicesConfigurator, sharedContainerUrl);
return new RunningDevService(KEYCLOAK_CONTAINER_NAME, containerAddress.getId(), null, configs);
})
.orElseGet(defaultKeycloakContainerSupplier);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ public interface KeycloakAdminClientConfig {

/**
* Keycloak server URL, for example, `https://host:port`.
* If this property is not set then the Keycloak Admin Client injection will fail - use
* {@linkplain org.keycloak.admin.client.KeycloakBuilder}
* to create it instead.
* When the Keycloak Dev Services is started and this property is not configured,
* Quarkus points the 'quarkus.keycloak.admin-client.server-url' configuration property to started Keycloak container.
* In other cases, when this property is not set then the Keycloak Admin Client injection will fail - use
* {@linkplain org.keycloak.admin.client.KeycloakBuilder} to create the client instead.
*/
Optional<String> serverUrl();

Expand Down
9 changes: 4 additions & 5 deletions extensions/keycloak-admin-rest-client/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-keycloak-admin-client-common-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-devservices-keycloak</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
Expand All @@ -45,11 +49,6 @@
<artifactId>quarkus-rest-jackson-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.smallrye.certs</groupId>
<artifactId>smallrye-certificate-generator-junit5</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.quarkus.keycloak.admin.client.reactive.devservices;

import java.util.Map;

import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.IsNormal;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.BuildSteps;
import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig;
import io.quarkus.devservices.keycloak.KeycloakAdminPageBuildItem;
import io.quarkus.devservices.keycloak.KeycloakDevServicesRequiredBuildItem;
import io.quarkus.devui.spi.page.CardPageBuildItem;
import io.quarkus.keycloak.admin.client.common.KeycloakAdminClientInjectionEnabled;

@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = { GlobalDevServicesConfig.Enabled.class,
KeycloakAdminClientInjectionEnabled.class })
public class KeycloakDevServiceRequiredBuildStep {

private static final String SERVER_URL_CONFIG_KEY = "quarkus.keycloak.admin-client.server-url";

@BuildStep
KeycloakDevServicesRequiredBuildItem requireKeycloakDevService() {
return KeycloakDevServicesRequiredBuildItem.of(
ctx -> Map.of(SERVER_URL_CONFIG_KEY, ctx.authServerInternalBaseUrl()),
SERVER_URL_CONFIG_KEY);
}

@BuildStep(onlyIf = IsDevelopment.class)
KeycloakAdminPageBuildItem addCardWithLinkToKeycloakAdmin() {
return new KeycloakAdminPageBuildItem(new CardPageBuildItem());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,23 @@
import org.keycloak.admin.client.Keycloak;
import org.keycloak.representations.idm.RoleRepresentation;

import io.quarkus.test.QuarkusDevModeTest;
import io.quarkus.builder.Version;
import io.quarkus.maven.dependency.Dependency;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;
import io.restassured.response.Response;

public class KeycloakAdminClientInjectionDevServicesTest {

@RegisterExtension
final static QuarkusDevModeTest app = new QuarkusDevModeTest()
final static QuarkusUnitTest app = new QuarkusUnitTest()
.withApplicationRoot(jar -> jar
.addClasses(AdminResource.class)
.addAsResource("app-dev-mode-config.properties", "application.properties"));
.addAsResource("app-dev-mode-config.properties", "application.properties"))
// intention of this forced dependency is to test backwards compatibility
// when users started Keycloak Dev Service by adding OIDC extension and configured 'server-url'
.setForcedDependencies(
List.of(Dependency.of("io.quarkus", "quarkus-oidc-deployment", Version.getVersion())));

@Test
public void testGetRoles() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.RolesRepresentation;

import io.quarkus.test.QuarkusDevModeTest;
import io.quarkus.builder.Version;
import io.quarkus.maven.dependency.Dependency;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;
import io.smallrye.certs.Format;
import io.smallrye.certs.junit5.Certificate;
Expand All @@ -29,14 +31,18 @@
public class KeycloakAdminClientMutualTlsDevServicesTest {

@RegisterExtension
final static QuarkusDevModeTest app = new QuarkusDevModeTest()
final static QuarkusUnitTest app = new QuarkusUnitTest()
.withApplicationRoot(jar -> jar
.addClasses(MtlsResource.class)
.addAsResource(new File("target/certs/mtls-test-keystore.p12"), "server-keystore.p12")
.addAsResource(new File("target/certs/mtls-test-server-ca.crt"), "server-ca.crt")
.addAsResource(new File("target/certs/mtls-test-client-keystore.p12"), "client-keystore.p12")
.addAsResource(new File("target/certs/mtls-test-client-truststore.p12"), "client-truststore.p12")
.addAsResource("app-mtls-config.properties", "application.properties"));
.addAsResource("app-mtls-config.properties", "application.properties"))
// intention of this forced dependency is to test backwards compatibility
// when users started Keycloak Dev Service by adding OIDC extension and configured 'server-url'
.setForcedDependencies(
List.of(Dependency.of("io.quarkus", "quarkus-oidc-deployment", Version.getVersion())));

@Test
public void testCreateRealm() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.quarkus.keycloak.admin.client.reactive;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.representations.idm.RoleRepresentation;

import io.quarkus.test.QuarkusDevModeTest;
import io.restassured.RestAssured;
import io.restassured.response.Response;

public class KeycloakAdminClientZeroConfigDevServicesTest {

@RegisterExtension
final static QuarkusDevModeTest app = new QuarkusDevModeTest()
.withApplicationRoot(jar -> jar.addClasses(AdminResource.class));

@Test
public void testGetRoles() {
// use 'password' grant type
final Response getRolesReq = RestAssured.given().get("/api/admin/roles");
assertEquals(200, getRolesReq.statusCode());
final List<RoleRepresentation> roles = getRolesReq.jsonPath().getList(".", RoleRepresentation.class);
assertNotNull(roles);
// assert there are roles admin and user (among others)
assertTrue(roles.stream().anyMatch(rr -> "user".equals(rr.getName())));
assertTrue(roles.stream().anyMatch(rr -> "admin".equals(rr.getName())));
}

@Path("/api/admin")
public static class AdminResource {

@Inject
Keycloak keycloak;

@GET
@Path("/roles")
public List<RoleRepresentation> getRoles() {
return keycloak.realm("quarkus").roles().list();
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@
quarkus.keycloak.devservices.port=8082

# Configure Keycloak Admin Client
quarkus.keycloak.admin-client=true
quarkus.keycloak.admin-client.server-url=http://localhost:${quarkus.keycloak.devservices.port}
9 changes: 4 additions & 5 deletions extensions/keycloak-admin-resteasy-client/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-keycloak-admin-client-common-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-devservices-keycloak</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
Expand All @@ -53,11 +57,6 @@
<artifactId>quarkus-resteasy-jackson-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.smallrye.certs</groupId>
<artifactId>smallrye-certificate-generator-junit5</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.quarkus.keycloak.adminclient.deployment.devservices;

import java.util.Map;

import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.IsNormal;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.BuildSteps;
import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig;
import io.quarkus.devservices.keycloak.KeycloakAdminPageBuildItem;
import io.quarkus.devservices.keycloak.KeycloakDevServicesRequiredBuildItem;
import io.quarkus.devui.spi.page.CardPageBuildItem;
import io.quarkus.keycloak.admin.client.common.KeycloakAdminClientInjectionEnabled;

@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = { GlobalDevServicesConfig.Enabled.class,
KeycloakAdminClientInjectionEnabled.class })
public class KeycloakDevServiceRequiredBuildStep {

private static final String SERVER_URL_CONFIG_KEY = "quarkus.keycloak.admin-client.server-url";

@BuildStep
KeycloakDevServicesRequiredBuildItem requireKeycloakDevService() {
return KeycloakDevServicesRequiredBuildItem.of(
ctx -> Map.of(SERVER_URL_CONFIG_KEY, ctx.authServerInternalBaseUrl()),
SERVER_URL_CONFIG_KEY);
}

@BuildStep(onlyIf = IsDevelopment.class)
KeycloakAdminPageBuildItem addCardWithLinkToKeycloakAdmin() {
return new KeycloakAdminPageBuildItem(new CardPageBuildItem());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,23 @@
import org.keycloak.admin.client.Keycloak;
import org.keycloak.representations.idm.RoleRepresentation;

import io.quarkus.test.QuarkusDevModeTest;
import io.quarkus.builder.Version;
import io.quarkus.maven.dependency.Dependency;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;
import io.restassured.response.Response;

public class KeycloakAdminClientInjectionDevServicesTest {

@RegisterExtension
final static QuarkusDevModeTest app = new QuarkusDevModeTest()
final static QuarkusUnitTest app = new QuarkusUnitTest()
.withApplicationRoot(jar -> jar
.addClasses(AdminResource.class)
.addAsResource("app-dev-mode-config.properties", "application.properties"));
.addAsResource("app-dev-mode-config.properties", "application.properties"))
// intention of this forced dependency is to test backwards compatibility
// when users started Keycloak Dev Service by adding OIDC extension and configured 'server-url'
.setForcedDependencies(
List.of(Dependency.of("io.quarkus", "quarkus-oidc-deployment", Version.getVersion())));

@Test
public void testGetRoles() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.RolesRepresentation;

import io.quarkus.test.QuarkusDevModeTest;
import io.quarkus.builder.Version;
import io.quarkus.maven.dependency.Dependency;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;
import io.smallrye.certs.Format;
import io.smallrye.certs.junit5.Certificate;
Expand All @@ -29,14 +31,18 @@
public class KeycloakAdminClientMutualTlsDevServicesTest {

@RegisterExtension
final static QuarkusDevModeTest app = new QuarkusDevModeTest()
final static QuarkusUnitTest app = new QuarkusUnitTest()
.withApplicationRoot(jar -> jar
.addClasses(MtlsResource.class)
.addAsResource(new File("target/certs/mtls-test-keystore.p12"), "server-keystore.p12")
.addAsResource(new File("target/certs/mtls-test-server-ca.crt"), "server-ca.crt")
.addAsResource(new File("target/certs/mtls-test-client-keystore.p12"), "client-keystore.p12")
.addAsResource(new File("target/certs/mtls-test-client-truststore.p12"), "client-truststore.p12")
.addAsResource("app-mtls-config.properties", "application.properties"));
.addAsResource("app-mtls-config.properties", "application.properties"))
// intention of this forced dependency is to test backwards compatibility
// when users started Keycloak Dev Service by adding OIDC extension and configured 'server-url'
.setForcedDependencies(
List.of(Dependency.of("io.quarkus", "quarkus-oidc-deployment", Version.getVersion())));

@Test
public void testCreateRealm() {
Expand Down
Loading

0 comments on commit ee2100b

Please sign in to comment.