Move all dist options to the new module

Co-authored-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
andreaTP 2022-06-02 15:17:41 +01:00 committed by Pedro Igor
parent 71e7982a49
commit 3abcc699a1
35 changed files with 985 additions and 534 deletions

View file

@ -38,4 +38,11 @@
<maven.compiler.target>11</maven.compiler.target> <maven.compiler.target>11</maven.compiler.target>
</properties> </properties>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-common</artifactId>
</dependency>
</dependencies>
</project> </project>

View file

@ -5,9 +5,19 @@ import java.util.List;
public class AllOptions { public class AllOptions {
public final static List<Option<?>> ALL_OPTIONS = new ArrayList<>(); public static final List<Option<?>> ALL_OPTIONS = new ArrayList<>();
static { static {
ALL_OPTIONS.addAll(ClusteringOptions.ALL_OPTIONS);
ALL_OPTIONS.addAll(DatabaseOptions.ALL_OPTIONS);
ALL_OPTIONS.addAll(FeatureOptions.ALL_OPTIONS);
ALL_OPTIONS.addAll(HealthOptions.ALL_OPTIONS);
ALL_OPTIONS.addAll(HostnameOptions.ALL_OPTIONS);
ALL_OPTIONS.addAll(HttpOptions.ALL_OPTIONS); ALL_OPTIONS.addAll(HttpOptions.ALL_OPTIONS);
ALL_OPTIONS.addAll(LoggingOptions.ALL_OPTIONS);
ALL_OPTIONS.addAll(MetricsOptions.ALL_OPTIONS);
ALL_OPTIONS.addAll(ProxyOptions.ALL_OPTIONS);
ALL_OPTIONS.addAll(TransactionOptions.ALL_OPTIONS);
ALL_OPTIONS.addAll(VaultOptions.ALL_OPTIONS);
} }
} }

View file

@ -0,0 +1,54 @@
package org.keycloak.config;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class ClusteringOptions {
public enum Mechanism {
ispn,
local
}
public static final Option CACHE = new OptionBuilder<>("cache", Mechanism.class)
.category(OptionCategory.CLUSTERING)
.description("Defines the cache mechanism for high-availability. "
+ "By default, a 'ispn' cache is used to create a cluster between multiple server nodes. "
+ "A 'local' cache disables clustering and is intended for development and testing purposes.")
.defaultValue(Mechanism.ispn)
.buildTime(true)
.build();
public enum Stack {
tcp,
udp,
kubernetes,
ec2,
azure,
google;
}
public static final Option CACHE_STACK = new OptionBuilder<>("cache-stack", Stack.class)
.category(OptionCategory.CLUSTERING)
.description("Define the default stack to use for cluster communication and node discovery. This option only takes effect "
+ "if 'cache' is set to 'ispn'. Default: udp.")
.buildTime(true)
.expectedValues(Stack.values())
.build();
public static final Option<File> CACHE_CONFIG_FILE = new OptionBuilder<>("cache-config-file", File.class)
.category(OptionCategory.CLUSTERING)
.description("Defines the file from which cache configuration should be loaded from. "
+ "The configuration file is relative to the 'conf/' directory.")
.buildTime(true)
.build();
public static final List<Option<?>> ALL_OPTIONS = new ArrayList<>();
static {
ALL_OPTIONS.add(CACHE);
ALL_OPTIONS.add(CACHE_STACK);
ALL_OPTIONS.add(CACHE_CONFIG_FILE);
}
}

View file

@ -0,0 +1,104 @@
package org.keycloak.config;
import org.keycloak.config.database.Database;
import java.util.ArrayList;
import java.util.List;
public class DatabaseOptions {
public static final Option<String> DB_DIALECT = new OptionBuilder<>("db-dialect", String.class)
.category(OptionCategory.DATABASE)
.runtimes(Option.Runtime.OPERATOR)
.buildTime(true)
.build();
public static final Option<String> DB_DRIVER = new OptionBuilder<>("db-driver", String.class)
.category(OptionCategory.DATABASE)
.runtimes(Option.Runtime.OPERATOR)
.defaultValue(Database.getDriver("dev-file", true).get())
.build();
public static final Option<Database.Vendor> DB = new OptionBuilder<>("db", Database.Vendor.class)
.category(OptionCategory.DATABASE)
.description("The database vendor. Possible values are: " + String.join(", ", Database.getAliases()))
.expectedStringValues(Database.getAliases())
.buildTime(true)
.build();
public static final Option<String> DB_URL = new OptionBuilder<>("db-url", String.class)
.category(OptionCategory.DATABASE)
.description("The full database JDBC URL. If not provided, a default URL is set based on the selected database vendor. " +
"For instance, if using 'postgres', the default JDBC URL would be 'jdbc:postgresql://localhost/keycloak'. ")
.build();
public static final Option<String> DB_URL_HOST = new OptionBuilder<>("db-url-host", String.class)
.category(OptionCategory.DATABASE)
.description("Sets the hostname of the default JDBC URL of the chosen vendor. If the `db-url` option is set, this option is ignored.")
.build();
public static final Option<String> DB_URL_DATABASE = new OptionBuilder<>("db-url-database", String.class)
.category(OptionCategory.DATABASE)
.description("Sets the database name of the default JDBC URL of the chosen vendor. If the `db-url` option is set, this option is ignored.")
.build();
public static final Option<Integer> DB_URL_PORT = new OptionBuilder<>("db-url-port", Integer.class)
.category(OptionCategory.DATABASE)
.description("Sets the port of the default JDBC URL of the chosen vendor. If the `db-url` option is set, this option is ignored.")
.build();
public static final Option<String> DB_URL_PROPERTIES = new OptionBuilder<>("db-url-properties", String.class)
.category(OptionCategory.DATABASE)
.description("Sets the properties of the default JDBC URL of the chosen vendor. If the `db-url` option is set, this option is ignored.")
.build();
public static final Option<String> DB_USERNAME = new OptionBuilder<>("db-username", String.class)
.category(OptionCategory.DATABASE)
.description("The username of the database user.")
.build();
public static final Option<String> DB_PASSWORD = new OptionBuilder<>("db-password", String.class)
.category(OptionCategory.DATABASE)
.description("The password of the database user.")
.build();
public static final Option<String> DB_SCHEMA = new OptionBuilder<>("db-schema", String.class)
.category(OptionCategory.DATABASE)
.description("The database schema to be used.")
.build();
public static final Option<Integer> DB_POOL_INITIAL_SIZE = new OptionBuilder<>("db-pool-initial-size", Integer.class)
.category(OptionCategory.DATABASE)
.description("The initial size of the connection pool.")
.build();
public static final Option<Integer> DB_POOL_MIN_SIZE = new OptionBuilder<>("db-pool-min-size", Integer.class)
.category(OptionCategory.DATABASE)
.description("The minimal size of the connection pool.")
.build();
public static final Option<Integer> DB_POOL_MAX_SIZE = new OptionBuilder<>("db-pool-max-size", Integer.class)
.category(OptionCategory.DATABASE)
.defaultValue(100)
.description("The maximum size of the connection pool.")
.build();
public static final List<Option<?>> ALL_OPTIONS = new ArrayList<>();
static {
ALL_OPTIONS.add(DB_DIALECT);
ALL_OPTIONS.add(DB_DRIVER);
ALL_OPTIONS.add(DB);
ALL_OPTIONS.add(DB_URL);
ALL_OPTIONS.add(DB_URL_HOST);
ALL_OPTIONS.add(DB_URL_DATABASE);
ALL_OPTIONS.add(DB_URL_PORT);
ALL_OPTIONS.add(DB_URL_PROPERTIES);
ALL_OPTIONS.add(DB_USERNAME);
ALL_OPTIONS.add(DB_PASSWORD);
ALL_OPTIONS.add(DB_SCHEMA);
ALL_OPTIONS.add(DB_POOL_INITIAL_SIZE);
ALL_OPTIONS.add(DB_POOL_MIN_SIZE);
ALL_OPTIONS.add(DB_POOL_MAX_SIZE);
}
}

View file

@ -0,0 +1,42 @@
package org.keycloak.config;
import org.keycloak.common.Profile;
import java.util.ArrayList;
import java.util.List;
public class FeatureOptions {
public static final Option FEATURES = new OptionBuilder("features", List.class, Profile.Feature.class)
.category(OptionCategory.FEATURE)
.description("Enables a set of one or more features.")
.expectedStringValues(getFeatureValues())
.buildTime(true)
.build();
public static final Option FEATURES_DISABLED = new OptionBuilder("features-disabled", List.class, Profile.Feature.class)
.category(OptionCategory.FEATURE)
.description("Disables a set of one or more features.")
.expectedStringValues(getFeatureValues())
.buildTime(true)
.build();
private static List<String> getFeatureValues() {
List<String> features = new ArrayList<>();
for (Profile.Feature value : Profile.Feature.values()) {
features.add(value.name().toLowerCase().replace('_', '-'));
}
features.add(Profile.Type.PREVIEW.name().toLowerCase());
return features;
}
public static final List<Option<?>> ALL_OPTIONS = new ArrayList<>();
static {
ALL_OPTIONS.add(FEATURES);
ALL_OPTIONS.add(FEATURES_DISABLED);
}
}

View file

@ -0,0 +1,21 @@
package org.keycloak.config;
import java.util.ArrayList;
import java.util.List;
public class HealthOptions {
public static final Option HEALTH_ENABLED = new OptionBuilder<>("health-enabled", Boolean.class)
.category(OptionCategory.HEALTH)
.description("If the server should expose health check endpoints. If enabled, health checks are available at the '/health', '/health/ready' and '/health/live' endpoints.")
.defaultValue(Boolean.FALSE)
.buildTime(true)
.expectedValues(Boolean.TRUE, Boolean.FALSE)
.build();
public static final List<Option<?>> ALL_OPTIONS = new ArrayList<>();
static {
ALL_OPTIONS.add(HEALTH_ENABLED);
}
}

View file

@ -0,0 +1,57 @@
package org.keycloak.config;
import java.util.ArrayList;
import java.util.List;
public class HostnameOptions {
public static final Option HOSTNAME = new OptionBuilder<>("hostname", String.class)
.category(OptionCategory.HOSTNAME)
.description("Hostname for the Keycloak server.")
.build();
public static final Option HOSTNAME_ADMIN = new OptionBuilder<>("hostname-admin", String.class)
.category(OptionCategory.HOSTNAME)
.description("The hostname for accessing the administration console. Use this option if you are exposing the administration console using a hostname other than the value set to the 'hostname' option.")
.build();
public static final Option HOSTNAME_STRICT = new OptionBuilder<>("hostname-strict", Boolean.class)
.category(OptionCategory.HOSTNAME)
.description("Disables dynamically resolving the hostname from request headers. Should always be set to true in production, unless proxy verifies the Host header.")
.defaultValue(Boolean.TRUE)
.build();
public static final Option HOSTNAME_STRICT_HTTPS = new OptionBuilder<>("hostname-strict-https", Boolean.class)
.category(OptionCategory.HOSTNAME)
.description("Forces URLs to use HTTPS. Only needed if proxy does not properly set the X-Forwarded-Proto header.")
.runtimes(Option.Runtime.OPERATOR)
.defaultValue(Boolean.TRUE)
.build();
public static final Option HOSTNAME_STRICT_BACKCHANNEL = new OptionBuilder<>("hostname-strict-backchannel", Boolean.class)
.category(OptionCategory.HOSTNAME)
.description("By default backchannel URLs are dynamically resolved from request headers to allow internal and external applications. If all applications use the public URL this option should be enabled.")
.build();
public static final Option HOSTNAME_PATH = new OptionBuilder<>("hostname-path", String.class)
.category(OptionCategory.HOSTNAME)
.description("This should be set if proxy uses a different context-path for Keycloak.")
.build();
public static final Option HOSTNAME_PORT = new OptionBuilder<>("hostname-port", Integer.class)
.category(OptionCategory.HOSTNAME)
.description("The port used by the proxy when exposing the hostname. Set this option if the proxy uses a port other than the default HTTP and HTTPS ports.")
.defaultValue(-1)
.build();
public static final List<Option<?>> ALL_OPTIONS = new ArrayList<>();
static {
ALL_OPTIONS.add(HOSTNAME);
ALL_OPTIONS.add(HOSTNAME_STRICT);
ALL_OPTIONS.add(HOSTNAME_STRICT_HTTPS);
ALL_OPTIONS.add(HOSTNAME_STRICT_BACKCHANNEL);
ALL_OPTIONS.add(HOSTNAME_PATH);
ALL_OPTIONS.add(HOSTNAME_PORT);
}
}

View file

@ -1,20 +1,128 @@
package org.keycloak.config; package org.keycloak.config;
import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
public class HttpOptions { public class HttpOptions {
public final static Option httpPort = new OptionBuilder<Integer>("http-port", Integer.class) public static final Option<Boolean> HTTP_ENABLED = new OptionBuilder<>("http-enabled", Boolean.class)
.description("The used HTTP port.")
.category(OptionCategory.HTTP) .category(OptionCategory.HTTP)
.description("Enables the HTTP listener.")
.defaultValue(Boolean.FALSE)
.expectedValues(Boolean.TRUE, Boolean.FALSE)
.build();
public static final Option HTTP_HOST = new OptionBuilder<>("http-host", String.class)
.category(OptionCategory.HTTP)
.description("The used HTTP Host.")
.defaultValue("0.0.0.0")
.build();
public static final Option HTTP_RELATIVE_PATH = new OptionBuilder<>("http-relative-path", String.class)
.category(OptionCategory.HTTP)
.description("Set the path relative to '/' for serving resources.")
.defaultValue("/")
.buildTime(true)
.build();
public static final Option HTTP_PORT = new OptionBuilder<>("http-port", Integer.class)
.category(OptionCategory.HTTP)
.description("The used HTTP port.")
.defaultValue(8080) .defaultValue(8080)
.build(); .build();
public final static List<Option<?>> ALL_OPTIONS = new ArrayList<>(); public static final Option HTTPS_PORT = new OptionBuilder<>("https-port", Integer.class)
.category(OptionCategory.HTTP)
.description("The used HTTPS port.")
.defaultValue(8443)
.build();
public enum ClientAuth {
none,
request,
required
}
public static final Option HTTPS_CLIENT_AUTH = new OptionBuilder<>("https-client-auth", ClientAuth.class)
.category(OptionCategory.HTTP)
.description("Configures the server to require/request client authentication. Possible Values: none, request, required.")
.defaultValue(ClientAuth.none)
.expectedValues(ClientAuth.values())
.build();
public static final Option HTTPS_CIPHER_SUITES = new OptionBuilder<>("https-cipher-suites", String.class)
.category(OptionCategory.HTTP)
.description("The cipher suites to use. If none is given, a reasonable default is selected.")
.build();
public static final Option HTTPS_PROTOCOLS = new OptionBuilder<>("https-protocols", String.class)
.category(OptionCategory.HTTP)
.description("The list of protocols to explicitly enable.")
.defaultValue("TLSv1.3")
.build();
public static final Option HTTPS_CERTIFICATE_FILE = new OptionBuilder<>("https-certificate-file", File.class)
.category(OptionCategory.HTTP)
.description("The file path to a server certificate or certificate chain in PEM format.")
.build();
public static final Option HTTPS_CERTIFICATE_KEY_FILE = new OptionBuilder<>("https-certificate-key-file", File.class)
.category(OptionCategory.HTTP)
.description("The file path to a private key in PEM format.")
.build();
public static final Option HTTPS_KEY_STORE_FILE = new OptionBuilder<>("https-key-store-file", File.class)
.category(OptionCategory.HTTP)
.description("The key store which holds the certificate information instead of specifying separate files.")
.build();
public static final Option HTTPS_KEY_STORE_PASSWORD = new OptionBuilder<>("https-key-store-password", String.class)
.category(OptionCategory.HTTP)
.description("The password of the key store file.")
.defaultValue("password")
.build();
public static final Option HTTPS_KEY_STORE_TYPE = new OptionBuilder<>("https-key-store-type", String.class)
.category(OptionCategory.HTTP)
.description("The type of the key store file. " +
"If not given, the type is automatically detected based on the file name.")
.build();
public static final Option HTTPS_TRUST_STORE_FILE = new OptionBuilder<>("https-trust-store-file", File.class)
.category(OptionCategory.HTTP)
.description("The trust store which holds the certificate information of the certificates to trust.")
.build();
public static final Option HTTPS_TRUST_STORE_PASSWORD = new OptionBuilder<>("https-trust-store-password", String.class)
.category(OptionCategory.HTTP)
.description("The password of the trust store file.")
.build();
public static final Option HTTPS_TRUST_STORE_TYPE = new OptionBuilder<>("https-trust-store-type", File.class)
.category(OptionCategory.HTTP)
.description("The type of the trust store file. " +
"If not given, the type is automatically detected based on the file name.")
.build();
public static final List<Option<?>> ALL_OPTIONS = new ArrayList<>();
static { static {
ALL_OPTIONS.add(httpPort); ALL_OPTIONS.add(HTTP_ENABLED);
ALL_OPTIONS.add(HTTP_HOST);
ALL_OPTIONS.add(HTTP_RELATIVE_PATH);
ALL_OPTIONS.add(HTTP_PORT);
ALL_OPTIONS.add(HTTPS_PORT);
ALL_OPTIONS.add(HTTPS_CLIENT_AUTH);
ALL_OPTIONS.add(HTTPS_CIPHER_SUITES);
ALL_OPTIONS.add(HTTPS_PROTOCOLS);
ALL_OPTIONS.add(HTTPS_CERTIFICATE_FILE);
ALL_OPTIONS.add(HTTPS_CERTIFICATE_KEY_FILE);
ALL_OPTIONS.add(HTTPS_KEY_STORE_FILE);
ALL_OPTIONS.add(HTTPS_KEY_STORE_PASSWORD);
ALL_OPTIONS.add(HTTPS_KEY_STORE_TYPE);
ALL_OPTIONS.add(HTTPS_TRUST_STORE_FILE);
ALL_OPTIONS.add(HTTPS_TRUST_STORE_PASSWORD);
ALL_OPTIONS.add(HTTPS_TRUST_STORE_TYPE);
} }
} }

View file

@ -0,0 +1,116 @@
package org.keycloak.config;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
public class LoggingOptions {
public static final Handler DEFAULT_LOG_HANDLER = Handler.console;
public static final Level DEFAULT_LOG_LEVEL = Level.INFO;
public static final Output DEFAULT_CONSOLE_OUTPUT = Output.DEFAULT;
public static final String DEFAULT_LOG_FILENAME = "keycloak.log";
public static final String DEFAULT_LOG_PATH = "data" + File.separator + "log" + File.separator + DEFAULT_LOG_FILENAME;
public enum Handler {
console,
file;
}
public static final Option log = new OptionBuilder("log", List.class, Handler.class)
.category(OptionCategory.LOGGING)
.description("Enable one or more log handlers in a comma-separated list. Available log handlers are: " + Arrays.stream(Handler.values()).limit(2).map(h -> h.toString()).collect(Collectors.joining(",")))
.defaultValue(DEFAULT_LOG_HANDLER)
.expectedValues(Handler.values())
.build();
public enum Level {
OFF,
FATAL,
ERROR,
WARN,
INFO,
DEBUG,
TRACE,
ALL;
@Override
public String toString() {
return super.toString().toLowerCase(Locale.ROOT);
}
}
public static final Option<Level> LOG_LEVEL = new OptionBuilder<>("log-level", Level.class)
.category(OptionCategory.LOGGING)
.defaultValue(DEFAULT_LOG_LEVEL)
.description("The log level of the root category or a comma-separated list of individual categories and their levels. For the root category, you don't need to specify a category.")
.build();
public enum Output {
DEFAULT,
JSON;
@Override
public String toString() {
return super.toString().toLowerCase(Locale.ROOT);
}
}
public static final Option LOG_CONSOLE_OUTPUT = new OptionBuilder<>("log-console-output", Output.class)
.category(OptionCategory.LOGGING)
.defaultValue(DEFAULT_CONSOLE_OUTPUT)
.description("Set the log output to JSON or default (plain) unstructured logging.")
.expectedValues(Output.values())
.build();
public static final Option LOG_CONSOLE_FORMAT = new OptionBuilder<>("log-console-format", String.class)
.category(OptionCategory.LOGGING)
.description("The format of unstructured console log entries. If the format has spaces in it, escape the value using \"<format>\".")
.defaultValue("%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n")
.build();
public static final Option LOG_CONSOLE_COLOR = new OptionBuilder<>("log-console-color", Boolean.class)
.category(OptionCategory.LOGGING)
.description("Enable or disable colors when logging to console.")
.defaultValue(Boolean.FALSE) // :-(
.build();
public static final Option<Boolean> LOG_CONSOLE_ENABLED = new OptionBuilder<>("log-console-enabled", Boolean.class)
.category(OptionCategory.LOGGING)
.runtimes(Collections.emptySet())
.build();
public static final Option LOG_FILE_ENABLED = new OptionBuilder<>("log-file-enabled", Boolean.class)
.category(OptionCategory.LOGGING)
.runtimes(Collections.emptySet())
.build();
public static final Option<File> LOG_FILE = new OptionBuilder<>("log-file", File.class)
.category(OptionCategory.LOGGING)
.description("Set the log file path and filename.")
.defaultValue(new File(DEFAULT_LOG_PATH))
.build();
public static final Option LOG_FILE_FORMAT = new OptionBuilder<>("log-file-format", String.class)
.category(OptionCategory.LOGGING)
.description("Set a format specific to file log entries.")
.defaultValue("%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n")
.build();
public static final List<Option<?>> ALL_OPTIONS = new ArrayList<>();
static {
ALL_OPTIONS.add(log);
ALL_OPTIONS.add(LOG_LEVEL);
ALL_OPTIONS.add(LOG_CONSOLE_OUTPUT);
ALL_OPTIONS.add(LOG_CONSOLE_FORMAT);
ALL_OPTIONS.add(LOG_CONSOLE_COLOR);
ALL_OPTIONS.add(LOG_CONSOLE_ENABLED);
ALL_OPTIONS.add(LOG_FILE_ENABLED);
ALL_OPTIONS.add(LOG_FILE);
ALL_OPTIONS.add(LOG_FILE_FORMAT);
}
}

View file

@ -0,0 +1,21 @@
package org.keycloak.config;
import java.util.ArrayList;
import java.util.List;
public class MetricsOptions {
public static final Option METRICS_ENABLED = new OptionBuilder<>("metrics-enabled", Boolean.class)
.category(OptionCategory.METRICS)
.description("If the server should expose metrics. If enabled, metrics are available at the '/metrics' endpoint.")
.buildTime(true)
.defaultValue(Boolean.FALSE)
.expectedValues(Boolean.TRUE, Boolean.FALSE)
.build();
public static final List<Option<?>> ALL_OPTIONS = new ArrayList<>();
static {
ALL_OPTIONS.add(METRICS_ENABLED);
}
}

View file

@ -0,0 +1,19 @@
package org.keycloak.config;
import java.util.List;
import java.util.Optional;
import java.util.Set;
public class MultiOption<T> extends Option<T> {
private final Class auxiliaryType;
public MultiOption(Class type, Class auxiliaryType, String key, OptionCategory category, Set supportedRuntimes, boolean buildTime, String description, Optional defaultValue, List expectedValues) {
super(type, key, category, supportedRuntimes, buildTime, description, defaultValue, expectedValues);
this.auxiliaryType = auxiliaryType;
}
public Class<?> getAuxiliaryType() {
return auxiliaryType;
}
}

View file

@ -19,9 +19,9 @@ public class Option<T> {
private final boolean buildTime; private final boolean buildTime;
private final String description; private final String description;
private final Optional<T> defaultValue; private final Optional<T> defaultValue;
private final List<T> expectedValues; private final List<String> expectedValues;
public Option(Class<T> type, String key, OptionCategory category, Set<Runtime> supportedRuntimes, boolean buildTime, String description, Optional<T> defaultValue, List<T> expectedValues) { public Option(Class<T> type, String key, OptionCategory category, Set<Runtime> supportedRuntimes, boolean buildTime, String description, Optional<T> defaultValue, List<String> expectedValues) {
this.type = type; this.type = type;
this.key = key; this.key = key;
this.category = category; this.category = category;
@ -58,8 +58,21 @@ public class Option<T> {
return defaultValue; return defaultValue;
} }
public List<T> getExpectedValues() { public List<String> getExpectedValues() {
return expectedValues; return expectedValues;
} }
public Option<T> withRuntimeSpecificDefault(T defaultValue) {
return new Option<T>(
this.type,
this.key,
this.category,
this.supportedRuntimes,
this.buildTime,
this.description,
Optional.ofNullable(defaultValue),
this.expectedValues
);
}
} }

View file

@ -2,31 +2,50 @@ package org.keycloak.config;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class OptionBuilder<T> { public class OptionBuilder<T> {
private Class<T> type; private final Class<T> type;
private String key; private final Class<T> auxiliaryType;
private final String key;
private OptionCategory category; private OptionCategory category;
private Set<Option.Runtime> supportedRuntimes; private Set<Option.Runtime> supportedRuntimes;
private boolean build; private boolean build;
private String description; private String description;
private Optional<T> defaultValue; private Optional<T> defaultValue;
private List<T> expectedValues; private List<String> expectedValues;
public OptionBuilder(String key, Class<T> type) { public OptionBuilder(String key, Class<T> type) {
this.type = type; this.type = type;
this.auxiliaryType = null;
this.key = key; this.key = key;
category = OptionCategory.GENERAL; category = OptionCategory.GENERAL;
supportedRuntimes = Arrays.stream(Option.Runtime.values()).collect(Collectors.toSet()); supportedRuntimes = Arrays.stream(Option.Runtime.values()).collect(Collectors.toSet());
build = false; build = false;
description = ""; description = null;
defaultValue = Optional.empty(); defaultValue = Boolean.class.equals(type) ? Optional.of((T) Boolean.FALSE) : Optional.empty();
expectedValues = new ArrayList<>(); expectedValues = new ArrayList<>();
if (Boolean.class.equals(type)) {
expectedStringValues(Boolean.TRUE.toString(), Boolean.FALSE.toString());
}
}
public OptionBuilder(String key, Class<T> type, Class<T> auxiliaryType) {
this.type = type;
this.auxiliaryType = auxiliaryType;
this.key = key;
category = OptionCategory.GENERAL;
supportedRuntimes = Arrays.stream(Option.Runtime.values()).collect(Collectors.toSet());
build = false;
description = null;
defaultValue = Boolean.class.equals(type) ? Optional.of((T) Boolean.FALSE) : Optional.empty();
expectedValues = new ArrayList<>();
if (Boolean.class.equals(type)) {
expectedStringValues(Boolean.TRUE.toString(), Boolean.FALSE.toString());
}
} }
public OptionBuilder<T> category(OptionCategory category) { public OptionBuilder<T> category(OptionCategory category) {
@ -66,20 +85,37 @@ public class OptionBuilder<T> {
return this; return this;
} }
public OptionBuilder<T> expectedValues(List<T> expected) { public OptionBuilder<T> expectedStringValues(List<String> expected) {
this.expectedValues.clear(); this.expectedValues.clear();
this.expectedValues.addAll(expected); this.expectedValues.addAll(expected);
return this; return this;
} }
public OptionBuilder<T> expectedValues(T ... expected) { public OptionBuilder<T> expectedStringValues(String ... expected) {
this.expectedValues.clear(); this.expectedValues.clear();
this.expectedValues.addAll(Arrays.asList(expected)); this.expectedValues.addAll(Arrays.asList(expected));
return this; return this;
} }
public OptionBuilder<T> expectedValues(List<T> expected) {
this.expectedValues.clear();
this.expectedValues.addAll(expected.stream().map(v -> v.toString()).collect(Collectors.toList()));
return this;
}
public OptionBuilder<T> expectedValues(T ... expected) {
this.expectedValues.clear();
this.expectedValues.addAll(Arrays.asList(expected).stream().map(v -> v.toString()).collect(Collectors.toList()));
return this;
}
public Option<T> build() { public Option<T> build() {
if (auxiliaryType != null) {
return new MultiOption<T>(type, auxiliaryType, key, category, supportedRuntimes, build, description, defaultValue, expectedValues);
} else {
return new Option<T>(type, key, category, supportedRuntimes, build, description, defaultValue, expectedValues); return new Option<T>(type, key, category, supportedRuntimes, build, description, defaultValue, expectedValues);
} }
}
} }

View file

@ -0,0 +1,36 @@
package org.keycloak.config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class ProxyOptions {
public enum Mode {
none,
edge,
reencrypt,
passthrough;
}
public static final Option<Mode> proxy = new OptionBuilder<>("proxy", Mode.class)
.category(OptionCategory.PROXY)
.description("The proxy address forwarding mode if the server is behind a reverse proxy. " +
"Possible values are: " + String.join(",", Arrays.stream(Mode.values()).skip(1).map(m -> m.name()).collect(Collectors.joining(","))))
.defaultValue(Mode.none)
.expectedValues(Mode.values())
.build();
public static final Option<Boolean> proxyForwardedHost = new OptionBuilder<>("proxy-forwarded-host", Boolean.class)
.category(OptionCategory.PROXY)
.defaultValue(Boolean.FALSE)
.build();
public static final List<Option<?>> ALL_OPTIONS = new ArrayList<>();
static {
ALL_OPTIONS.add(proxy);
ALL_OPTIONS.add(proxyForwardedHost);
}
}

View file

@ -0,0 +1,21 @@
package org.keycloak.config;
import java.util.ArrayList;
import java.util.List;
public class TransactionOptions {
public static final Option<Boolean> TRANSACTION_XA_ENABLED = new OptionBuilder<>("transaction-xa-enabled", Boolean.class)
.category(OptionCategory.TRANSACTION)
.description("Manually override the transaction type. Transaction type XA and the appropriate driver is used by default.")
.buildTime(true)
.defaultValue(Boolean.TRUE)
.expectedValues(Boolean.TRUE, Boolean.FALSE)
.build();
public static final List<Option<?>> ALL_OPTIONS = new ArrayList<>();
static {
ALL_OPTIONS.add(TRANSACTION_XA_ENABLED);
}
}

View file

@ -0,0 +1,56 @@
package org.keycloak.config;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class VaultOptions {
public enum Provider {
file,
hashicorp;
}
public static final Option VAULT = new OptionBuilder<>("vault", Provider.class)
.category(OptionCategory.VAULT)
.description("Enables a vault provider.")
.buildTime(true)
.expectedValues(Provider.values())
.build();
public static final Option VAULT_DIR = new OptionBuilder<>("vault-dir", File.class)
.category(OptionCategory.VAULT)
.description("If set, secrets can be obtained by reading the content of files within the given directory.")
.build();
public static final Option VAULT_UNMAPPED = new OptionBuilder<>("vault-", String.class)
.category(OptionCategory.VAULT)
.description("Maps any vault option to their corresponding properties in quarkus-vault extension.")
.runtimes()
.buildTime(true)
.build();
public static final Option VAULT_URL = new OptionBuilder<>("vault-url", String.class)
.category(OptionCategory.VAULT)
.description("The vault server url.")
.runtimes()
.buildTime(true)
.build();
public static final Option VAULT_KV_PATHS = new OptionBuilder("vault-kv-paths", Map.class, String.class)
.category(OptionCategory.VAULT)
.description("A set of one or more key/value paths that should be used when looking up secrets.")
.runtimes()
.build();
public static final List<Option<?>> ALL_OPTIONS = new ArrayList<>();
static {
ALL_OPTIONS.add(VAULT);
ALL_OPTIONS.add(VAULT_DIR);
ALL_OPTIONS.add(VAULT_UNMAPPED);
ALL_OPTIONS.add(VAULT_URL);
ALL_OPTIONS.add(VAULT_KV_PATHS);
}
}

View file

@ -15,17 +15,18 @@
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.quarkus.runtime.storage.database; package org.keycloak.config.database;
import static java.util.Arrays.asList;
import java.io.File; import java.io.File;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
import static java.util.Arrays.asList;
public final class Database { public final class Database {
private static final Map<String, Vendor> DATABASES = new HashMap<>(); private static final Map<String, Vendor> DATABASES = new HashMap<>();
@ -96,7 +97,7 @@ public final class Database {
return DATABASES.keySet().stream().sorted().toArray(String[]::new); return DATABASES.keySet().stream().sorted().toArray(String[]::new);
} }
private enum Vendor { public enum Vendor {
H2("h2", H2("h2",
"org.h2.jdbcx.JdbcDataSource", "org.h2.jdbcx.JdbcDataSource",
"org.h2.Driver", "org.h2.Driver",
@ -187,5 +188,10 @@ public final class Database {
public boolean isOfKind(String dbKind) { public boolean isOfKind(String dbKind) {
return databaseKind.equals(dbKind); return databaseKind.equals(dbKind);
} }
@Override
public String toString() {
return databaseKind.toLowerCase(Locale.ROOT);
}
} }
} }

View file

@ -111,7 +111,7 @@
<!-- Keycloak --> <!-- Keycloak -->
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-common</artifactId> <artifactId>keycloak-config-api</artifactId>
</dependency> </dependency>
<!-- Test --> <!-- Test -->

View file

@ -43,9 +43,11 @@ public class ServerConfigGen {
field.addSingleMemberAnnotation( field.addSingleMemberAnnotation(
ANNOTATION_JSON_PROPERTY, ANNOTATION_JSON_PROPERTY,
new StringLiteralExpr(o.getKey())); new StringLiteralExpr(o.getKey()));
if (o.getDescription() != null) {
field.addSingleMemberAnnotation( field.addSingleMemberAnnotation(
ANNOTATION_JSON_PROPERTY_DESCRIPTION, ANNOTATION_JSON_PROPERTY_DESCRIPTION,
new StringLiteralExpr(StringEscapeUtils.escapeJava(o.getDescription()))); new StringLiteralExpr(StringEscapeUtils.escapeJava(o.getDescription())));
}
field.createGetter(); field.createGetter();
field.createSetter(); field.createSetter();
} }

View file

@ -81,7 +81,7 @@ class LiquibaseProcessor {
private void filterImplementations(Class<?> types, String dbKind, Set<ClassInfo> classes) { private void filterImplementations(Class<?> types, String dbKind, Set<ClassInfo> classes) {
if (Database.class.equals(types)) { if (Database.class.equals(types)) {
// removes unsupported databases // removes unsupported databases
classes.removeIf(classInfo -> !org.keycloak.quarkus.runtime.storage.database.Database.isLiquibaseDatabaseSupported(classInfo.name().toString(), dbKind)); classes.removeIf(classInfo -> !org.keycloak.config.database.Database.isLiquibaseDatabaseSupported(classInfo.name().toString(), dbKind));
} }
} }
} }

View file

@ -47,6 +47,7 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.eclipse.microprofile.config.spi.ConfigSource; import org.eclipse.microprofile.config.spi.ConfigSource;
import org.keycloak.config.MultiOption;
import org.keycloak.config.OptionCategory; import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.cli.command.Build; import org.keycloak.quarkus.runtime.cli.command.Build;
import org.keycloak.quarkus.runtime.cli.command.ImportRealmMixin; import org.keycloak.quarkus.runtime.cli.command.ImportRealmMixin;
@ -420,6 +421,9 @@ public final class Picocli {
if (mapper.getType() != null) { if (mapper.getType() != null) {
optBuilder.type(mapper.getType()); optBuilder.type(mapper.getType());
if (mapper.getOption() instanceof MultiOption) {
optBuilder.auxiliaryTypes(((MultiOption<?>) mapper.getOption()).getAuxiliaryType());
}
} else { } else {
optBuilder.type(String.class); optBuilder.type(String.class);
} }

View file

@ -1,13 +1,12 @@
package org.keycloak.quarkus.runtime.configuration.mappers; package org.keycloak.quarkus.runtime.configuration.mappers;
import java.util.Arrays; import org.keycloak.config.ClusteringOptions;
import java.util.function.BiFunction;
import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.Environment;
import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigSourceInterceptorContext;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
final class ClusteringPropertyMappers { final class ClusteringPropertyMappers {
private ClusteringPropertyMappers() { private ClusteringPropertyMappers() {
@ -15,31 +14,23 @@ final class ClusteringPropertyMappers {
public static PropertyMapper[] getClusteringPropertyMappers() { public static PropertyMapper[] getClusteringPropertyMappers() {
return new PropertyMapper[] { return new PropertyMapper[] {
builder().from("cache") fromOption(ClusteringOptions.CACHE)
.defaultValue("ispn")
.description("Defines the cache mechanism for high-availability. "
+ "By default, a 'ispn' cache is used to create a cluster between multiple server nodes. "
+ "A 'local' cache disables clustering and is intended for development and testing purposes.")
.paramLabel("type") .paramLabel("type")
.isBuildTimeProperty(true)
.expectedValues("local", "ispn")
.build(), .build(),
builder().from("cache-stack") fromOption(ClusteringOptions.CACHE_STACK)
.to("kc.spi-connections-infinispan-quarkus-stack") .to("kc.spi-connections-infinispan-quarkus-stack")
.description("Define the default stack to use for cluster communication and node discovery. This option only takes effect "
+ "if 'cache' is set to 'ispn'. Default: udp.")
.paramLabel("stack") .paramLabel("stack")
.isBuildTimeProperty(true)
.expectedValues(Arrays.asList("tcp", "udp", "kubernetes", "ec2", "azure", "google"))
.build(), .build(),
builder().from("cache-config-file") fromOption(ClusteringOptions.CACHE_CONFIG_FILE)
.mapFrom("cache") .mapFrom("cache")
.to("kc.spi-connections-infinispan-quarkus-config-file") .to("kc.spi-connections-infinispan-quarkus-config-file")
.description("Defines the file from which cache configuration should be loaded from. " .transformer(ClusteringPropertyMappers::resolveConfigFile)
+ "The configuration file is relative to the 'conf/' directory.") .paramLabel("file")
.transformer(new BiFunction<String, ConfigSourceInterceptorContext, String>() { .build()
@Override };
public String apply(String value, ConfigSourceInterceptorContext context) { }
private static String resolveConfigFile(String value, ConfigSourceInterceptorContext context) {
if ("local".equals(value)) { if ("local".equals(value)) {
return "cache-local.xml"; return "cache-local.xml";
} else if ("ispn".equals(value)) { } else if ("ispn".equals(value)) {
@ -57,14 +48,4 @@ final class ClusteringPropertyMappers {
return pathPrefix + value; return pathPrefix + value;
} }
})
.paramLabel("file")
.isBuildTimeProperty(true)
.build()
};
}
private static PropertyMapper.Builder builder() {
return PropertyMapper.builder(OptionCategory.CLUSTERING);
}
} }

View file

@ -3,14 +3,14 @@ package org.keycloak.quarkus.runtime.configuration.mappers;
import io.quarkus.datasource.common.runtime.DatabaseKind; import io.quarkus.datasource.common.runtime.DatabaseKind;
import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue; import io.smallrye.config.ConfigValue;
import org.keycloak.config.OptionCategory; import org.keycloak.config.DatabaseOptions;
import org.keycloak.quarkus.runtime.storage.database.Database; import org.keycloak.config.database.Database;
import java.util.Optional; import java.util.Optional;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import static java.util.Arrays.asList;
import static org.keycloak.quarkus.runtime.Messages.invalidDatabaseVendor; import static org.keycloak.quarkus.runtime.Messages.invalidDatabaseVendor;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
import static org.keycloak.quarkus.runtime.integration.QuarkusPlatform.addInitializationException; import static org.keycloak.quarkus.runtime.integration.QuarkusPlatform.addInitializationException;
final class DatabasePropertyMappers { final class DatabasePropertyMappers {
@ -19,105 +19,90 @@ final class DatabasePropertyMappers {
public static PropertyMapper[] getDatabasePropertyMappers() { public static PropertyMapper[] getDatabasePropertyMappers() {
return new PropertyMapper[] { return new PropertyMapper[] {
builder().from("db-dialect") fromOption(DatabaseOptions.DB_DIALECT)
.mapFrom("db") .mapFrom("db")
.to("quarkus.hibernate-orm.dialect") .to("quarkus.hibernate-orm.dialect")
.isBuildTimeProperty(true)
.transformer(DatabasePropertyMappers::transformDialect) .transformer(DatabasePropertyMappers::transformDialect)
.hidden(true)
.build(), .build(),
builder().from("db-driver") fromOption(DatabaseOptions.DB_DRIVER)
.mapFrom("db") .mapFrom("db")
.defaultValue(Database.getDriver("dev-file", true).get())
.to("quarkus.datasource.jdbc.driver") .to("quarkus.datasource.jdbc.driver")
.transformer(DatabasePropertyMappers::getXaOrNonXaDriver) .transformer(DatabasePropertyMappers.getXaOrNonXaDriver())
.hidden(true)
.build(), .build(),
builder().from("db"). fromOption(DatabaseOptions.DB)
to("quarkus.datasource.db-kind") .to("quarkus.datasource.db-kind")
.isBuildTimeProperty(true) .transformer(DatabasePropertyMappers::toDatabaseKind)
.transformer(toDatabaseKind())
.description("The database vendor. Possible values are: " + String.join(", ", Database.getAliases()))
.paramLabel("vendor") .paramLabel("vendor")
.expectedValues(asList(Database.getAliases()))
.build(), .build(),
builder().from("db-url") fromOption(DatabaseOptions.DB_URL)
.to("quarkus.datasource.jdbc.url") .to("quarkus.datasource.jdbc.url")
.mapFrom("db") .mapFrom("db")
.transformer((value, context) -> Database.getDefaultUrl(value).orElse(value)) .transformer(DatabasePropertyMappers.getDatabaseUrl())
.description("The full database JDBC URL. If not provided, a default URL is set based on the selected database vendor. " +
"For instance, if using 'postgres', the default JDBC URL would be 'jdbc:postgresql://localhost/keycloak'. ")
.paramLabel("jdbc-url") .paramLabel("jdbc-url")
.build(), .build(),
builder().from("db-url-host") fromOption(DatabaseOptions.DB_URL_HOST)
.to("kc.db-url-host") .to("kc.db-url-host")
.description("Sets the hostname of the default JDBC URL of the chosen vendor. If the `db-url` option is set, this option is ignored.")
.paramLabel("hostname") .paramLabel("hostname")
.build(), .build(),
builder().from("db-url-database") fromOption(DatabaseOptions.DB_URL_DATABASE)
.to("kc.db-url-database") .to("kc.db-url-database")
.description("Sets the database name of the default JDBC URL of the chosen vendor. If the `db-url` option is set, this option is ignored.")
.paramLabel("dbname") .paramLabel("dbname")
.build(), .build(),
builder().from("db-url-port") fromOption(DatabaseOptions.DB_URL_PORT)
.to("kc.db-url-port") .to("kc.db-url-port")
.description("Sets the port of the default JDBC URL of the chosen vendor. If the `db-url` option is set, this option is ignored.")
.paramLabel("port") .paramLabel("port")
.build(), .build(),
builder().from("db-url-properties") fromOption(DatabaseOptions.DB_URL_PROPERTIES)
.to("kc.db-url-properties") .to("kc.db-url-properties")
.description("Sets the properties of the default JDBC URL of the chosen vendor. If the `db-url` option is set, this option is ignored.")
.paramLabel("properties") .paramLabel("properties")
.build(), .build(),
builder().from("db-username") fromOption(DatabaseOptions.DB_USERNAME)
.to("quarkus.datasource.username") .to("quarkus.datasource.username")
.mapFrom("db") .mapFrom("db")
.transformer(DatabasePropertyMappers::resolveUsername) .transformer(DatabasePropertyMappers::resolveUsername)
.description("The username of the database user.")
.paramLabel("username") .paramLabel("username")
.build(), .build(),
builder().from("db-password") fromOption(DatabaseOptions.DB_PASSWORD)
.to("quarkus.datasource.password") .to("quarkus.datasource.password")
.mapFrom("db") .mapFrom("db")
.transformer(DatabasePropertyMappers::resolvePassword) .transformer(DatabasePropertyMappers::resolvePassword)
.description("The password of the database user.")
.paramLabel("password") .paramLabel("password")
.isMasked(true) .isMasked(true)
.build(), .build(),
builder().from("db-schema") fromOption(DatabaseOptions.DB_SCHEMA)
.to("quarkus.hibernate-orm.database.default-schema") .to("quarkus.hibernate-orm.database.default-schema")
.description("The database schema to be used.")
.paramLabel("schema") .paramLabel("schema")
.build(), .build(),
builder().from("db-pool-initial-size") fromOption(DatabaseOptions.DB_POOL_INITIAL_SIZE)
.to("quarkus.datasource.jdbc.initial-size") .to("quarkus.datasource.jdbc.initial-size")
.description("The initial size of the connection pool.")
.paramLabel("size") .paramLabel("size")
.build(), .build(),
builder().from("db-pool-min-size") fromOption(DatabaseOptions.DB_POOL_MIN_SIZE)
.to("quarkus.datasource.jdbc.min-size") .to("quarkus.datasource.jdbc.min-size")
.description("The minimal size of the connection pool.")
.paramLabel("size") .paramLabel("size")
.build(), .build(),
builder().from("db-pool-max-size") fromOption(DatabaseOptions.DB_POOL_MAX_SIZE)
.to("quarkus.datasource.jdbc.max-size") .to("quarkus.datasource.jdbc.max-size")
.defaultValue(String.valueOf(100))
.description("The maximum size of the connection pool.")
.paramLabel("size") .paramLabel("size")
.build() .build()
}; };
} }
private static String getXaOrNonXaDriver(String db, ConfigSourceInterceptorContext context) { private static BiFunction<String, ConfigSourceInterceptorContext, String> getDatabaseUrl() {
return (s, c) -> Database.getDefaultUrl(s).orElse(s);
}
private static BiFunction<String, ConfigSourceInterceptorContext, String> getXaOrNonXaDriver() {
return (String db, ConfigSourceInterceptorContext context) -> {
ConfigValue xaEnabledConfigValue = context.proceed("kc.transaction-xa-enabled"); ConfigValue xaEnabledConfigValue = context.proceed("kc.transaction-xa-enabled");
boolean isXaEnabled = xaEnabledConfigValue == null || Boolean.parseBoolean(xaEnabledConfigValue.getValue()); boolean isXaEnabled = xaEnabledConfigValue == null || Boolean.parseBoolean(xaEnabledConfigValue.getValue());
return Database.getDriver(db, isXaEnabled).orElse(db); return Database.getDriver(db, isXaEnabled).orElse(db);
};
} }
private static BiFunction<String, ConfigSourceInterceptorContext, String> toDatabaseKind() { private static String toDatabaseKind(String db, ConfigSourceInterceptorContext context) {
return (db, context) -> {
Optional<String> databaseKind = Database.getDatabaseKind(db); Optional<String> databaseKind = Database.getDatabaseKind(db);
if (databaseKind.isPresent()) { if (databaseKind.isPresent()) {
@ -127,11 +112,6 @@ final class DatabasePropertyMappers {
addInitializationException(invalidDatabaseVendor(db, Database.getAliases())); addInitializationException(invalidDatabaseVendor(db, Database.getAliases()));
return "h2"; return "h2";
};
}
private static <T> PropertyMapper.Builder<T> builder() {
return PropertyMapper.builder(OptionCategory.DATABASE);
} }
private static String resolveUsername(String value, ConfigSourceInterceptorContext context) { private static String resolveUsername(String value, ConfigSourceInterceptorContext context) {

View file

@ -1,9 +1,8 @@
package org.keycloak.quarkus.runtime.configuration.mappers; package org.keycloak.quarkus.runtime.configuration.mappers;
import java.util.ArrayList; import org.keycloak.config.FeatureOptions;
import java.util.List;
import org.keycloak.common.Profile; import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
import org.keycloak.config.OptionCategory;
final class FeaturePropertyMappers { final class FeaturePropertyMappers {
@ -12,34 +11,13 @@ final class FeaturePropertyMappers {
public static PropertyMapper[] getMappers() { public static PropertyMapper[] getMappers() {
return new PropertyMapper[] { return new PropertyMapper[] {
builder() fromOption(FeatureOptions.FEATURES)
.from("features")
.description("Enables a set of one or more features.")
.expectedValues(getFeatureValues())
.paramLabel("feature") .paramLabel("feature")
.build(), .build(),
builder() fromOption(FeatureOptions.FEATURES_DISABLED)
.from("features-disabled")
.expectedValues(getFeatureValues())
.paramLabel("feature") .paramLabel("feature")
.description("Disables a set of one or more features.")
.build() .build()
}; };
} }
private static List<String> getFeatureValues() {
List<String> features = new ArrayList<>();
for (Profile.Feature value : Profile.Feature.values()) {
features.add(value.name().toLowerCase().replace('_', '-'));
}
features.add(Profile.Type.PREVIEW.name().toLowerCase());
return features;
}
private static PropertyMapper.Builder builder() {
return PropertyMapper.builder(OptionCategory.FEATURE).isBuildTimeProperty(true);
}
} }

View file

@ -1,8 +1,8 @@
package org.keycloak.quarkus.runtime.configuration.mappers; package org.keycloak.quarkus.runtime.configuration.mappers;
import org.keycloak.config.OptionCategory; import org.keycloak.config.HealthOptions;
import java.util.Arrays; import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
final class HealthPropertyMappers { final class HealthPropertyMappers {
@ -11,18 +11,11 @@ final class HealthPropertyMappers {
public static PropertyMapper[] getHealthPropertyMappers() { public static PropertyMapper[] getHealthPropertyMappers() {
return new PropertyMapper[] { return new PropertyMapper[] {
builder().from("health-enabled") fromOption(HealthOptions.HEALTH_ENABLED)
.to("quarkus.datasource.health.enabled") .to("quarkus.datasource.health.enabled")
.isBuildTimeProperty(true)
.defaultValue(Boolean.FALSE.toString())
.description("If the server should expose health check endpoints. If enabled, health checks are available at the '/health', '/health/ready' and '/health/live' endpoints.")
.paramLabel(Boolean.TRUE + "|" + Boolean.FALSE) .paramLabel(Boolean.TRUE + "|" + Boolean.FALSE)
.expectedValues(Arrays.asList(Boolean.TRUE.toString(), Boolean.FALSE.toString()))
.build() .build()
}; };
} }
private static PropertyMapper.Builder builder() {
return PropertyMapper.builder(OptionCategory.HEALTH);
}
} }

View file

@ -1,7 +1,8 @@
package org.keycloak.quarkus.runtime.configuration.mappers; package org.keycloak.quarkus.runtime.configuration.mappers;
import org.keycloak.config.HostnameOptions;
import org.keycloak.config.OptionCategory; import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
final class HostnamePropertyMappers { final class HostnamePropertyMappers {
@ -9,49 +10,32 @@ final class HostnamePropertyMappers {
public static PropertyMapper[] getHostnamePropertyMappers() { public static PropertyMapper[] getHostnamePropertyMappers() {
return new PropertyMapper[] { return new PropertyMapper[] {
builder().from("hostname") fromOption(HostnameOptions.HOSTNAME)
.to("kc.spi-hostname-default-hostname") .to("kc.spi-hostname-default-hostname")
.description("Hostname for the Keycloak server.")
.paramLabel("hostname") .paramLabel("hostname")
.build(), .build(),
builder().from("hostname-admin") fromOption(HostnameOptions.HOSTNAME_ADMIN)
.to("kc.spi-hostname-default-admin") .to("kc.spi-hostname-default-admin")
.description("The hostname for accessing the administration console. Use this option if you are exposing the administration console using a hostname other than the value set to the 'hostname' option.")
.paramLabel("hostname") .paramLabel("hostname")
.build(), .build(),
builder().from("hostname-strict") fromOption(HostnameOptions.HOSTNAME_STRICT)
.to("kc.spi-hostname-default-strict") .to("kc.spi-hostname-default-strict")
.description("Disables dynamically resolving the hostname from request headers. Should always be set to true in production, unless proxy verifies the Host header.")
.type(Boolean.class)
.defaultValue(Boolean.TRUE.toString())
.build(), .build(),
builder().from("hostname-strict-https") fromOption(HostnameOptions.HOSTNAME_STRICT_HTTPS)
.to("kc.spi-hostname-default-strict-https") .to("kc.spi-hostname-default-strict-https")
.description("Forces URLs to use HTTPS. Only needed if proxy does not properly set the X-Forwarded-Proto header.")
.hidden(true)
.defaultValue(Boolean.TRUE.toString())
.type(Boolean.class)
.build(), .build(),
builder().from("hostname-strict-backchannel") fromOption(HostnameOptions.HOSTNAME_STRICT_BACKCHANNEL)
.to("kc.spi-hostname-default-strict-backchannel") .to("kc.spi-hostname-default-strict-backchannel")
.description("By default backchannel URLs are dynamically resolved from request headers to allow internal and external applications. If all applications use the public URL this option should be enabled.")
.type(Boolean.class)
.build(), .build(),
builder().from("hostname-path") fromOption(HostnameOptions.HOSTNAME_PATH)
.to("kc.spi-hostname-default-path") .to("kc.spi-hostname-default-path")
.description("This should be set if proxy uses a different context-path for Keycloak.")
.paramLabel("path") .paramLabel("path")
.build(), .build(),
builder().from("hostname-port") fromOption(HostnameOptions.HOSTNAME_PORT)
.to("kc.spi-hostname-default-hostname-port") .to("kc.spi-hostname-default-hostname-port")
.defaultValue("-1")
.description("The port used by the proxy when exposing the hostname. Set this option if the proxy uses a port other than the default HTTP and HTTPS ports.")
.paramLabel("port") .paramLabel("port")
.build() .build()
}; };
} }
private static PropertyMapper.Builder builder() {
return PropertyMapper.builder(OptionCategory.HOSTNAME);
}
} }

View file

@ -3,15 +3,12 @@ package org.keycloak.quarkus.runtime.configuration.mappers;
import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue; import io.smallrye.config.ConfigValue;
import org.keycloak.config.HttpOptions; import org.keycloak.config.HttpOptions;
import org.keycloak.config.Option;
import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.Messages; import org.keycloak.quarkus.runtime.Messages;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import java.io.File; import java.io.File;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Arrays;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption; import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers.getMapper; import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers.getMapper;
@ -23,103 +20,74 @@ final class HttpPropertyMappers {
public static PropertyMapper[] getHttpPropertyMappers() { public static PropertyMapper[] getHttpPropertyMappers() {
return new PropertyMapper[] { return new PropertyMapper[] {
builder().from("http-enabled") fromOption(HttpOptions.HTTP_ENABLED)
.to("quarkus.http.insecure-requests") .to("quarkus.http.insecure-requests")
.defaultValue(Boolean.FALSE.toString())
.transformer(HttpPropertyMappers::getHttpEnabledTransformer) .transformer(HttpPropertyMappers::getHttpEnabledTransformer)
.description("Enables the HTTP listener.")
.paramLabel(Boolean.TRUE + "|" + Boolean.FALSE) .paramLabel(Boolean.TRUE + "|" + Boolean.FALSE)
.expectedValues(Arrays.asList(Boolean.TRUE.toString(), Boolean.FALSE.toString()))
.build(), .build(),
builder().from("http-host") fromOption(HttpOptions.HTTP_HOST)
.to("quarkus.http.host") .to("quarkus.http.host")
.defaultValue("0.0.0.0")
.description("The used HTTP Host.")
.paramLabel("host") .paramLabel("host")
.build(), .build(),
builder().from("http-relative-path") fromOption(HttpOptions.HTTP_RELATIVE_PATH)
.to("quarkus.http.root-path") .to("quarkus.http.root-path")
.defaultValue("/")
.description("Set the path relative to '/' for serving resources.")
.paramLabel("path") .paramLabel("path")
.isBuildTimeProperty(true)
.build(), .build(),
fromOption(HttpOptions.httpPort) fromOption(HttpOptions.HTTP_PORT)
.to("quarkus.http.port") .to("quarkus.http.port")
.paramLabel("port") .paramLabel("port")
.build(), .build(),
builder().from("https-port") fromOption(HttpOptions.HTTPS_PORT)
.to("quarkus.http.ssl-port") .to("quarkus.http.ssl-port")
.defaultValue(String.valueOf(8443))
.description("The used HTTPS port.")
.paramLabel("port") .paramLabel("port")
.build(), .build(),
builder().from("https-client-auth") fromOption(HttpOptions.HTTPS_CLIENT_AUTH)
.to("quarkus.http.ssl.client-auth") .to("quarkus.http.ssl.client-auth")
.defaultValue("none")
.description("Configures the server to require/request client authentication. Possible Values: none, request, required.")
.paramLabel("auth") .paramLabel("auth")
.expectedValues(Arrays.asList("none", "request", "required"))
.build(), .build(),
builder().from("https-cipher-suites") fromOption(HttpOptions.HTTPS_CIPHER_SUITES)
.to("quarkus.http.ssl.cipher-suites") .to("quarkus.http.ssl.cipher-suites")
.description("The cipher suites to use. If none is given, a reasonable default is selected.")
.paramLabel("ciphers") .paramLabel("ciphers")
.build(), .build(),
builder().from("https-protocols") fromOption(HttpOptions.HTTPS_PROTOCOLS)
.to("quarkus.http.ssl.protocols") .to("quarkus.http.ssl.protocols")
.description("The list of protocols to explicitly enable.")
.paramLabel("protocols") .paramLabel("protocols")
.defaultValue("TLSv1.3")
.build(), .build(),
builder().from("https-certificate-file") fromOption(HttpOptions.HTTPS_CERTIFICATE_FILE)
.to("quarkus.http.ssl.certificate.file") .to("quarkus.http.ssl.certificate.file")
.description("The file path to a server certificate or certificate chain in PEM format.")
.paramLabel("file") .paramLabel("file")
.build(), .build(),
builder().from("https-certificate-key-file") fromOption(HttpOptions.HTTPS_CERTIFICATE_KEY_FILE)
.to("quarkus.http.ssl.certificate.key-file") .to("quarkus.http.ssl.certificate.key-file")
.description("The file path to a private key in PEM format.")
.paramLabel("file") .paramLabel("file")
.build(), .build(),
builder().from("https-key-store-file") fromOption(HttpOptions.HTTPS_KEY_STORE_FILE
.withRuntimeSpecificDefault(getDefaultKeystorePathValue()))
.to("quarkus.http.ssl.certificate.key-store-file") .to("quarkus.http.ssl.certificate.key-store-file")
.defaultValue(getDefaultKeystorePathValue())
.description("The key store which holds the certificate information instead of specifying separate files.")
.paramLabel("file") .paramLabel("file")
.build(), .build(),
builder().from("https-key-store-password") fromOption(HttpOptions.HTTPS_KEY_STORE_PASSWORD)
.to("quarkus.http.ssl.certificate.key-store-password") .to("quarkus.http.ssl.certificate.key-store-password")
.description("The password of the key store file.")
.defaultValue("password")
.paramLabel("password") .paramLabel("password")
.isMasked(true) .isMasked(true)
.build(), .build(),
builder().from("https-key-store-type") fromOption(HttpOptions.HTTPS_KEY_STORE_TYPE)
.to("quarkus.http.ssl.certificate.key-store-file-type") .to("quarkus.http.ssl.certificate.key-store-file-type")
.description("The type of the key store file. " +
"If not given, the type is automatically detected based on the file name.")
.paramLabel("type") .paramLabel("type")
.build(), .build(),
builder().from("https-trust-store-file") fromOption(HttpOptions.HTTPS_TRUST_STORE_FILE)
.to("quarkus.http.ssl.certificate.trust-store-file") .to("quarkus.http.ssl.certificate.trust-store-file")
.description("The trust store which holds the certificate information of the certificates to trust.")
.paramLabel("file") .paramLabel("file")
.build(), .build(),
builder().from("https-trust-store-password") fromOption(HttpOptions.HTTPS_TRUST_STORE_PASSWORD)
.to("quarkus.http.ssl.certificate.trust-store-password") .to("quarkus.http.ssl.certificate.trust-store-password")
.description("The password of the trust store file.")
.paramLabel("password") .paramLabel("password")
.isMasked(true) .isMasked(true)
.build(), .build(),
builder().from("https-trust-store-type") fromOption(HttpOptions.HTTPS_TRUST_STORE_TYPE)
.to("quarkus.http.ssl.certificate.trust-store-file-type") .to("quarkus.http.ssl.certificate.trust-store-file-type")
.defaultValue(getDefaultKeystorePathValue())
.description("The type of the trust store file. " +
"If not given, the type is automatically detected based on the file name.")
.paramLabel("type") .paramLabel("type")
.build() .build()
}; };
} }
@ -161,6 +129,5 @@ final class HttpPropertyMappers {
return null; return null;
} }
private static <T> PropertyMapper.Builder<T> builder() { return PropertyMapper.<T> builder(OptionCategory.HTTP); }
} }

View file

@ -1,44 +1,123 @@
package org.keycloak.quarkus.runtime.configuration.mappers; package org.keycloak.quarkus.runtime.configuration.mappers;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
import static org.keycloak.quarkus.runtime.integration.QuarkusPlatform.addInitializationException; import static org.keycloak.quarkus.runtime.integration.QuarkusPlatform.addInitializationException;
import java.io.File; import java.io.File;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors;
import org.jboss.logmanager.LogContext; import org.jboss.logmanager.LogContext;
import org.keycloak.config.OptionCategory; import org.keycloak.config.LoggingOptions;
import org.keycloak.quarkus.runtime.Messages; import org.keycloak.quarkus.runtime.Messages;
import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigSourceInterceptorContext;
public final class LoggingPropertyMappers { public final class LoggingPropertyMappers {
private static final String DEFAULT_LOG_LEVEL = "info";
private static final String DEFAULT_LOG_HANDLER = "console";
private static final String DEFAULT_LOG_FILENAME = "keycloak.log";
public static final String DEFAULT_LOG_PATH = "data" + File.separator + "log" + File.separator + DEFAULT_LOG_FILENAME;
private static final List<String> AVAILABLE_LOG_HANDLERS = List.of(DEFAULT_LOG_HANDLER,"file");
private static final String DEFAULT_CONSOLE_OUTPUT = "default";
private LoggingPropertyMappers(){} private LoggingPropertyMappers(){}
public static PropertyMapper[] getMappers() { public static PropertyMapper[] getMappers() {
return new PropertyMapper[] { return new PropertyMapper[] {
builder().from("log") fromOption(LoggingOptions.log)
.defaultValue(DEFAULT_LOG_HANDLER)
.description("Enable one or more log handlers in a comma-separated list. Available log handlers are: " + String.join(",", AVAILABLE_LOG_HANDLERS))
.paramLabel("<handler>") .paramLabel("<handler>")
.expectedValues("console","file","console,file","file,console")
.build(), .build(),
builder().from("log-level") fromOption(LoggingOptions.LOG_CONSOLE_OUTPUT)
.to("quarkus.log.console.json")
.paramLabel("default|json")
.transformer((value, context) -> {
if(value.equals(LoggingOptions.DEFAULT_CONSOLE_OUTPUT.name().toLowerCase(Locale.ROOT))) {
return Boolean.FALSE.toString();
}
return Boolean.TRUE.toString();
})
.build(),
fromOption(LoggingOptions.LOG_CONSOLE_FORMAT)
.to("quarkus.log.console.format")
.paramLabel("format")
.build(),
fromOption(LoggingOptions.LOG_CONSOLE_COLOR)
.to("quarkus.log.console.color")
.paramLabel(Boolean.TRUE + "|" + Boolean.FALSE)
.build(),
fromOption(LoggingOptions.LOG_CONSOLE_ENABLED)
.mapFrom("log")
.to("quarkus.log.console.enable")
.transformer(LoggingPropertyMappers.resolveLogHandler(LoggingOptions.DEFAULT_LOG_HANDLER.name()))
.build(),
fromOption(LoggingOptions.LOG_FILE_ENABLED)
.mapFrom("log")
.to("quarkus.log.file.enable")
.transformer(LoggingPropertyMappers.resolveLogHandler("file"))
.build(),
fromOption(LoggingOptions.LOG_FILE)
.to("quarkus.log.file.path")
.paramLabel("<path>/<file-name>.log")
.transformer(LoggingPropertyMappers::resolveFileLogLocation)
.build(),
fromOption(LoggingOptions.LOG_FILE_FORMAT)
.to("quarkus.log.file.format")
.paramLabel("<format>")
.build(),
fromOption(LoggingOptions.LOG_LEVEL)
.to("quarkus.log.level") .to("quarkus.log.level")
.transformer(new BiFunction<String, ConfigSourceInterceptorContext, String>() { .transformer(LoggingPropertyMappers::resolveLogLevel)
@Override .paramLabel("category:level")
public String apply(String value, ConfigSourceInterceptorContext configSourceInterceptorContext) { .build()
String rootLevel = DEFAULT_LOG_LEVEL; };
}
private static BiFunction<String, ConfigSourceInterceptorContext, String> resolveLogHandler(String handler) {
return (parentValue, context) -> {
//we want to fall back to console to not have nothing shown up when wrong values are set.
String consoleDependantErrorResult = handler.equals(LoggingOptions.DEFAULT_LOG_HANDLER.name()) ? Boolean.TRUE.toString() : Boolean.FALSE.toString();
if(parentValue.isBlank()) {
addInitializationException(Messages.emptyValueForKey("log"));
return consoleDependantErrorResult;
}
String[] logHandlerValues = parentValue.split(",");
List<String> availableLogHandlers = Arrays.stream(LoggingOptions.Handler.values()).map(h -> h.name()).collect(Collectors.toList());
if (!availableLogHandlers.containsAll(List.of(logHandlerValues))) {
addInitializationException(Messages.notRecognizedValueInList("log", parentValue, String.join(",", availableLogHandlers)));
return consoleDependantErrorResult;
}
for (String handlerInput : logHandlerValues) {
if (handlerInput.equals(handler)) {
return Boolean.TRUE.toString();
}
}
return Boolean.FALSE.toString();
};
}
private static String resolveFileLogLocation(String value, ConfigSourceInterceptorContext configSourceInterceptorContext) {
if (value.endsWith(File.separator)) {
return value + LoggingOptions.DEFAULT_LOG_FILENAME;
}
return value;
}
private static Level toLevel(String categoryLevel) throws IllegalArgumentException {
return LogContext.getLogContext().getLevelForName(categoryLevel.toUpperCase(Locale.ROOT));
}
private static void setCategoryLevel(String category, String level) {
LogContext.getLogContext().getLogger(category).setLevel(toLevel(level));
}
private static String resolveLogLevel(String value, ConfigSourceInterceptorContext configSourceInterceptorContext) {
String rootLevel = LoggingOptions.DEFAULT_LOG_LEVEL.name();
for (String level : value.split(",")) { for (String level : value.split(",")) {
String[] parts = level.split(":"); String[] parts = level.split(":");
@ -73,110 +152,4 @@ public final class LoggingPropertyMappers {
return rootLevel; return rootLevel;
} }
})
.defaultValue(DEFAULT_LOG_LEVEL)
.description("The log level of the root category or a comma-separated list of individual categories and their levels. For the root category, you don't need to specify a category.")
.paramLabel("category:level")
.build(),
builder().from("log-console-output")
.to("quarkus.log.console.json")
.defaultValue(DEFAULT_CONSOLE_OUTPUT)
.description("Set the log output to JSON or default (plain) unstructured logging.")
.paramLabel("default|json")
.expectedValues(DEFAULT_CONSOLE_OUTPUT,"json")
.transformer((value, context) -> {
if(value.equals(DEFAULT_CONSOLE_OUTPUT)) {
return Boolean.FALSE.toString();
}
return Boolean.TRUE.toString();
})
.build(),
builder().from("log-console-format")
.to("quarkus.log.console.format")
.defaultValue("%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n")
.description("The format of unstructured console log entries. If the format has spaces in it, escape the value using \"<format>\".")
.paramLabel("format")
.build(),
builder().from("log-console-color")
.to("quarkus.log.console.color")
.defaultValue(Boolean.FALSE.toString())
.description("Enable or disable colors when logging to console.")
.paramLabel(Boolean.TRUE + "|" + Boolean.FALSE)
.build(),
builder().from("log-console-enabled")
.mapFrom("log")
.to("quarkus.log.console.enable")
.hidden(true)
.transformer(resolveLogHandler(DEFAULT_LOG_HANDLER))
.build(),
builder().from("log-file-enabled")
.mapFrom("log")
.to("quarkus.log.file.enable")
.hidden(true)
.transformer(resolveLogHandler("file"))
.build(),
builder().from("log-file")
.to("quarkus.log.file.path")
.defaultValue(DEFAULT_LOG_PATH)
.description("Set the log file path and filename.")
.paramLabel("<path>/<file-name>.log")
.transformer(LoggingPropertyMappers::resolveFileLogLocation)
.build(),
builder().from("log-file-format")
.to("quarkus.log.file.format")
.defaultValue("%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n")
.description("Set a format specific to file log entries.")
.paramLabel("<format>")
.build()
};
}
private static BiFunction<String, ConfigSourceInterceptorContext, String> resolveLogHandler(String handler) {
return (parentValue, context) -> {
//we want to fall back to console to not have nothing shown up when wrong values are set.
String consoleDependantErrorResult = handler.equals(DEFAULT_LOG_HANDLER) ? Boolean.TRUE.toString() : Boolean.FALSE.toString();
if(parentValue.isBlank()) {
addInitializationException(Messages.emptyValueForKey("log"));
return consoleDependantErrorResult;
}
String[] logHandlerValues = parentValue.split(",");
if (!AVAILABLE_LOG_HANDLERS.containsAll(List.of(logHandlerValues))) {
addInitializationException(Messages.notRecognizedValueInList("log", parentValue, String.join(",", AVAILABLE_LOG_HANDLERS)));
return consoleDependantErrorResult;
}
for (String handlerInput : logHandlerValues) {
if (handlerInput.equals(handler)) {
return Boolean.TRUE.toString();
}
}
return Boolean.FALSE.toString();
};
}
private static String resolveFileLogLocation(String value, ConfigSourceInterceptorContext configSourceInterceptorContext) {
if (value.endsWith(File.separator))
{
return value + DEFAULT_LOG_FILENAME;
}
return value;
}
private static Level toLevel(String categoryLevel) throws IllegalArgumentException {
return LogContext.getLogContext().getLevelForName(categoryLevel.toUpperCase(Locale.ROOT));
}
private static void setCategoryLevel(String category, String level) {
LogContext.getLogContext().getLogger(category).setLevel(toLevel(level));
}
private static <T> PropertyMapper.Builder<T> builder() {
return PropertyMapper.builder(OptionCategory.LOGGING);
}
} }

View file

@ -1,8 +1,8 @@
package org.keycloak.quarkus.runtime.configuration.mappers; package org.keycloak.quarkus.runtime.configuration.mappers;
import org.keycloak.config.OptionCategory; import org.keycloak.config.MetricsOptions;
import java.util.Arrays; import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
final class MetricsPropertyMappers { final class MetricsPropertyMappers {
@ -11,18 +11,11 @@ final class MetricsPropertyMappers {
public static PropertyMapper[] getMetricsPropertyMappers() { public static PropertyMapper[] getMetricsPropertyMappers() {
return new PropertyMapper[] { return new PropertyMapper[] {
builder().from("metrics-enabled") fromOption(MetricsOptions.METRICS_ENABLED)
.to("quarkus.datasource.metrics.enabled") .to("quarkus.datasource.metrics.enabled")
.isBuildTimeProperty(true)
.defaultValue(Boolean.FALSE.toString())
.description("If the server should expose metrics. If enabled, metrics are available at the '/metrics' endpoint.")
.paramLabel(Boolean.TRUE + "|" + Boolean.FALSE) .paramLabel(Boolean.TRUE + "|" + Boolean.FALSE)
.expectedValues(Arrays.asList(Boolean.TRUE.toString(), Boolean.FALSE.toString()))
.build() .build()
}; };
} }
private static PropertyMapper.Builder builder() {
return PropertyMapper.builder(OptionCategory.METRICS);
}
} }

View file

@ -22,13 +22,8 @@ import static org.keycloak.quarkus.runtime.configuration.Configuration.OPTION_PA
import static org.keycloak.quarkus.runtime.configuration.Configuration.toCliFormat; import static org.keycloak.quarkus.runtime.configuration.Configuration.toCliFormat;
import static org.keycloak.quarkus.runtime.configuration.Configuration.toEnvVarFormat; import static org.keycloak.quarkus.runtime.configuration.Configuration.toEnvVarFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -42,8 +37,13 @@ import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
public class PropertyMapper<T> { public class PropertyMapper<T> {
static PropertyMapper IDENTITY = new PropertyMapper(String.class, null, null, Optional.empty(), null, null, static PropertyMapper IDENTITY = new PropertyMapper(
false,null, null, false,Collections.emptyList(),null, true) { new OptionBuilder<String>(null, String.class).build(),
null,
null,
null,
null,
false) {
@Override @Override
public ConfigValue getConfigValue(String name, ConfigSourceInterceptorContext context) { public ConfigValue getConfigValue(String name, ConfigSourceInterceptorContext context) {
return context.proceed(name); return context.proceed(name);
@ -55,43 +55,20 @@ public class PropertyMapper<T> {
private final BiFunction<String, ConfigSourceInterceptorContext, String> mapper; private final BiFunction<String, ConfigSourceInterceptorContext, String> mapper;
private final String mapFrom; private final String mapFrom;
private final boolean mask; private final boolean mask;
private final List<T> expectedValues;
private final String paramLabel; private final String paramLabel;
private final String envVarFormat; private final String envVarFormat;
private String cliFormat; private String cliFormat;
// Backward compatible constructor PropertyMapper(Option<T> option, String to, BiFunction<String, ConfigSourceInterceptorContext, String> mapper,
PropertyMapper(Class<T> type, String from, String to, Optional<T> defaultValue, BiFunction<String, ConfigSourceInterceptorContext, String> mapper, String mapFrom, String paramLabel, boolean mask) {
String mapFrom, boolean buildTime, String description, String paramLabel, boolean mask, List<T> expectedValues, this.option = option;
OptionCategory category, boolean hidden) { this.to = to == null ? getFrom() : to;
Set<Option.Runtime> runtimes = new HashSet<>();
if (!hidden) {
runtimes.add(Option.Runtime.QUARKUS);
}
this.option = new OptionBuilder<T>(MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + from, type)
.buildTime(buildTime)
.category(category != null ? category : OptionCategory.GENERAL)
.defaultValue(defaultValue)
.description(description)
.expectedValues(expectedValues)
.runtimes(runtimes)
.build();
this.to = to == null ? option.getKey() : to;
this.mapper = mapper == null ? PropertyMapper::defaultTransformer : mapper; this.mapper = mapper == null ? PropertyMapper::defaultTransformer : mapper;
this.mapFrom = mapFrom; this.mapFrom = mapFrom;
this.paramLabel = paramLabel; this.paramLabel = paramLabel;
this.mask = mask; this.mask = mask;
this.expectedValues = expectedValues == null ? Collections.emptyList() : expectedValues; this.cliFormat = toCliFormat(option.getKey());
this.cliFormat = toCliFormat(from); this.envVarFormat = toEnvVarFormat(getFrom());
this.envVarFormat = toEnvVarFormat(option.getKey());
}
public static PropertyMapper.Builder builder(String fromProp, String toProp) {
return new PropertyMapper.Builder(fromProp, toProp);
}
public static PropertyMapper.Builder builder(OptionCategory category) {
return new PropertyMapper.Builder(category);
} }
private static String defaultTransformer(String value, ConfigSourceInterceptorContext context) { private static String defaultTransformer(String value, ConfigSourceInterceptorContext context) {
@ -103,7 +80,7 @@ public class PropertyMapper<T> {
} }
ConfigValue getConfigValue(String name, ConfigSourceInterceptorContext context) { ConfigValue getConfigValue(String name, ConfigSourceInterceptorContext context) {
String from = this.option.getKey(); String from = getFrom();
if (to != null && to.endsWith(OPTION_PART_SEPARATOR)) { if (to != null && to.endsWith(OPTION_PART_SEPARATOR)) {
// in case mapping is based on prefixes instead of full property names // in case mapping is based on prefixes instead of full property names
@ -166,16 +143,18 @@ public class PropertyMapper<T> {
return value; return value;
} }
public Option<T> getOption() { return this.option; }
public Class<T> getType() { return this.option.getType(); } public Class<T> getType() { return this.option.getType(); }
public String getFrom() { public String getFrom() {
return this.option.getKey(); return MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + this.option.getKey();
} }
public String getDescription() { return this.option.getDescription(); } public String getDescription() { return this.option.getDescription(); }
public List<T> getExpectedValues() { public List<String> getExpectedValues() {
return expectedValues; return this.option.getExpectedValues().stream().map(v -> v.toString()).collect(Collectors.toList());
} }
public Optional<T> getDefaultValue() {return this.option.getDefaultValue(); } public Optional<T> getDefaultValue() {return this.option.getDefaultValue(); }
@ -236,32 +215,15 @@ public class PropertyMapper<T> {
public static class Builder<T> { public static class Builder<T> {
private Class<T> type; private final Option<T> option;
private String from;
private String to; private String to;
private T defaultValue;
private BiFunction<String, ConfigSourceInterceptorContext, String> mapper; private BiFunction<String, ConfigSourceInterceptorContext, String> mapper;
private String description;
private String mapFrom = null; private String mapFrom = null;
private List<T> expectedValues = new ArrayList<>();
private boolean isBuildTimeProperty = false;
private boolean isMasked = false; private boolean isMasked = false;
private OptionCategory category = OptionCategory.GENERAL;
private String paramLabel; private String paramLabel;
private boolean hidden;
public Builder(OptionCategory category) { public Builder(Option<T> option) {
this.category = category; this.option = option;
}
public Builder(String fromProp, String toProp) {
this.from = fromProp;
this.to = toProp;
}
public Builder<T> from(String from) {
this.from = from;
return this;
} }
public Builder<T> to(String to) { public Builder<T> to(String to) {
@ -269,22 +231,11 @@ public class PropertyMapper<T> {
return this; return this;
} }
public Builder<T> defaultValue(T defaultValue) {
this.defaultValue = defaultValue;
return this;
}
public Builder<T> transformer(BiFunction<String, ConfigSourceInterceptorContext, String> mapper) { public Builder<T> transformer(BiFunction<String, ConfigSourceInterceptorContext, String> mapper) {
this.mapper = mapper; this.mapper = mapper;
return this; return this;
} }
public Builder<T> description(String description) {
this.description = description;
return this;
}
public Builder<T> paramLabel(String label) { public Builder<T> paramLabel(String label) {
this.paramLabel = label; this.paramLabel = label;
return this; return this;
@ -295,63 +246,21 @@ public class PropertyMapper<T> {
return this; return this;
} }
public Builder<T> expectedValues(List<T> expectedValues) {
this.expectedValues = new ArrayList<>(expectedValues);
return this;
}
public Builder expectedValues(T... expectedValues) {
this.expectedValues = new ArrayList<>(Arrays.asList(expectedValues));
return this;
}
public Builder<T> isBuildTimeProperty(boolean isBuildTime) {
this.isBuildTimeProperty = isBuildTime;
return this;
}
public Builder<T> isMasked(boolean isMasked) { public Builder<T> isMasked(boolean isMasked) {
this.isMasked = isMasked; this.isMasked = isMasked;
return this; return this;
} }
public Builder<T> category(OptionCategory category) {
this.category = category;
return this;
}
public Builder<T> type(Class<T> type) {
if (Boolean.class.equals(type)) {
expectedValues((T) Boolean.TRUE.toString(), (T) Boolean.FALSE.toString());
paramLabel(defaultValue == null ? "true|false" : defaultValue.toString());
defaultValue(defaultValue == null ? (T) Boolean.FALSE : defaultValue);
}
this.type = type;
return this;
}
public Builder<T> hidden(boolean hidden) {
this.hidden = hidden;
return this;
}
public PropertyMapper<T> build() { public PropertyMapper<T> build() {
return new PropertyMapper<T>(type, from, to, Optional.ofNullable(defaultValue), mapper, mapFrom, isBuildTimeProperty, description, paramLabel, if (paramLabel == null && Boolean.class.equals(option.getType())) {
isMasked, expectedValues, category, hidden); paramLabel = Boolean.TRUE + "|" + Boolean.FALSE;
}
return new PropertyMapper<T>(option, to, mapper, mapFrom, paramLabel, isMasked);
} }
} }
public static <T> PropertyMapper.Builder<T> fromOption(Option<T> opt) { public static <T> PropertyMapper.Builder<T> fromOption(Option<T> opt) {
Builder<T> builder = PropertyMapper.builder(opt.getCategory()) return new PropertyMapper.Builder<>(opt);
.type(opt.getType())
.from(opt.getKey())
.hidden(!opt.getSupportedRuntimes().contains(Option.Runtime.QUARKUS))
.description(opt.getDescription())
.isBuildTimeProperty(opt.isBuildTime())
.expectedValues(opt.getExpectedValues());
if (opt.getDefaultValue().isPresent()) {
builder.defaultValue(opt.getDefaultValue().get());
}
return builder;
} }
} }

View file

@ -1,12 +1,9 @@
package org.keycloak.quarkus.runtime.configuration.mappers; package org.keycloak.quarkus.runtime.configuration.mappers;
import io.quarkus.runtime.QuarkusApplication;
import io.quarkus.runtime.configuration.ConfigurationRuntimeConfig;
import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue; import io.smallrye.config.ConfigValue;
import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource; import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
@ -173,4 +170,5 @@ public final class PropertyMappers {
return super.put(key, value); return super.put(key, value);
} }
} }
} }

View file

@ -2,43 +2,34 @@ package org.keycloak.quarkus.runtime.configuration.mappers;
import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigSourceInterceptorContext;
import java.util.Arrays;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
import static org.keycloak.quarkus.runtime.integration.QuarkusPlatform.addInitializationException; import static org.keycloak.quarkus.runtime.integration.QuarkusPlatform.addInitializationException;
import org.keycloak.config.OptionCategory; import org.keycloak.config.ProxyOptions;
import org.keycloak.quarkus.runtime.Messages; import org.keycloak.quarkus.runtime.Messages;
final class ProxyPropertyMappers { final class ProxyPropertyMappers {
private static final String[] possibleProxyValues = {"edge", "reencrypt", "passthrough"};
private ProxyPropertyMappers(){} private ProxyPropertyMappers(){}
public static PropertyMapper[] getProxyPropertyMappers() { public static PropertyMapper[] getProxyPropertyMappers() {
return new PropertyMapper[] { return new PropertyMapper[] {
builder().from("proxy") fromOption(ProxyOptions.proxy)
.to("quarkus.http.proxy.proxy-address-forwarding") .to("quarkus.http.proxy.proxy-address-forwarding")
.defaultValue("none") .transformer(ProxyPropertyMappers::getValidProxyModeValue)
.transformer(getValidProxyModeValue())
.expectedValues(Arrays.asList(possibleProxyValues))
.description("The proxy address forwarding mode if the server is behind a reverse proxy. " +
"Possible values are: " + String.join(",",possibleProxyValues))
.paramLabel("mode") .paramLabel("mode")
.category(OptionCategory.PROXY)
.build(), .build(),
builder().to("quarkus.http.proxy.enable-forwarded-host") fromOption(ProxyOptions.proxyForwardedHost)
.to("quarkus.http.proxy.enable-forwarded-host")
.mapFrom("proxy") .mapFrom("proxy")
.defaultValue("false") .transformer(ProxyPropertyMappers::getResolveEnableForwardedHost)
.transformer(ProxyPropertyMappers::resolveEnableForwardedHost)
.category(OptionCategory.PROXY)
.build() .build()
}; };
} }
private static BiFunction<String, ConfigSourceInterceptorContext, String> getValidProxyModeValue() { private static String getValidProxyModeValue(String mode, ConfigSourceInterceptorContext context) {
return (mode, context) -> {
switch (mode) { switch (mode) {
case "none": case "none":
return "false"; return "false";
@ -50,14 +41,10 @@ final class ProxyPropertyMappers {
addInitializationException(Messages.invalidProxyMode(mode)); addInitializationException(Messages.invalidProxyMode(mode));
return "false"; return "false";
} }
};
} }
private static String resolveEnableForwardedHost(String proxy, ConfigSourceInterceptorContext context) { private static String getResolveEnableForwardedHost(String proxy, ConfigSourceInterceptorContext context) {
return String.valueOf(!"none".equals(proxy)); return String.valueOf(!"none".equals(proxy));
} }
private static <T> PropertyMapper.Builder<T> builder() {
return PropertyMapper.builder(OptionCategory.PROXY);
}
} }

View file

@ -1,9 +1,9 @@
package org.keycloak.quarkus.runtime.configuration.mappers; package org.keycloak.quarkus.runtime.configuration.mappers;
import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigSourceInterceptorContext;
import org.keycloak.config.OptionCategory; import org.keycloak.config.TransactionOptions;
import java.util.Arrays; import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
public class TransactionPropertyMappers { public class TransactionPropertyMappers {
@ -11,15 +11,11 @@ public class TransactionPropertyMappers {
public static PropertyMapper[] getTransactionPropertyMappers() { public static PropertyMapper[] getTransactionPropertyMappers() {
return new PropertyMapper[] { return new PropertyMapper[] {
builder().from("transaction-xa-enabled") fromOption(TransactionOptions.TRANSACTION_XA_ENABLED)
.to("quarkus.datasource.jdbc.transactions") .to("quarkus.datasource.jdbc.transactions")
.defaultValue(Boolean.TRUE.toString())
.description("Manually override the transaction type. Transaction type XA and the appropriate driver is used by default.")
.paramLabel(Boolean.TRUE + "|" + Boolean.FALSE) .paramLabel(Boolean.TRUE + "|" + Boolean.FALSE)
.expectedValues(Arrays.asList(Boolean.TRUE.toString(), Boolean.FALSE.toString()))
.isBuildTimeProperty(true)
.transformer(TransactionPropertyMappers::getQuarkusTransactionsValue) .transformer(TransactionPropertyMappers::getQuarkusTransactionsValue)
.build(), .build()
}; };
} }
@ -33,8 +29,4 @@ public class TransactionPropertyMappers {
return "enabled"; return "enabled";
} }
private static <T> PropertyMapper.Builder<T> builder() {
return PropertyMapper.builder(OptionCategory.TRANSACTION);
}
} }

View file

@ -1,6 +1,8 @@
package org.keycloak.quarkus.runtime.configuration.mappers; package org.keycloak.quarkus.runtime.configuration.mappers;
import org.keycloak.config.OptionCategory; import org.keycloak.config.VaultOptions;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
final class VaultPropertyMappers { final class VaultPropertyMappers {
@ -9,45 +11,25 @@ final class VaultPropertyMappers {
public static PropertyMapper[] getVaultPropertyMappers() { public static PropertyMapper[] getVaultPropertyMappers() {
return new PropertyMapper[] { return new PropertyMapper[] {
builder() fromOption(VaultOptions.VAULT)
.from("vault")
.description("Enables a vault provider.")
.expectedValues("file", "hashicorp")
.paramLabel("provider") .paramLabel("provider")
.isBuildTimeProperty(true)
.build(), .build(),
builder() fromOption(VaultOptions.VAULT_DIR)
.from("vault-dir")
.to("kc.spi-vault-file-dir") .to("kc.spi-vault-file-dir")
.description("If set, secrets can be obtained by reading the content of files within the given directory.")
.paramLabel("dir") .paramLabel("dir")
.build(), .build(),
builder() fromOption(VaultOptions.VAULT_UNMAPPED)
.from("vault-")
.to("quarkus.vault.") .to("quarkus.vault.")
.description("Maps any vault option to their corresponding properties in quarkus-vault extension.")
.hidden(true)
.isBuildTimeProperty(true)
.build(), .build(),
builder() fromOption(VaultOptions.VAULT_URL)
.from("vault-url")
.to("quarkus.vault.url") .to("quarkus.vault.url")
.description("The vault server url.")
.paramLabel("paths") .paramLabel("paths")
.hidden(true)
.isBuildTimeProperty(true)
.build(), .build(),
builder() fromOption(VaultOptions.VAULT_KV_PATHS)
.from("vault-kv-paths")
.to("kc.spi-vault-hashicorp-paths") .to("kc.spi-vault-hashicorp-paths")
.description("A set of one or more key/value paths that should be used when looking up secrets.")
.paramLabel("paths") .paramLabel("paths")
.hidden(true)
.build() .build()
}; };
} }
private static PropertyMapper.Builder builder() {
return PropertyMapper.builder(OptionCategory.VAULT);
}
} }

View file

@ -24,6 +24,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.condition.OS;
import org.keycloak.config.LoggingOptions;
import org.keycloak.it.junit5.extension.CLIResult; import org.keycloak.it.junit5.extension.CLIResult;
import org.keycloak.it.junit5.extension.DistributionTest; import org.keycloak.it.junit5.extension.DistributionTest;
import org.keycloak.it.junit5.extension.RawDistOnly; import org.keycloak.it.junit5.extension.RawDistOnly;
@ -142,7 +143,7 @@ public class LoggingDistTest {
@EnabledOnOs(value = { OS.LINUX, OS.MAC }, disabledReason = "different shell escaping behaviour on Windows.") @EnabledOnOs(value = { OS.LINUX, OS.MAC }, disabledReason = "different shell escaping behaviour on Windows.")
@Launch({ "start-dev", "--log=console,file"}) @Launch({ "start-dev", "--log=console,file"})
void testKeycloakLogFileCreated(RawDistRootPath path) { void testKeycloakLogFileCreated(RawDistRootPath path) {
Path logFilePath = Paths.get(path.getDistRootPath() + File.separator + LoggingPropertyMappers.DEFAULT_LOG_PATH); Path logFilePath = Paths.get(path.getDistRootPath() + File.separator + LoggingOptions.DEFAULT_LOG_PATH);
File logFile = new File(logFilePath.toString()); File logFile = new File(logFilePath.toString());
assertTrue(logFile.isFile(), "Log file does not exist!"); assertTrue(logFile.isFile(), "Log file does not exist!");
} }
@ -151,7 +152,7 @@ public class LoggingDistTest {
@EnabledOnOs(value = { OS.WINDOWS }, disabledReason = "different shell escaping behaviour on Windows.") @EnabledOnOs(value = { OS.WINDOWS }, disabledReason = "different shell escaping behaviour on Windows.")
@Launch({ "start-dev", "--log=\"console,file\""}) @Launch({ "start-dev", "--log=\"console,file\""})
void testWinKeycloakLogFileCreated(RawDistRootPath path) { void testWinKeycloakLogFileCreated(RawDistRootPath path) {
Path logFilePath = Paths.get(path.getDistRootPath() + File.separator + LoggingPropertyMappers.DEFAULT_LOG_PATH); Path logFilePath = Paths.get(path.getDistRootPath() + File.separator + LoggingOptions.DEFAULT_LOG_PATH);
File logFile = new File(logFilePath.toString()); File logFile = new File(logFilePath.toString());
assertTrue(logFile.isFile(), "Log file does not exist!"); assertTrue(logFile.isFile(), "Log file does not exist!");
} }
@ -160,7 +161,7 @@ public class LoggingDistTest {
@EnabledOnOs(value = { OS.LINUX, OS.MAC }, disabledReason = "different shell escaping behaviour on Windows.") @EnabledOnOs(value = { OS.LINUX, OS.MAC }, disabledReason = "different shell escaping behaviour on Windows.")
@Launch({ "start-dev", "--log=console,file", "--log-file-format=\"%d{HH:mm:ss} %-5p [%c{1.}] (%t) %s%e%n\""}) @Launch({ "start-dev", "--log=console,file", "--log-file-format=\"%d{HH:mm:ss} %-5p [%c{1.}] (%t) %s%e%n\""})
void testFileLoggingHasDifferentFormat(RawDistRootPath path) throws IOException { void testFileLoggingHasDifferentFormat(RawDistRootPath path) throws IOException {
Path logFilePath = Paths.get(path.getDistRootPath() + File.separator + LoggingPropertyMappers.DEFAULT_LOG_PATH); Path logFilePath = Paths.get(path.getDistRootPath() + File.separator + LoggingOptions.DEFAULT_LOG_PATH);
File logFile = new File(logFilePath.toString()); File logFile = new File(logFilePath.toString());
String data = FileUtils.readFileToString(logFile, Charset.defaultCharset()); String data = FileUtils.readFileToString(logFile, Charset.defaultCharset());