diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/BootstrapAdminPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/BootstrapAdminPropertyMappers.java index 6665f4e4fb..e24b08a6ba 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/BootstrapAdminPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/BootstrapAdminPropertyMappers.java @@ -35,7 +35,7 @@ public final class BootstrapAdminPropertyMappers { return new PropertyMapper[]{ fromOption(BootstrapAdminOptions.USERNAME) .paramLabel("username") - .appendValidateEnabled(BootstrapAdminPropertyMappers::isPasswordSet, PASSWORD_SET) + .addValidateEnabled(BootstrapAdminPropertyMappers::isPasswordSet, PASSWORD_SET) .build(), fromOption(BootstrapAdminOptions.PASSWORD) .paramLabel("password") @@ -47,7 +47,7 @@ public final class BootstrapAdminPropertyMappers { .build(),*/ fromOption(BootstrapAdminOptions.CLIENT_ID) .paramLabel("client id") - .appendValidateEnabled(BootstrapAdminPropertyMappers::isClientSecretSet, CLIENT_SECRET_SET) + .addValidateEnabled(BootstrapAdminPropertyMappers::isClientSecretSet, CLIENT_SECRET_SET) .build(), fromOption(BootstrapAdminOptions.CLIENT_SECRET) .paramLabel("client secret") diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ExportPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ExportPropertyMappers.java index a2d101fb20..242ad243db 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ExportPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ExportPropertyMappers.java @@ -69,7 +69,7 @@ public final class ExportPropertyMappers { .build(), fromOption(ExportOptions.USERS) .to("kc.spi-export-dir-users-export-strategy") - .validator(ExportPropertyMappers::validateUsersUsage) + .addValidator(ExportPropertyMappers::validateUsersUsage) .paramLabel("strategy") .build(), fromOption(ExportOptions.USERS_PER_FILE) @@ -81,8 +81,6 @@ public final class ExportPropertyMappers { } private static void validateUsersUsage(PropertyMapper mapper, ConfigValue value) { - mapper.validateExpectedValues(value, mapper::validateSingleValue); - if (!isBlank(ExportOptions.FILE) && isBlank(ExportOptions.DIR)) { var sameFileIsSpecified = UsersExportStrategy.SAME_FILE.toString().toLowerCase().equals(value.getValue()); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/FeaturePropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/FeaturePropertyMappers.java index 73c457b50b..07e085da09 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/FeaturePropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/FeaturePropertyMappers.java @@ -23,8 +23,7 @@ public final class FeaturePropertyMappers { return new PropertyMapper[] { fromOption(FeatureOptions.FEATURES) .paramLabel("feature") - .validator((mapper, value) -> mapper.validateExpectedValues(value, - (c, v) -> validateEnabledFeature(v))) + .validator(FeaturePropertyMappers::validateEnabledFeature) .build(), fromOption(FeatureOptions.FEATURES_DISABLED) .paramLabel("feature") diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/LoggingPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/LoggingPropertyMappers.java index 176eac59d0..7aee0060b6 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/LoggingPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/LoggingPropertyMappers.java @@ -94,8 +94,7 @@ public final class LoggingPropertyMappers { fromOption(LoggingOptions.LOG_LEVEL) .to("quarkus.log.level") .transformer(LoggingPropertyMappers::resolveLogLevel) - .validator((mapper, value) -> mapper.validateExpectedValues(value, - (c, v) -> validateLogLevel(v))) + .validator(LoggingPropertyMappers::validateLogLevel) .paramLabel("category:level") .build(), // Syslog diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ManagementPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ManagementPropertyMappers.java index 2249977de1..cccf6b0d66 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ManagementPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ManagementPropertyMappers.java @@ -76,25 +76,25 @@ public class ManagementPropertyMappers { fromOption(ManagementOptions.HTTPS_MANAGEMENT_CERTIFICATE_FILE) .mapFrom(HttpOptions.HTTPS_CERTIFICATE_FILE.getKey()) .to("quarkus.management.ssl.certificate.files") - .validator((mapper, value) -> validateTlsProperties()) + .validator(value -> validateTlsProperties()) .paramLabel("file") .build(), fromOption(ManagementOptions.HTTPS_MANAGEMENT_CERTIFICATE_KEY_FILE) .mapFrom(HttpOptions.HTTPS_CERTIFICATE_KEY_FILE.getKey()) .to("quarkus.management.ssl.certificate.key-files") - .validator((mapper, value) -> validateTlsProperties()) + .validator(value -> validateTlsProperties()) .paramLabel("file") .build(), fromOption(ManagementOptions.HTTPS_MANAGEMENT_KEY_STORE_FILE) .mapFrom(HttpOptions.HTTPS_KEY_STORE_FILE.getKey()) .to("quarkus.management.ssl.certificate.key-store-file") - .validator((mapper, value) -> validateTlsProperties()) + .validator(value -> validateTlsProperties()) .paramLabel("file") .build(), fromOption(ManagementOptions.HTTPS_MANAGEMENT_KEY_STORE_PASSWORD) .mapFrom(HttpOptions.HTTPS_KEY_STORE_PASSWORD.getKey()) .to("quarkus.management.ssl.certificate.key-store-password") - .validator((mapper, value) -> validateTlsProperties()) + .validator(value -> validateTlsProperties()) .paramLabel("password") .isMasked(true) .build(), diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java index e5e83a2371..932f62adaf 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java @@ -24,10 +24,13 @@ import static org.keycloak.quarkus.runtime.configuration.Configuration.toCliForm import static org.keycloak.quarkus.runtime.configuration.Configuration.toEnvVarFormat; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.stream.Stream; import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigValue; @@ -297,7 +300,7 @@ public class PropertyMapper { private BooleanSupplier isEnabled = () -> true; private String enabledWhen = ""; private String paramLabel; - private BiConsumer, ConfigValue> validator = (mapper, value) -> mapper.validateExpectedValues(value, mapper::validateSingleValue); + private BiConsumer, ConfigValue> validator = (mapper, value) -> mapper.validateValues(value, mapper::validateExpectedValues); private String description; public Builder(Option option) { @@ -349,27 +352,41 @@ public class PropertyMapper { /** * Set the validator, overwriting the current one. */ - public Builder validator(BiConsumer, ConfigValue> validator) { - this.validator = validator; + public Builder validator(Consumer validator) { + this.validator = (mapper, value) -> mapper.validateValues(value, + (c, v) -> validator.accept(v)); + if (!Objects.equals(this.description, this.option.getDescription())) { + throw new AssertionError("Overwriting the validator will cause the description modification from addValidateEnabled to be incorrect."); + } return this; } - public Builder appendValidator(BiConsumer, ConfigValue> validator) { + public Builder addValidator(BiConsumer, ConfigValue> validator) { var current = this.validator; this.validator = (mapper, value) -> { - validator.accept(mapper, value); - current.accept(mapper, value); + Stream.of(current, validator).map(v -> { + try { + v.accept(mapper, value); + return Optional.empty(); + } catch (PropertyException e) { + return Optional.of(e); + } + }).flatMap(Optional::stream) + .reduce((e1, e2) -> new PropertyException(String.format("%s.\n%s", e1.getMessage(), e2.getMessage()))) + .ifPresent(e -> { + throw e; + }); }; return this; } /** - * Similar to {@link #enabledWhen}, but uses the condition as a validator that is appended to the current one. This allows the option + * Similar to {@link #enabledWhen}, but uses the condition as a validator that is added to the current one. This allows the option * to appear in help. * @return */ - public Builder appendValidateEnabled(BooleanSupplier isEnabled, String enabledWhen) { - this.appendValidator((mapper, value) -> { + public Builder addValidateEnabled(BooleanSupplier isEnabled, String enabledWhen) { + this.addValidator((mapper, value) -> { if (!isEnabled.getAsBoolean()) { throw new PropertyException(mapper.getOption().getKey() + " available only when " + enabledWhen); } @@ -396,18 +413,34 @@ public class PropertyMapper { } } - public void validateExpectedValues(ConfigValue configValue, BiConsumer singleValidator) { + public void validateValues(ConfigValue configValue, BiConsumer singleValidator) { String value = configValue.getValue(); boolean multiValued = getOption().getType() == java.util.List.class; + StringBuilder result = new StringBuilder(); String[] values = multiValued ? value.split(",") : new String[] { value }; for (String v : values) { if (multiValued && !v.trim().equals(v)) { - throw new PropertyException("Invalid value for multivalued option " + getOptionAndSourceMessage(configValue) + if (!result.isEmpty()) { + result.append(".\n"); + } + result.append("Invalid value for multivalued option " + getOptionAndSourceMessage(configValue) + ": list value '" + v + "' should not have leading nor trailing whitespace"); + continue; } - singleValidator.accept(configValue, v); + try { + singleValidator.accept(configValue, v); + } catch (PropertyException e) { + if (!result.isEmpty()) { + result.append(".\n"); + } + result.append(e.getMessage()); + } + } + + if (!result.isEmpty()) { + throw new PropertyException(result.toString()); } } @@ -419,7 +452,7 @@ public class PropertyMapper { return Optional.ofNullable(configValue.getConfigSourceName()).filter(name -> name.contains(KcEnvConfigSource.NAME)).isPresent(); } - void validateSingleValue(ConfigValue configValue, String v) { + void validateExpectedValues(ConfigValue configValue, String v) { List expectedValues = getExpectedValues(); if (!expectedValues.isEmpty() && !expectedValues.contains(v) && getOption().isStrictExpectedValues()) { throw new PropertyException( diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ProxyPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ProxyPropertyMappers.java index 56fe0b568a..403f2e8188 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ProxyPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ProxyPropertyMappers.java @@ -38,9 +38,8 @@ final class ProxyPropertyMappers { .build(), fromOption(ProxyOptions.PROXY_TRUSTED_ADDRESSES) .to("quarkus.http.proxy.trusted-proxies") - .validator((mapper, value) -> mapper.validateExpectedValues(value, - (c, v) -> validateAddress(v))) - .appendValidateEnabled(() -> !Configuration.isBlank(ProxyOptions.PROXY_HEADERS), "proxy-headers is set") + .validator(ProxyPropertyMappers::validateAddress) + .addValidateEnabled(() -> !Configuration.isBlank(ProxyOptions.PROXY_HEADERS), "proxy-headers is set") .paramLabel("trusted proxies") .build() }; diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/TracingPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/TracingPropertyMappers.java index 09ced24865..3496065b64 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/TracingPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/TracingPropertyMappers.java @@ -17,7 +17,6 @@ package org.keycloak.quarkus.runtime.configuration.mappers; -import io.smallrye.config.ConfigValue; import org.keycloak.common.Profile; import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.cli.PropertyException; @@ -97,23 +96,23 @@ public class TracingPropertyMappers { }; } - private static void validateEndpoint(PropertyMapper mapper, ConfigValue value) { - if (value == null || StringUtil.isBlank(value.getValue())) { + private static void validateEndpoint(String value) { + if (StringUtil.isBlank(value)) { throw new PropertyException("URL specified in 'tracing-endpoint' option must not be empty."); } - if (!isValidUrl(value.getValue())) { + if (!isValidUrl(value)) { throw new PropertyException("URL specified in 'tracing-endpoint' option is invalid."); } } - private static void validateRatio(PropertyMapper mapper, ConfigValue value) { - if (value == null || StringUtil.isBlank(value.getValue())) { + private static void validateRatio(String value) { + if (StringUtil.isBlank(value)) { throw new PropertyException("Ratio in 'tracing-sampler-ratio' option must not be empty."); } try { - var ratio = Double.parseDouble(value.getValue()); + var ratio = Double.parseDouble(value); if (ratio <= 0.0 || ratio > 1.0) { throw new NumberFormatException(); }