keycloak/quarkus/tests/junit5/src/main/java/org/keycloak/Keycloak.java

339 lines
12 KiB
Java

/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak;
import static java.util.Optional.ofNullable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
import org.keycloak.common.Version;
import org.keycloak.common.crypto.FipsMode;
import org.keycloak.config.DatabaseOptions;
import org.keycloak.config.HttpOptions;
import org.keycloak.config.LoggingOptions;
import org.keycloak.config.Option;
import org.keycloak.config.SecurityOptions;
import org.keycloak.config.StorageOptions;
import org.keycloak.platform.Platform;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.cli.Picocli;
import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import io.quarkus.bootstrap.app.AugmentAction;
import io.quarkus.bootstrap.app.CuratedApplication;
import io.quarkus.bootstrap.app.QuarkusBootstrap;
import io.quarkus.bootstrap.app.RunningQuarkusApplication;
import io.quarkus.bootstrap.app.StartupAction;
import io.quarkus.bootstrap.model.ApplicationModel;
import io.quarkus.bootstrap.resolver.AppModelResolverException;
import io.quarkus.bootstrap.resolver.BootstrapAppModelResolver;
import io.quarkus.bootstrap.resolver.maven.BootstrapMavenException;
import io.quarkus.bootstrap.resolver.maven.MavenArtifactResolver;
import io.quarkus.bootstrap.workspace.WorkspaceModule;
import io.quarkus.bootstrap.workspace.WorkspaceModuleId;
import io.quarkus.maven.dependency.Dependency;
import io.quarkus.maven.dependency.DependencyBuilder;
import io.quarkus.runtime.configuration.QuarkusConfigFactory;
public class Keycloak {
static {
System.setProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager");
System.setProperty("quarkus.http.test-port", "${kc.http-port}");
System.setProperty("quarkus.http.test-ssl-port", "${kc.https-port}");
}
public static void main(String[] args) {
Keycloak.builder().start(args);
}
public static class Builder {
private String version;
private Path homeDir;
private List<Dependency> dependencies = new ArrayList<>();
private Builder() {
}
public Builder setVersion(String version) {
this.version = version;
return this;
}
public Builder setHomeDir(Path path) {
this.homeDir = path;
return this;
}
public Builder addDependency(String groupId, String artifactId, String version) {
addDependency(groupId, artifactId, version, null);
return this;
}
public Builder addDependency(String groupId, String artifactId, String version, String classifier) {
this.dependencies.add(DependencyBuilder.newInstance()
.setGroupId(groupId)
.setArtifactId(artifactId)
.setVersion(version)
.setClassifier(classifier)
.build());
return this;
}
public Keycloak start(String... args) {
return start(List.of(args));
}
public Keycloak start(List<String> rawArgs) {
if (homeDir == null) {
homeDir = Platform.getPlatform().getTmpDirectory().toPath();
}
List<String> args = new ArrayList<>(rawArgs);
addOptionIfNotSet(args, HttpOptions.HTTP_ENABLED, true);
addOptionIfNotSet(args, HttpOptions.HTTP_PORT);
addOptionIfNotSet(args, HttpOptions.HTTPS_PORT);
if (getOptionValue(args, DatabaseOptions.DB) == null) {
addOptionIfNotSet(args, StorageOptions.STORAGE, StorageOptions.StorageType.chm);
}
boolean isFipsEnabled = ofNullable(getOptionValue(args, SecurityOptions.FIPS_MODE)).orElse(FipsMode.DISABLED).isFipsEnabled();
if (isFipsEnabled) {
String logLevel = getOptionValue(args, LoggingOptions.LOG_LEVEL);
if (logLevel == null) {
args.add("--log-level=org.keycloak.common.crypto:TRACE,org.keycloak.crypto:TRACE");
}
}
return new Keycloak(homeDir, version, dependencies, isFipsEnabled).start(args);
}
private <T> void addOptionIfNotSet(List<String> args, Option<T> option) {
addOptionIfNotSet(args, option, null);
}
private <T> void addOptionIfNotSet(List<String> args, Option<T> option, T defaultValue) {
T value = getOptionValue(args, option);
if (value == null) {
defaultValue = ofNullable(defaultValue).orElseGet(option.getDefaultValue()::get);
args.add(Configuration.toCliFormat(option.getKey()) + "=" + defaultValue);
}
}
private <T> T getOptionValue(List<String> args, Option<T> option) {
for (String arg : args) {
if (arg.contains(option.getKey())) {
if (arg.endsWith(option.getKey())) {
throw new IllegalArgumentException("Option '" + arg + "' value must be set using '=' as a separator");
}
String value = arg.substring(Picocli.ARG_PREFIX.length() + option.getKey().length() + 1);
Class<T> type = option.getType();
if (type.equals(String.class)) {
return (T) value;
}
if (type.isEnum()) {
return (T) Enum.valueOf((Class<Enum>) type, value);
}
if (Integer.class.isAssignableFrom(type)) {
return (T) Integer.valueOf(value);
}
if (Boolean.class.isAssignableFrom(type)) {
return (T) Boolean.valueOf(value);
}
throw new RuntimeException("Unsupported option type '" + type + "'");
}
}
return null;
}
}
public static Builder builder() {
return new Builder();
}
private CuratedApplication curated;
private RunningQuarkusApplication application;
private ApplicationModel applicationModel;
private Path homeDir;
private List<Dependency> dependencies;
private boolean fipsEnabled;
public Keycloak() {
this(null, Version.VERSION, List.of(), false);
}
public Keycloak(Path homeDir, String version, List<Dependency> dependencies, boolean fipsEnabled) {
this.homeDir = homeDir;
this.dependencies = dependencies;
this.fipsEnabled = fipsEnabled;
try {
applicationModel = createApplicationModel(version);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private Keycloak start(List<String> args) {
QuarkusBootstrap.Builder builder = QuarkusBootstrap.builder()
.setExistingModel(applicationModel)
.setApplicationRoot(applicationModel.getApplicationModule().getModuleDir().toPath())
.setTargetDirectory(applicationModel.getApplicationModule().getModuleDir().toPath())
.setIsolateDeployment(true)
.setFlatClassPath(true)
.setMode(QuarkusBootstrap.Mode.TEST);
try {
curated = builder.build().bootstrap();
AugmentAction action = curated.createAugmentor();
Environment.setHomeDir(homeDir);
ConfigArgsConfigSource.setCliArgs(args.toArray(new String[0]));
StartupAction startupAction = action.createInitialRuntimeApplication();
application = startupAction.runMainClass(args.toArray(new String[0]));
return this;
} catch (Exception cause) {
throw new RuntimeException("Fail to start the server", cause);
}
}
public void stop() throws TimeoutException {
if (isRunning()) {
closeApplication();
}
}
private ApplicationModel createApplicationModel(String keycloakVersion)
throws AppModelResolverException {
// initialize Quarkus application model resolver
BootstrapAppModelResolver appModelResolver = new BootstrapAppModelResolver(getMavenArtifactResolver());
// configure server dependencies
WorkspaceModule module = createWorkspaceModule(keycloakVersion);
// resolve Keycloak server Quarkus application model
return appModelResolver.resolveModel(module);
}
private WorkspaceModule createWorkspaceModule(String keycloakVersion) {
Path moduleDir = createModuleDir();
DependencyBuilder serverDependency = DependencyBuilder.newInstance()
.setGroupId("org.keycloak")
.setArtifactId("keycloak-quarkus-server")
.setVersion(keycloakVersion)
.addExclusion("org.jboss.logmanager", "log4j-jboss-logmanager");
if (fipsEnabled) {
serverDependency.addExclusion("org.bouncycastle", "bcprov-jdk15on");
serverDependency.addExclusion("org.bouncycastle", "bcpkix-jdk15on");
serverDependency.addExclusion("org.keycloak", "keycloak-crypto-default");
} else {
serverDependency.addExclusion("org.keycloak", "keycloak-crypto-fips1402");
}
WorkspaceModule.Mutable builder = WorkspaceModule.builder()
.setModuleId(WorkspaceModuleId.of("org.keycloak", "keycloak-embedded", "1"))
.setModuleDir(moduleDir)
.setBuildDir(moduleDir)
.addDependencyConstraint(
Dependency.pomImport("org.keycloak", "keycloak-quarkus-parent", keycloakVersion))
.addDependency(serverDependency.build());
if (fipsEnabled) {
builder.addDependency(Dependency.of("org.bouncycastle", "bc-fips"));
builder.addDependency(Dependency.of("org.bouncycastle", "bctls-fips"));
builder.addDependency(Dependency.of("org.bouncycastle", "bcpkix-fips"));
}
for (Dependency dependency : dependencies) {
builder.addDependency(dependency);
}
return builder.build();
}
private static Path createModuleDir() {
Path moduleDir;
try {
moduleDir = Files.createTempDirectory("kc-embedded");
} catch (IOException e) {
throw new RuntimeException(e);
}
return moduleDir;
}
MavenArtifactResolver getMavenArtifactResolver() throws BootstrapMavenException {
return MavenArtifactResolver.builder()
.setWorkspaceDiscovery(true)
.setOffline(false)
.build();
}
private boolean isRunning() {
return application != null;
}
private void closeApplication() {
if (application != null) {
try {
// curated application is also closed
application.close();
} catch (Exception cause) {
cause.printStackTrace();
}
}
QuarkusConfigFactory.setConfig(null);
ClassLoader old = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
try {
ConfigProviderResolver cpr = ConfigProviderResolver.instance();
cpr.releaseConfig(cpr.getConfig());
} catch (Throwable ignored) {
// just means no config was installed, which is fine
} finally {
Thread.currentThread().setContextClassLoader(old);
}
application = null;
curated = null;
}
}