fix: simplify / refine validation methods (#32487)

closes: #32455

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
This commit is contained in:
Steven Hawkins 2024-09-04 05:21:26 -04:00 committed by GitHub
parent 0fcbec8daa
commit 081a3852c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 63 additions and 36 deletions

View file

@ -35,7 +35,7 @@ public final class BootstrapAdminPropertyMappers {
return new PropertyMapper[]{ return new PropertyMapper[]{
fromOption(BootstrapAdminOptions.USERNAME) fromOption(BootstrapAdminOptions.USERNAME)
.paramLabel("username") .paramLabel("username")
.appendValidateEnabled(BootstrapAdminPropertyMappers::isPasswordSet, PASSWORD_SET) .addValidateEnabled(BootstrapAdminPropertyMappers::isPasswordSet, PASSWORD_SET)
.build(), .build(),
fromOption(BootstrapAdminOptions.PASSWORD) fromOption(BootstrapAdminOptions.PASSWORD)
.paramLabel("password") .paramLabel("password")
@ -47,7 +47,7 @@ public final class BootstrapAdminPropertyMappers {
.build(),*/ .build(),*/
fromOption(BootstrapAdminOptions.CLIENT_ID) fromOption(BootstrapAdminOptions.CLIENT_ID)
.paramLabel("client id") .paramLabel("client id")
.appendValidateEnabled(BootstrapAdminPropertyMappers::isClientSecretSet, CLIENT_SECRET_SET) .addValidateEnabled(BootstrapAdminPropertyMappers::isClientSecretSet, CLIENT_SECRET_SET)
.build(), .build(),
fromOption(BootstrapAdminOptions.CLIENT_SECRET) fromOption(BootstrapAdminOptions.CLIENT_SECRET)
.paramLabel("client secret") .paramLabel("client secret")

View file

@ -69,7 +69,7 @@ public final class ExportPropertyMappers {
.build(), .build(),
fromOption(ExportOptions.USERS) fromOption(ExportOptions.USERS)
.to("kc.spi-export-dir-users-export-strategy") .to("kc.spi-export-dir-users-export-strategy")
.validator(ExportPropertyMappers::validateUsersUsage) .addValidator(ExportPropertyMappers::validateUsersUsage)
.paramLabel("strategy") .paramLabel("strategy")
.build(), .build(),
fromOption(ExportOptions.USERS_PER_FILE) fromOption(ExportOptions.USERS_PER_FILE)
@ -81,8 +81,6 @@ public final class ExportPropertyMappers {
} }
private static void validateUsersUsage(PropertyMapper<?> mapper, ConfigValue value) { private static void validateUsersUsage(PropertyMapper<?> mapper, ConfigValue value) {
mapper.validateExpectedValues(value, mapper::validateSingleValue);
if (!isBlank(ExportOptions.FILE) && isBlank(ExportOptions.DIR)) { if (!isBlank(ExportOptions.FILE) && isBlank(ExportOptions.DIR)) {
var sameFileIsSpecified = UsersExportStrategy.SAME_FILE.toString().toLowerCase().equals(value.getValue()); var sameFileIsSpecified = UsersExportStrategy.SAME_FILE.toString().toLowerCase().equals(value.getValue());

View file

@ -23,8 +23,7 @@ public final class FeaturePropertyMappers {
return new PropertyMapper[] { return new PropertyMapper[] {
fromOption(FeatureOptions.FEATURES) fromOption(FeatureOptions.FEATURES)
.paramLabel("feature") .paramLabel("feature")
.validator((mapper, value) -> mapper.validateExpectedValues(value, .validator(FeaturePropertyMappers::validateEnabledFeature)
(c, v) -> validateEnabledFeature(v)))
.build(), .build(),
fromOption(FeatureOptions.FEATURES_DISABLED) fromOption(FeatureOptions.FEATURES_DISABLED)
.paramLabel("feature") .paramLabel("feature")

View file

@ -94,8 +94,7 @@ public final class LoggingPropertyMappers {
fromOption(LoggingOptions.LOG_LEVEL) fromOption(LoggingOptions.LOG_LEVEL)
.to("quarkus.log.level") .to("quarkus.log.level")
.transformer(LoggingPropertyMappers::resolveLogLevel) .transformer(LoggingPropertyMappers::resolveLogLevel)
.validator((mapper, value) -> mapper.validateExpectedValues(value, .validator(LoggingPropertyMappers::validateLogLevel)
(c, v) -> validateLogLevel(v)))
.paramLabel("category:level") .paramLabel("category:level")
.build(), .build(),
// Syslog // Syslog

View file

@ -76,25 +76,25 @@ public class ManagementPropertyMappers {
fromOption(ManagementOptions.HTTPS_MANAGEMENT_CERTIFICATE_FILE) fromOption(ManagementOptions.HTTPS_MANAGEMENT_CERTIFICATE_FILE)
.mapFrom(HttpOptions.HTTPS_CERTIFICATE_FILE.getKey()) .mapFrom(HttpOptions.HTTPS_CERTIFICATE_FILE.getKey())
.to("quarkus.management.ssl.certificate.files") .to("quarkus.management.ssl.certificate.files")
.validator((mapper, value) -> validateTlsProperties()) .validator(value -> validateTlsProperties())
.paramLabel("file") .paramLabel("file")
.build(), .build(),
fromOption(ManagementOptions.HTTPS_MANAGEMENT_CERTIFICATE_KEY_FILE) fromOption(ManagementOptions.HTTPS_MANAGEMENT_CERTIFICATE_KEY_FILE)
.mapFrom(HttpOptions.HTTPS_CERTIFICATE_KEY_FILE.getKey()) .mapFrom(HttpOptions.HTTPS_CERTIFICATE_KEY_FILE.getKey())
.to("quarkus.management.ssl.certificate.key-files") .to("quarkus.management.ssl.certificate.key-files")
.validator((mapper, value) -> validateTlsProperties()) .validator(value -> validateTlsProperties())
.paramLabel("file") .paramLabel("file")
.build(), .build(),
fromOption(ManagementOptions.HTTPS_MANAGEMENT_KEY_STORE_FILE) fromOption(ManagementOptions.HTTPS_MANAGEMENT_KEY_STORE_FILE)
.mapFrom(HttpOptions.HTTPS_KEY_STORE_FILE.getKey()) .mapFrom(HttpOptions.HTTPS_KEY_STORE_FILE.getKey())
.to("quarkus.management.ssl.certificate.key-store-file") .to("quarkus.management.ssl.certificate.key-store-file")
.validator((mapper, value) -> validateTlsProperties()) .validator(value -> validateTlsProperties())
.paramLabel("file") .paramLabel("file")
.build(), .build(),
fromOption(ManagementOptions.HTTPS_MANAGEMENT_KEY_STORE_PASSWORD) fromOption(ManagementOptions.HTTPS_MANAGEMENT_KEY_STORE_PASSWORD)
.mapFrom(HttpOptions.HTTPS_KEY_STORE_PASSWORD.getKey()) .mapFrom(HttpOptions.HTTPS_KEY_STORE_PASSWORD.getKey())
.to("quarkus.management.ssl.certificate.key-store-password") .to("quarkus.management.ssl.certificate.key-store-password")
.validator((mapper, value) -> validateTlsProperties()) .validator(value -> validateTlsProperties())
.paramLabel("password") .paramLabel("password")
.isMasked(true) .isMasked(true)
.build(), .build(),

View file

@ -24,10 +24,13 @@ import static org.keycloak.quarkus.runtime.configuration.Configuration.toCliForm
import static org.keycloak.quarkus.runtime.configuration.Configuration.toEnvVarFormat; import static org.keycloak.quarkus.runtime.configuration.Configuration.toEnvVarFormat;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.BooleanSupplier; import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.stream.Stream;
import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue; import io.smallrye.config.ConfigValue;
@ -297,7 +300,7 @@ public class PropertyMapper<T> {
private BooleanSupplier isEnabled = () -> true; private BooleanSupplier isEnabled = () -> true;
private String enabledWhen = ""; private String enabledWhen = "";
private String paramLabel; private String paramLabel;
private BiConsumer<PropertyMapper<T>, ConfigValue> validator = (mapper, value) -> mapper.validateExpectedValues(value, mapper::validateSingleValue); private BiConsumer<PropertyMapper<T>, ConfigValue> validator = (mapper, value) -> mapper.validateValues(value, mapper::validateExpectedValues);
private String description; private String description;
public Builder(Option<T> option) { public Builder(Option<T> option) {
@ -349,27 +352,41 @@ public class PropertyMapper<T> {
/** /**
* Set the validator, overwriting the current one. * Set the validator, overwriting the current one.
*/ */
public Builder<T> validator(BiConsumer<PropertyMapper<T>, ConfigValue> validator) { public Builder<T> validator(Consumer<String> validator) {
this.validator = 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; return this;
} }
public Builder<T> appendValidator(BiConsumer<PropertyMapper<T>, ConfigValue> validator) { public Builder<T> addValidator(BiConsumer<PropertyMapper<T>, ConfigValue> validator) {
var current = this.validator; var current = this.validator;
this.validator = (mapper, value) -> { this.validator = (mapper, value) -> {
validator.accept(mapper, value); Stream.of(current, validator).map(v -> {
current.accept(mapper, value); try {
v.accept(mapper, value);
return Optional.<PropertyException>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; 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. * to appear in help.
* @return * @return
*/ */
public Builder<T> appendValidateEnabled(BooleanSupplier isEnabled, String enabledWhen) { public Builder<T> addValidateEnabled(BooleanSupplier isEnabled, String enabledWhen) {
this.appendValidator((mapper, value) -> { this.addValidator((mapper, value) -> {
if (!isEnabled.getAsBoolean()) { if (!isEnabled.getAsBoolean()) {
throw new PropertyException(mapper.getOption().getKey() + " available only when " + enabledWhen); throw new PropertyException(mapper.getOption().getKey() + " available only when " + enabledWhen);
} }
@ -396,18 +413,34 @@ public class PropertyMapper<T> {
} }
} }
public void validateExpectedValues(ConfigValue configValue, BiConsumer<ConfigValue, String> singleValidator) { public void validateValues(ConfigValue configValue, BiConsumer<ConfigValue, String> singleValidator) {
String value = configValue.getValue(); String value = configValue.getValue();
boolean multiValued = getOption().getType() == java.util.List.class; boolean multiValued = getOption().getType() == java.util.List.class;
StringBuilder result = new StringBuilder();
String[] values = multiValued ? value.split(",") : new String[] { value }; String[] values = multiValued ? value.split(",") : new String[] { value };
for (String v : values) { for (String v : values) {
if (multiValued && !v.trim().equals(v)) { if (multiValued && !v.trim().equals(v)) {
throw new PropertyException("Invalid value for multivalued option " + getOptionAndSourceMessage(configValue) if (!result.isEmpty()) {
+ ": list value '" + v + "' should not have leading nor trailing whitespace"); result.append(".\n");
} }
result.append("Invalid value for multivalued option " + getOptionAndSourceMessage(configValue)
+ ": list value '" + v + "' should not have leading nor trailing whitespace");
continue;
}
try {
singleValidator.accept(configValue, v); 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<T> {
return Optional.ofNullable(configValue.getConfigSourceName()).filter(name -> name.contains(KcEnvConfigSource.NAME)).isPresent(); 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<String> expectedValues = getExpectedValues(); List<String> expectedValues = getExpectedValues();
if (!expectedValues.isEmpty() && !expectedValues.contains(v) && getOption().isStrictExpectedValues()) { if (!expectedValues.isEmpty() && !expectedValues.contains(v) && getOption().isStrictExpectedValues()) {
throw new PropertyException( throw new PropertyException(

View file

@ -38,9 +38,8 @@ final class ProxyPropertyMappers {
.build(), .build(),
fromOption(ProxyOptions.PROXY_TRUSTED_ADDRESSES) fromOption(ProxyOptions.PROXY_TRUSTED_ADDRESSES)
.to("quarkus.http.proxy.trusted-proxies") .to("quarkus.http.proxy.trusted-proxies")
.validator((mapper, value) -> mapper.validateExpectedValues(value, .validator(ProxyPropertyMappers::validateAddress)
(c, v) -> validateAddress(v))) .addValidateEnabled(() -> !Configuration.isBlank(ProxyOptions.PROXY_HEADERS), "proxy-headers is set")
.appendValidateEnabled(() -> !Configuration.isBlank(ProxyOptions.PROXY_HEADERS), "proxy-headers is set")
.paramLabel("trusted proxies") .paramLabel("trusted proxies")
.build() .build()
}; };

View file

@ -17,7 +17,6 @@
package org.keycloak.quarkus.runtime.configuration.mappers; package org.keycloak.quarkus.runtime.configuration.mappers;
import io.smallrye.config.ConfigValue;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.cli.PropertyException; import org.keycloak.quarkus.runtime.cli.PropertyException;
@ -97,23 +96,23 @@ public class TracingPropertyMappers {
}; };
} }
private static void validateEndpoint(PropertyMapper<String> mapper, ConfigValue value) { private static void validateEndpoint(String value) {
if (value == null || StringUtil.isBlank(value.getValue())) { if (StringUtil.isBlank(value)) {
throw new PropertyException("URL specified in 'tracing-endpoint' option must not be empty."); 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."); throw new PropertyException("URL specified in 'tracing-endpoint' option is invalid.");
} }
} }
private static void validateRatio(PropertyMapper<Double> mapper, ConfigValue value) { private static void validateRatio(String value) {
if (value == null || StringUtil.isBlank(value.getValue())) { if (StringUtil.isBlank(value)) {
throw new PropertyException("Ratio in 'tracing-sampler-ratio' option must not be empty."); throw new PropertyException("Ratio in 'tracing-sampler-ratio' option must not be empty.");
} }
try { try {
var ratio = Double.parseDouble(value.getValue()); var ratio = Double.parseDouble(value);
if (ratio <= 0.0 || ratio > 1.0) { if (ratio <= 0.0 || ratio > 1.0) {
throw new NumberFormatException(); throw new NumberFormatException();
} }