Skip to content

Commit

Permalink
Update DevServices for Keycloak to support multiple realms
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Sep 30, 2022
1 parent 543ba54 commit 8077757
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> realmPath;
public Optional<List<String>> realmPath;

/**
* The JAVA_OPTS passed to the keycloak JVM
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public void setConfigProperties(BuildProducer<DevConsoleTemplateInfoBuildItem> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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";

Expand All @@ -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.
Expand Down Expand Up @@ -153,22 +153,26 @@ 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();
String usersString = result.getConfig().get(OIDC_USERS);
Map<String, String> 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<String> 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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -247,9 +251,9 @@ private String startURL(String host, Integer port, boolean isKeyCloakX) {

private Map<String, String> prepareConfiguration(
BuildProducer<KeycloakDevServicesConfigBuildItem> keycloakBuildItemBuildProducer, String internalURL,
String hostURL, RealmRepresentation realmRep,
String hostURL, List<RealmRepresentation> 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;
Expand All @@ -259,27 +263,34 @@ private Map<String, String> prepareConfiguration(
String oidcClientSecret = getOidcClientSecret();
String oidcApplicationType = getOidcApplicationType();

boolean createDefaultRealm = realmRep == null && capturedDevServicesConfiguration.createRealm;
boolean createDefaultRealm = realmReps.isEmpty() && capturedDevServicesConfiguration.createRealm;
Map<String, String> users = getUsers(capturedDevServicesConfiguration.users, createDefaultRealm);

List<String> 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<String, String> 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);
configProperties.put(CLIENT_ID_CONFIG_KEY, oidcClientId);
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;
}
Expand Down Expand Up @@ -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,
Expand All @@ -347,7 +358,7 @@ private RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuild
: null;

Map<String, String> configs = prepareConfiguration(keycloakBuildItemBuildProducer, internalUrl, hostUrl,
oidcContainer.realmRep,
oidcContainer.realmReps,
oidcContainer.keycloakX);
return new RunningDevService(KEYCLOAK_CONTAINER_NAME, oidcContainer.getContainerId(),
oidcContainer::close, configs);
Expand Down Expand Up @@ -378,23 +389,23 @@ private String getSharedContainerUrl(ContainerAddress containerAddress) {
private static class QuarkusOidcContainer extends GenericContainer<QuarkusOidcContainer> {
private final OptionalInt fixedExposedPort;
private final boolean useSharedNetwork;
private final Optional<String> realmPath;
private final List<String> realmPaths;
private final String containerLabelValue;
private final Optional<String> javaOpts;
private final boolean sharedContainer;
private String hostName;
private final boolean keycloakX;
private RealmRepresentation realmRep;
private List<RealmRepresentation> realmReps = new LinkedList<>();
private final Optional<String> startCommand;
private final boolean showLogs;

public QuarkusOidcContainer(DockerImageName dockerImageName, OptionalInt fixedExposedPort, boolean useSharedNetwork,
Optional<String> realmPath, String containerLabelValue,
List<String> realmPaths, String containerLabelValue,
boolean sharedContainer, Optional<String> javaOpts, Optional<String> startCommand, boolean showLogs) {
super(dockerImageName);

this.useSharedNetwork = useSharedNetwork;
this.realmPath = realmPath;
this.realmPaths = realmPaths;
this.containerLabelValue = containerLabelValue;
this.sharedContainer = sharedContainer;
this.javaOpts = javaOpts;
Expand Down Expand Up @@ -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());
Expand All @@ -494,7 +495,7 @@ private Integer findRandomPort() {
}
}

private RealmRepresentation readRealmFile(URI uri, String realmPath) {
private Optional<RealmRepresentation> readRealmFile(URI uri, String realmPath) {
try {
return readRealmFile(uri.toURL(), realmPath);
} catch (MalformedURLException ex) {
Expand All @@ -503,15 +504,15 @@ private RealmRepresentation readRealmFile(URI uri, String realmPath) {
}
}

private RealmRepresentation readRealmFile(URL url, String realmPath) {
private Optional<RealmRepresentation> 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
Expand Down Expand Up @@ -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());
Expand Down
Loading

0 comments on commit 8077757

Please sign in to comment.