339 lines
12 KiB
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;
|
|
}
|
|
}
|