diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java index 08f34baaf2e6f..cd580975597c5 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java @@ -78,10 +78,11 @@ public class DevServicesConfig { public String serviceName; /** - * The class or file system path to a Keycloak realm file which will be used to initialize Keycloak. + * The comma-separated list of class or file system paths to Keycloak realm files which will be used to initialize Keycloak. + * The first value in this list will be used to initialize default tenant connection properties. */ @ConfigItem - public Optional realmPath; + public Optional> realmPath; /** * The JAVA_OPTS passed to the keycloak JVM diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevConsoleProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevConsoleProcessor.java index 5047e5f2b8160..38b17975a97f1 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevConsoleProcessor.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevConsoleProcessor.java @@ -36,6 +36,9 @@ public void setConfigProperties(BuildProducer d devConsoleInfo.produce( new DevConsoleTemplateInfoBuildItem("keycloakUsers", configProps.get().getProperties().get("oidc.users"))); + devConsoleInfo.produce( + new DevConsoleTemplateInfoBuildItem("keycloakRealms", + configProps.get().getProperties().get("keycloak.realms"))); String realmUrl = configProps.get().getConfig().get("quarkus.oidc.auth-server-url"); produceDevConsoleTemplateItems(capabilities, diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java index 4fe2d13be6e9e..a4b1063326a6b 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; @@ -36,7 +37,6 @@ import org.keycloak.representations.idm.RolesRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.util.JsonSerialization; -import org.testcontainers.containers.BindMode; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; @@ -87,6 +87,7 @@ public class KeycloakDevServicesProcessor { private static final String CLIENT_ID_CONFIG_KEY = CONFIG_PREFIX + "client-id"; private static final String CLIENT_SECRET_CONFIG_KEY = CONFIG_PREFIX + "credentials.secret"; private static final String KEYCLOAK_URL_KEY = "keycloak.url"; + private static final String CLIENT_KEYCLOAK_URL_KEY = "client." + KEYCLOAK_URL_KEY; private static final String KEYCLOAK_CONTAINER_NAME = "keycloak"; private static final int KEYCLOAK_PORT = 8080; @@ -100,7 +101,6 @@ public class KeycloakDevServicesProcessor { private static final String KEYCLOAK_WILDFLY_FRONTEND_URL = "KEYCLOAK_FRONTEND_URL"; private static final String KEYCLOAK_WILDFLY_USER_PROP = "KEYCLOAK_USER"; private static final String KEYCLOAK_WILDFLY_PASSWORD_PROP = "KEYCLOAK_PASSWORD"; - private static final String KEYCLOAK_WILDFLY_IMPORT_PROP = "KEYCLOAK_IMPORT"; private static final String KEYCLOAK_WILDFLY_DB_VENDOR = "H2"; private static final String KEYCLOAK_WILDFLY_VENDOR_PROP = "DB_VENDOR"; @@ -111,8 +111,8 @@ public class KeycloakDevServicesProcessor { private static final String KEYCLOAK_QUARKUS_START_CMD = "start --storage=chm --http-enabled=true --hostname-strict=false --hostname-strict-https=false"; private static final String JAVA_OPTS = "JAVA_OPTS"; - private static final String KEYCLOAK_DOCKER_REALM_PATH = "/tmp/realm.json"; private static final String OIDC_USERS = "oidc.users"; + private static final String KEYCLOAK_REALMS = "keycloak.realms"; /** * Label to add to shared Dev Service for Keycloak running in containers. @@ -153,13 +153,13 @@ public DevServicesResultBuildItem startKeycloakContainer( if (devService != null) { boolean restartRequired = !currentDevServicesConfiguration.equals(capturedDevServicesConfiguration); if (!restartRequired) { - FileTime currentRealmFileLastModifiedDate = getRealmFileLastModifiedDate( - currentDevServicesConfiguration.realmPath); - if (currentRealmFileLastModifiedDate != null - && !currentRealmFileLastModifiedDate.equals(capturedRealmFileLastModifiedDate)) { - restartRequired = true; - capturedRealmFileLastModifiedDate = currentRealmFileLastModifiedDate; - } + //FileTime currentRealmFileLastModifiedDate = getRealmFileLastModifiedDate( + // currentDevServicesConfiguration.realmPath); + //if (currentRealmFileLastModifiedDate != null + // && !currentRealmFileLastModifiedDate.equals(capturedRealmFileLastModifiedDate)) { + // restartRequired = true; + // capturedRealmFileLastModifiedDate = currentRealmFileLastModifiedDate; + //} } if (!restartRequired) { DevServicesResultBuildItem result = devService.toBuildItem(); @@ -167,8 +167,12 @@ public DevServicesResultBuildItem startKeycloakContainer( Map users = (usersString == null || usersString.isBlank()) ? Map.of() : Arrays.stream(usersString.split(",")) .map(s -> s.split("=")).collect(Collectors.toMap(s -> s[0], s -> s[1])); + String realmsString = result.getConfig().get(KEYCLOAK_REALMS); + List realms = (realmsString == null || realmsString.isBlank()) ? List.of() + : Arrays.stream(realmsString.split(",")).collect(Collectors.toList()); keycloakBuildItemBuildProducer - .produce(new KeycloakDevServicesConfigBuildItem(result.getConfig(), Map.of(OIDC_USERS, users))); + .produce(new KeycloakDevServicesConfigBuildItem(result.getConfig(), + Map.of(OIDC_USERS, users, KEYCLOAK_REALMS, realms))); return result; } try { @@ -226,7 +230,7 @@ public void run() { closeBuildItem.addCloseTask(closeTask, true); } - capturedRealmFileLastModifiedDate = getRealmFileLastModifiedDate(capturedDevServicesConfiguration.realmPath); + //capturedRealmFileLastModifiedDate = getRealmFileLastModifiedDate(capturedDevServicesConfiguration.realmPath); if (devService == null) { compressor.closeAndDumpCaptured(); } else { @@ -247,9 +251,9 @@ private String startURL(String host, Integer port, boolean isKeyCloakX) { private Map prepareConfiguration( BuildProducer keycloakBuildItemBuildProducer, String internalURL, - String hostURL, RealmRepresentation realmRep, + String hostURL, List realmReps, boolean keycloakX) { - final String realmName = realmRep != null ? realmRep.getRealm() : getDefaultRealmName(); + final String realmName = !realmReps.isEmpty() ? realmReps.iterator().next().getRealm() : getDefaultRealmName(); final String authServerInternalUrl = realmsURL(internalURL, realmName); String clientAuthServerBaseUrl = hostURL != null ? hostURL : internalURL; @@ -259,17 +263,22 @@ private Map prepareConfiguration( String oidcClientSecret = getOidcClientSecret(); String oidcApplicationType = getOidcApplicationType(); - boolean createDefaultRealm = realmRep == null && capturedDevServicesConfiguration.createRealm; + boolean createDefaultRealm = realmReps.isEmpty() && capturedDevServicesConfiguration.createRealm; Map users = getUsers(capturedDevServicesConfiguration.users, createDefaultRealm); + List realmNames = new LinkedList<>(); if (createDefaultRealm) { createDefaultRealm(clientAuthServerBaseUrl, users, oidcClientId, oidcClientSecret); - } else if (realmRep != null && keycloakX) { - createRealm(clientAuthServerBaseUrl, realmRep); - } + realmNames.add(realmName); + } else + for (RealmRepresentation realmRep : realmReps) { + createRealm(clientAuthServerBaseUrl, realmRep); + realmNames.add(realmRep.getRealm()); + } Map configProperties = new HashMap<>(); configProperties.put(KEYCLOAK_URL_KEY, internalURL); + configProperties.put(CLIENT_KEYCLOAK_URL_KEY, clientAuthServerBaseUrl); configProperties.put(AUTH_SERVER_URL_CONFIG_KEY, authServerInternalUrl); configProperties.put(CLIENT_AUTH_SERVER_URL_CONFIG_KEY, clientAuthServerUrl); configProperties.put(APPLICATION_TYPE_CONFIG_KEY, oidcApplicationType); @@ -277,9 +286,11 @@ private Map prepareConfiguration( configProperties.put(CLIENT_SECRET_CONFIG_KEY, oidcClientSecret); configProperties.put(OIDC_USERS, users.entrySet().stream() .map(e -> e.toString()).collect(Collectors.joining(","))); + configProperties.put(KEYCLOAK_REALMS, realmNames.stream().collect(Collectors.joining(","))); keycloakBuildItemBuildProducer - .produce(new KeycloakDevServicesConfigBuildItem(configProperties, Map.of(OIDC_USERS, users))); + .produce(new KeycloakDevServicesConfigBuildItem(configProperties, + Map.of(OIDC_USERS, users, KEYCLOAK_REALMS, realmNames))); return configProperties; } @@ -331,7 +342,7 @@ private RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuild QuarkusOidcContainer oidcContainer = new QuarkusOidcContainer(dockerImageName, capturedDevServicesConfiguration.port, useSharedNetwork, - capturedDevServicesConfiguration.realmPath, + capturedDevServicesConfiguration.realmPath.orElse(List.of()), capturedDevServicesConfiguration.serviceName, capturedDevServicesConfiguration.shared, capturedDevServicesConfiguration.javaOpts, @@ -347,7 +358,7 @@ private RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuild : null; Map configs = prepareConfiguration(keycloakBuildItemBuildProducer, internalUrl, hostUrl, - oidcContainer.realmRep, + oidcContainer.realmReps, oidcContainer.keycloakX); return new RunningDevService(KEYCLOAK_CONTAINER_NAME, oidcContainer.getContainerId(), oidcContainer::close, configs); @@ -378,23 +389,23 @@ private String getSharedContainerUrl(ContainerAddress containerAddress) { private static class QuarkusOidcContainer extends GenericContainer { private final OptionalInt fixedExposedPort; private final boolean useSharedNetwork; - private final Optional realmPath; + private final List realmPaths; private final String containerLabelValue; private final Optional javaOpts; private final boolean sharedContainer; private String hostName; private final boolean keycloakX; - private RealmRepresentation realmRep; + private List realmReps = new LinkedList<>(); private final Optional startCommand; private final boolean showLogs; public QuarkusOidcContainer(DockerImageName dockerImageName, OptionalInt fixedExposedPort, boolean useSharedNetwork, - Optional realmPath, String containerLabelValue, + List realmPaths, String containerLabelValue, boolean sharedContainer, Optional javaOpts, Optional startCommand, boolean showLogs) { super(dockerImageName); this.useSharedNetwork = useSharedNetwork; - this.realmPath = realmPath; + this.realmPaths = realmPaths; this.containerLabelValue = containerLabelValue; this.sharedContainer = sharedContainer; this.javaOpts = javaOpts; @@ -452,31 +463,21 @@ protected void configure() { addEnv(KEYCLOAK_WILDFLY_VENDOR_PROP, KEYCLOAK_WILDFLY_DB_VENDOR); } - if (realmPath.isPresent()) { + for (String realmPath : realmPaths) { URL realmPathUrl = null; - if ((realmPathUrl = Thread.currentThread().getContextClassLoader().getResource(realmPath.get())) != null) { - realmRep = readRealmFile(realmPathUrl, realmPath.get()); - if (!keycloakX) { - withClasspathResourceMapping(realmPath.get(), KEYCLOAK_DOCKER_REALM_PATH, BindMode.READ_ONLY); - } + if ((realmPathUrl = Thread.currentThread().getContextClassLoader().getResource(realmPath)) != null) { + readRealmFile(realmPathUrl, realmPath).ifPresent(realmRep -> realmReps.add(realmRep)); } else { - Path filePath = Paths.get(realmPath.get()); + Path filePath = Paths.get(realmPath); if (Files.exists(filePath)) { - if (!keycloakX) { - withFileSystemBind(realmPath.get(), KEYCLOAK_DOCKER_REALM_PATH, BindMode.READ_ONLY); - } - realmRep = readRealmFile(filePath.toUri(), realmPath.get()); + readRealmFile(filePath.toUri(), realmPath).ifPresent(realmRep -> realmReps.add(realmRep)); } else { - LOG.debugf("Realm %s resource is not available", realmPath.get()); + LOG.debugf("Realm %s resource is not available", realmPath); } } } - if (realmRep != null && !keycloakX) { - addEnv(KEYCLOAK_WILDFLY_IMPORT_PROP, KEYCLOAK_DOCKER_REALM_PATH); - } - if (showLogs) { super.withLogConsumer(t -> { LOG.info("Keycloak: " + t.getUtf8String()); @@ -494,7 +495,7 @@ private Integer findRandomPort() { } } - private RealmRepresentation readRealmFile(URI uri, String realmPath) { + private Optional readRealmFile(URI uri, String realmPath) { try { return readRealmFile(uri.toURL(), realmPath); } catch (MalformedURLException ex) { @@ -503,15 +504,15 @@ private RealmRepresentation readRealmFile(URI uri, String realmPath) { } } - private RealmRepresentation readRealmFile(URL url, String realmPath) { + private Optional readRealmFile(URL url, String realmPath) { try { try (InputStream is = url.openStream()) { - return JsonSerialization.readValue(is, RealmRepresentation.class); + return Optional.of(JsonSerialization.readValue(is, RealmRepresentation.class)); } } catch (IOException ex) { LOG.errorf("Realm %s resource can not be opened: %s", realmPath, ex.getMessage()); } - return null; + return Optional.empty(); } @Override @@ -583,6 +584,7 @@ private void createRealm(String keycloakUrl, RealmRepresentation realm) { .transform(resp -> { LOG.debugf("Realm status: %d", resp.statusCode()); if (resp.statusCode() == 200) { + LOG.debugf("Realm %s has been created", realm.getRealm()); return 200; } else { throw new RealmEndpointAccessException(resp.statusCode()); diff --git a/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/client/KeycloakTestClient.java b/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/client/KeycloakTestClient.java index 13335c8a8dd2b..46fd5c180e6bf 100644 --- a/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/client/KeycloakTestClient.java +++ b/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/client/KeycloakTestClient.java @@ -18,6 +18,8 @@ public class KeycloakTestClient implements DevServicesContext.ContextAware { private final static String CLIENT_AUTH_SERVER_URL_PROP = "client.quarkus.oidc.auth-server-url"; private final static String AUTH_SERVER_URL_PROP = "quarkus.oidc.auth-server-url"; + private static final String CLIENT_KEYCLOAK_URL_KEY = "client.keycloak.url"; + private static final String KEYCLOAK_URL_KEY = "keycloak.url"; private final static String CLIENT_ID_PROP = "quarkus.oidc.client-id"; private final static String CLIENT_SECRET_PROP = "quarkus.oidc.credentials.secret"; @@ -32,7 +34,7 @@ public KeycloakTestClient() { } /** - * Get an access token using a password grant with a provided user name. + * Get an access token from the default tenant realm using a password grant with a provided user name. * User secret will be the same as the user name, client id will be set to 'quarkus-app' and client secret to 'secret'. */ public String getAccessToken(String userName) { @@ -40,7 +42,7 @@ public String getAccessToken(String userName) { } /** - * Get an access token using a password grant with the provided user name and client id. + * Get an access token from the default tenant realm using a password grant with the provided user name and client id. * User secret will be the same as the user name, client secret will be set to 'secret'. */ public String getAccessToken(String userName, String clientId) { @@ -48,7 +50,8 @@ public String getAccessToken(String userName, String clientId) { } /** - * Get an access token using a password grant with the provided user name, user secret and client id. + * Get an access token from the default tenant realm using a password grant with the provided user name, user secret and + * client id. * Client secret will be set to 'secret'. */ public String getAccessToken(String userName, String userSecret, String clientId) { @@ -56,13 +59,47 @@ public String getAccessToken(String userName, String userSecret, String clientId } /** - * Get an access token using a password grant with the provided user name, user secret, client id and secret. + * Get an access token from the default tenant realm using a password grant with the provided user name, user secret, client + * id and secret. * Set the client secret to an empty string or null if it is not required. */ public String getAccessToken(String userName, String userSecret, String clientId, String clientSecret) { return getAccessTokenInternal(userName, userSecret, clientId, clientSecret, getAuthServerUrl()); } + /** + * Get a realm access token using a password grant with a provided user name. + * User secret will be the same as the user name, client id will be set to 'quarkus-app' and client secret to 'secret'. + */ + public String getRealmAccessToken(String realm, String userName) { + return getRealmAccessToken(realm, userName, getClientId()); + } + + /** + * Get a realm access token using a password grant with the provided user name and client id. + * User secret will be the same as the user name, client secret will be set to 'secret'. + */ + public String getRealmAccessToken(String realm, String userName, String clientId) { + return getRealmAccessToken(realm, userName, userName, clientId); + } + + /** + * Get a realm access token using a password grant with the provided user name, user secret and client id. + * Client secret will be set to 'secret'. + */ + public String getRealmAccessToken(String realm, String userName, String userSecret, String clientId) { + return getRealmAccessToken(realm, userName, userSecret, clientId, getClientSecret()); + } + + /** + * Get a realm access token using a password grant with the provided user name, user secret, client id and secret. + * Set the client secret to an empty string or null if it is not required. + */ + public String getRealmAccessToken(String realm, String userName, String userSecret, String clientId, String clientSecret) { + return getAccessTokenInternal(userName, userSecret, clientId, clientSecret, + getAuthServerBaseUrl() + "/realms/" + realm); + } + private String getAccessTokenInternal(String userName, String userSecret, String clientId, String clientSecret, String authServerUrl) { RequestSpecification requestSpec = RestAssured.given().param("grant_type", "password")