KEYCLOAK-2045 Simple Validation SPI for UserProfile SPI (#8053)
* KEYCLOAK-2045 Simple Validation API Co-authored-by: Thomas Darimont <thomas.darimont@googlemail.com>
This commit is contained in:
parent
e609949264
commit
0913a22c30
25 changed files with 2873 additions and 0 deletions
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.keycloak.validate.validators.NotBlankValidator;
|
||||
import org.keycloak.validate.validators.NotEmptyValidator;
|
||||
|
||||
/**
|
||||
* Base class for arbitrary value type validators. Functionality covered in this base class:
|
||||
* <ul>
|
||||
* <li>accepts supported type, collection of supported type.
|
||||
* <li>null values are always treated as valid to support optional fields! Use other validators (like
|
||||
* {@link NotBlankValidator} or {@link NotEmptyValidator} to force field as required.
|
||||
* </ul>
|
||||
*
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*
|
||||
*/
|
||||
public abstract class AbstractSimpleValidator implements SimpleValidator {
|
||||
|
||||
@Override
|
||||
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||
|
||||
if (input != null) {
|
||||
if (input instanceof Collection) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Collection<Object> values = (Collection<Object>) input;
|
||||
|
||||
if (values.isEmpty()) {
|
||||
return context;
|
||||
}
|
||||
|
||||
for (Object value : values) {
|
||||
validate(value, inputHint, context, config);
|
||||
}
|
||||
} else {
|
||||
doValidate(input, inputHint, context, config);
|
||||
}
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate type, format, range of the value etc. Always use {@link ValidationContext#addError(ValidationError)} to
|
||||
* report error to the user! Can be called multiple time for one validation if input is Collection.
|
||||
*
|
||||
* @param value to be validated, never null
|
||||
* @param inputHint
|
||||
* @param context for the validation. Add errors into it.
|
||||
* @param config of the validation if provided
|
||||
*/
|
||||
protected abstract void doValidate(Object value, String inputHint, ValidationContext context, ValidatorConfig config);
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate;
|
||||
|
||||
import org.keycloak.validate.validators.NotBlankValidator;
|
||||
|
||||
/**
|
||||
* Base class for String value format validators. Functionality covered in this base class:
|
||||
* <ul>
|
||||
* <li>accepts plain string and collections of strings as input
|
||||
* <li>each item is validated for collections of strings, see
|
||||
* {@link #validateFormat(String, String, ValidationContext, ValidatorConfig)}
|
||||
* <li>null values are always treated as valid to support optional fields! Use other validators (like
|
||||
* {@link NotBlankValidator} to force field as required.
|
||||
* </ul>
|
||||
*
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*
|
||||
*/
|
||||
public abstract class AbstractStringValidator extends AbstractSimpleValidator {
|
||||
|
||||
@Override
|
||||
protected void doValidate(Object value, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||
if (value instanceof String) {
|
||||
doValidate(value.toString(), inputHint, context, config);
|
||||
} else {
|
||||
context.addError(new ValidationError(getId(), inputHint, ValidationError.MESSAGE_INVALID_VALUE, value));
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void doValidate(String value, String inputHint, ValidationContext context, ValidatorConfig config);
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
||||
/**
|
||||
* Convenience interface to ease implementation of small {@link Validator} implementations.
|
||||
*
|
||||
* {@link SimpleValidator SimpleValidator's} should be implemented as singletons.
|
||||
*/
|
||||
public interface SimpleValidator extends Validator, ValidatorFactory {
|
||||
|
||||
@Override
|
||||
default Validator create(KeycloakSession session) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
default void init(Config.Scope config) {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
@Override
|
||||
default void postInit(KeycloakSessionFactory factory) {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
@Override
|
||||
default void close() {
|
||||
// NOOP
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
/**
|
||||
* Holds information about the validation state.
|
||||
*/
|
||||
public class ValidationContext {
|
||||
|
||||
/**
|
||||
* Holds the {@link KeycloakSession} in which the validation is performed.
|
||||
*/
|
||||
private final KeycloakSession session;
|
||||
|
||||
/**
|
||||
* Holds the {@link ValidationError} found during validation.
|
||||
*/
|
||||
private Set<ValidationError> errors;
|
||||
|
||||
/**
|
||||
* Holds optional attributes that should be available to {@link Validator} implementations.
|
||||
*/
|
||||
private final Map<String, Object> attributes;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ValidationContext} without a {@link KeycloakSession}.
|
||||
*/
|
||||
public ValidationContext() {
|
||||
this(null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ValidationContext} with a {@link KeycloakSession}.
|
||||
*
|
||||
* @param session
|
||||
*/
|
||||
public ValidationContext(KeycloakSession session) {
|
||||
// we deliberately use a LinkedHashSet here to retain the order of errors.
|
||||
this(session, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ValidationContext}.
|
||||
*
|
||||
* @param session
|
||||
* @param errors
|
||||
*/
|
||||
protected ValidationContext(KeycloakSession session, Set<ValidationError> errors) {
|
||||
this.session = session;
|
||||
this.errors = errors;
|
||||
this.attributes = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Eases access to {@link Validator Validator's} for nested validation.
|
||||
*
|
||||
* @param validatorId
|
||||
* @return
|
||||
*/
|
||||
public Validator validator(String validatorId) {
|
||||
return Validators.validator(session, validatorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an {@link ValidationError}.
|
||||
*
|
||||
* @param error
|
||||
*/
|
||||
public void addError(ValidationError error) {
|
||||
if (errors == null)
|
||||
errors = new LinkedHashSet<>();
|
||||
errors.add(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for checking the validation status of the current {@link ValidationContext}.
|
||||
* <p>
|
||||
* This is an alternative to {@code toResult().isValid()} for brief validations.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return errors == null || errors.isEmpty();
|
||||
}
|
||||
|
||||
public Map<String, Object> getAttributes() {
|
||||
return attributes;
|
||||
}
|
||||
|
||||
public KeycloakSession getSession() {
|
||||
return session;
|
||||
}
|
||||
|
||||
public Set<ValidationError> getErrors() {
|
||||
return errors != null ? errors : Collections.emptySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link ValidationResult} based on the current errors;
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public ValidationResult toResult() {
|
||||
return new ValidationResult(getErrors());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ValidationContext{" + "valid=" + isValid() + ", errors=" + errors + ", attributes=" + attributes + '}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* Denotes an error found during validation.
|
||||
*/
|
||||
public class ValidationError implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 4950708316675951914L;
|
||||
|
||||
/**
|
||||
* A generic invalid value message.
|
||||
*/
|
||||
public static final String MESSAGE_INVALID_VALUE = "error-invalid-value";
|
||||
|
||||
/**
|
||||
* Empty message parameters fly-weight.
|
||||
*/
|
||||
private static final Object[] EMPTY_PARAMETERS = {};
|
||||
|
||||
/**
|
||||
* Holds the name of the validator that reported the {@link ValidationError}.
|
||||
*/
|
||||
private final String validatorId;
|
||||
|
||||
/**
|
||||
* Holds an inputHint.
|
||||
* <p>
|
||||
* This could be a attribute name, a nested field path or a logical key.
|
||||
*/
|
||||
private final String inputHint;
|
||||
|
||||
/**
|
||||
* Holds the message key for translation.
|
||||
*/
|
||||
private final String message;
|
||||
|
||||
/**
|
||||
* Optional parameters for the message translation.
|
||||
*/
|
||||
private final Object[] messageParameters;
|
||||
|
||||
public ValidationError(String validatorId, String inputHint, String message) {
|
||||
this(validatorId, inputHint, message, EMPTY_PARAMETERS);
|
||||
}
|
||||
|
||||
public ValidationError(String validatorId, String inputHint, String message, Object... messageParameters) {
|
||||
this.validatorId = validatorId;
|
||||
this.inputHint = inputHint;
|
||||
this.message = message;
|
||||
this.messageParameters = messageParameters == null ? EMPTY_PARAMETERS : messageParameters.clone();
|
||||
}
|
||||
|
||||
public String getValidatorId() {
|
||||
return validatorId;
|
||||
}
|
||||
|
||||
public String getInputHint() {
|
||||
return inputHint;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw message parameters, e.g. the actual input that was given for validation.
|
||||
*
|
||||
* @return
|
||||
* @see #getInputHintWithMessageParameters()
|
||||
*/
|
||||
public Object[] getMessageParameters() {
|
||||
return messageParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the current {@link ValidationError} with the given formatter {@link java.util.function.Function}.
|
||||
* <p>
|
||||
* The formatter {@link java.util.function.Function} will be called with the {@link #message} and
|
||||
* {@link #getInputHintWithMessageParameters()} to render the error message.
|
||||
*
|
||||
* @param formatter
|
||||
* @return
|
||||
*/
|
||||
public String formatMessage(BiFunction<String, Object[], String> formatter) {
|
||||
Objects.requireNonNull(formatter, "formatter must not be null");
|
||||
return formatter.apply(message, getInputHintWithMessageParameters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array where the first element is the {@link #inputHint} follwed by the {@link #messageParameters}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Object[] getInputHintWithMessageParameters() {
|
||||
|
||||
// insert to current input hint into the message
|
||||
Object[] args = new Object[messageParameters.length + 1];
|
||||
args[0] = getInputHint();
|
||||
System.arraycopy(messageParameters, 0, args, 1, messageParameters.length);
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof ValidationError)) {
|
||||
return false;
|
||||
}
|
||||
ValidationError that = (ValidationError) o;
|
||||
return Objects.equals(validatorId, that.validatorId) && Objects.equals(inputHint, that.inputHint) && Objects.equals(message, that.message) && Arrays.equals(messageParameters, that.messageParameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(validatorId, inputHint, message);
|
||||
result = 31 * result + Arrays.hashCode(messageParameters);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ValidationError{" + "validatorId='" + validatorId + '\'' + ", inputHint='" + inputHint + '\'' + ", message='" + message + '\'' + ", messageParameters=" + Arrays.toString(messageParameters) + '}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Denotes the result of a validation.
|
||||
*/
|
||||
public class ValidationResult {
|
||||
|
||||
/**
|
||||
* An empty ValidationResult that's valid by default.
|
||||
*/
|
||||
public static final ValidationResult OK = new ValidationResult(Collections.emptySet());
|
||||
|
||||
/**
|
||||
* Holds the {@link ValidationError ValidationError's} that occurred during validation.
|
||||
*/
|
||||
private final Set<ValidationError> errors;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ValidationResult} from the given errors.
|
||||
* <p>
|
||||
* The created {@link ValidationResult} is considered valid if the given {@code errors} are empty.
|
||||
*
|
||||
* @param errors
|
||||
*/
|
||||
public ValidationResult(Set<ValidationError> errors) {
|
||||
this.errors = errors == null ? Collections.emptySet() : errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method that accepts a {@link Consumer<ValidationResult>} if the result is not valid.
|
||||
*
|
||||
* @param consumer
|
||||
*/
|
||||
public void ifNotValidAccept(Consumer<ValidationResult> consumer) {
|
||||
if (!isValid()) {
|
||||
consumer.accept(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method that accepts a {@link Consumer<ValidationError>}.
|
||||
*
|
||||
* @param consumer
|
||||
*/
|
||||
public void forEachError(Consumer<ValidationError> consumer) {
|
||||
for (ValidationError error : getErrors()) {
|
||||
consumer.accept(error);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return errors.isEmpty();
|
||||
}
|
||||
|
||||
public Set<ValidationError> getErrors() {
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this {@link ValidationResult} contains {@link ValidationError ValidationError's} from the {@link Validator} with the given {@code id}.
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
public boolean hasErrorsForValidatorId(String id) {
|
||||
return getErrors().stream().anyMatch(e -> e.getValidatorId().equals(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Set} of {@link ValidationError ValidationError's} from the {@link Validator} with the given {@code id} if present, otherwise an empty {@link Set} is returned.
|
||||
* <p>
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
public Set<ValidationError> getErrorsForValidatorId(String id) {
|
||||
return getErrors().stream().filter(e -> e.getValidatorId().equals(id)).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this {@link ValidationResult} contains {@link ValidationError ValidationError's} with the given {@code inputHint}.
|
||||
* <p>
|
||||
* This can be used to test if there are {@link ValidationError ValidationError's} for a specified attribute or attribute path.
|
||||
*
|
||||
* @param inputHint
|
||||
* @return
|
||||
*/
|
||||
public boolean hasErrorsForInputHint(String inputHint) {
|
||||
return getErrors().stream().anyMatch(e -> e.getInputHint().equals(inputHint));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Set} of {@link ValidationError ValidationError's} with the given {@code inputHint} if present, otherwise an empty {@link Set} is returned.
|
||||
* <p>
|
||||
*
|
||||
* @param inputHint
|
||||
* @return
|
||||
*/
|
||||
public Set<ValidationError> getErrorsForInputHint(String inputHint) {
|
||||
return getErrors().stream().filter(e -> e.getInputHint().equals(inputHint)).collect(Collectors.toSet());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Validates given input in a {@link ValidationContext}.
|
||||
* <p>
|
||||
* Validations can be supported with an optional {@code inputHint}, which could denote a reference to a potentially
|
||||
* nested attribute of an object to validate.
|
||||
* <p>
|
||||
* Validations can be configured with an optional {@code config} {@link Map}.
|
||||
*/
|
||||
public interface Validator extends Provider {
|
||||
|
||||
/**
|
||||
* Validates the given {@code input}.
|
||||
*
|
||||
* @param input the value to validate
|
||||
* @return the validation context with the outcome of the validation
|
||||
*/
|
||||
default ValidationContext validate(Object input) {
|
||||
return validate(input, "input", new ValidationContext(), ValidatorConfig.EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given {@code input} with an additional {@code config}.
|
||||
*
|
||||
* @param input the value to validate
|
||||
* @param config parameterization for the current validation
|
||||
* @return the validation context with the outcome of the validation
|
||||
*/
|
||||
default ValidationContext validate(Object input, ValidatorConfig config) {
|
||||
return validate(input, "input", new ValidationContext(), config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given {@code input}.
|
||||
*
|
||||
* @param input the value to validate
|
||||
* @param context the validation context
|
||||
* @return the validation context with the outcome of the validation
|
||||
*/
|
||||
default ValidationContext validate(Object input, ValidationContext context) {
|
||||
return validate(input, "input", context, ValidatorConfig.EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given {@code input} with an additional {@code inputHint}.
|
||||
*
|
||||
* @param input the value to validate
|
||||
* @param inputHint an optional input hint to guide the validation
|
||||
* @return the validation context with the outcome of the validation
|
||||
*/
|
||||
default ValidationContext validate(Object input, String inputHint) {
|
||||
return validate(input, inputHint, new ValidationContext(), ValidatorConfig.EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given {@code input} with an additional {@code inputHint}.
|
||||
*
|
||||
* @param input the value to validate
|
||||
* @param inputHint an optional input hint to guide the validation
|
||||
* @param config parameterization for the current validation
|
||||
* @return the validation context with the outcome of the validation
|
||||
*/
|
||||
default ValidationContext validate(Object input, String inputHint, ValidatorConfig config) {
|
||||
return validate(input, inputHint, new ValidationContext(), config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given {@code input} with an additional {@code inputHint}.
|
||||
*
|
||||
* @param input the value to validate
|
||||
* @param inputHint an optional input hint to guide the validation
|
||||
* @param context the validation context
|
||||
* @return the validation context with the outcome of the validation
|
||||
*/
|
||||
default ValidationContext validate(Object input, String inputHint, ValidationContext context) {
|
||||
return validate(input, inputHint, context, ValidatorConfig.EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given {@code input} with an additional {@code inputHint} and {@code config}.
|
||||
*
|
||||
* @param input the value to validate
|
||||
* @param inputHint an optional input hint to guide the validation
|
||||
* @param context the validation context
|
||||
* @param config parameterization for the current validation
|
||||
* @return the validation context with the outcome of the validation
|
||||
*/
|
||||
ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config);
|
||||
|
||||
default void close() {
|
||||
// NOOP
|
||||
}
|
||||
}
|
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A typed wrapper around a {@link Map} based {@link Validator} configuration.
|
||||
*/
|
||||
public class ValidatorConfig {
|
||||
|
||||
/**
|
||||
* An empty {@link ValidatorConfig}.
|
||||
*/
|
||||
public static final ValidatorConfig EMPTY = new ValidatorConfig(Collections.emptyMap());
|
||||
|
||||
/**
|
||||
* Holds the backing map for the {@link Validator} config.
|
||||
*/
|
||||
private final Map<String, Object> config;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ValidatorConfig} from the given {@code map}.
|
||||
*
|
||||
* @param config
|
||||
*/
|
||||
public ValidatorConfig(Map<String, Object> config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper to create a {@link ValidatorConfig} from the given {@code map}.
|
||||
*
|
||||
* @param map
|
||||
* @return
|
||||
*/
|
||||
public static ValidatorConfig configFromMap(Map<String, Object> map) {
|
||||
if (map == null || map.isEmpty()) {
|
||||
return EMPTY;
|
||||
}
|
||||
return new ValidatorConfig(map);
|
||||
}
|
||||
|
||||
public boolean containsKey(String key) {
|
||||
return config.containsKey(key);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return config.size();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return config.isEmpty();
|
||||
}
|
||||
|
||||
public Object get(String key) {
|
||||
return config.get(key);
|
||||
}
|
||||
|
||||
public Object getOrDefault(String key, Object defaultValue) {
|
||||
return config.getOrDefault(key, defaultValue);
|
||||
}
|
||||
|
||||
public String getString(String key) {
|
||||
return getStringOrDefault(key, null);
|
||||
}
|
||||
|
||||
public String getStringOrDefault(String key, String defaultValue) {
|
||||
Object value = config.get(key);
|
||||
if (value instanceof String) {
|
||||
return (String) value;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public Integer getInt(String key) {
|
||||
return getIntOrDefault(key, null);
|
||||
}
|
||||
|
||||
public Integer getIntOrDefault(String key, Integer defaultValue) {
|
||||
Object value = config.get(key);
|
||||
if (value instanceof Integer) {
|
||||
return (Integer) value;
|
||||
} else if (value instanceof Number) {
|
||||
return ((Number) value).intValue();
|
||||
} else if (value instanceof String) {
|
||||
try {
|
||||
return new Integer((String) value);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public Long getLong(String key) {
|
||||
return getLongOrDefault(key, null);
|
||||
}
|
||||
|
||||
public Long getLongOrDefault(String key, Long defaultValue) {
|
||||
Object value = config.get(key);
|
||||
if (value instanceof Long) {
|
||||
return (Long) value;
|
||||
} else if (value instanceof Number) {
|
||||
return ((Number) value).longValue();
|
||||
} else if (value instanceof String) {
|
||||
try {
|
||||
return new Long((String) value);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public Double getDouble(String key) {
|
||||
return getDoubleOrDefault(key, null);
|
||||
}
|
||||
|
||||
public Double getDoubleOrDefault(String key, Double defaultValue) {
|
||||
Object value = config.get(key);
|
||||
if (value instanceof Double) {
|
||||
return (Double) value;
|
||||
} else if (value instanceof Number) {
|
||||
return ((Number) value).doubleValue();
|
||||
} else if (value instanceof String) {
|
||||
try {
|
||||
return Double.parseDouble((String) value);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public Boolean getBoolean(String key) {
|
||||
return getBooleanOrDefault(key, null);
|
||||
}
|
||||
|
||||
public Boolean getBooleanOrDefault(String key, Boolean defaultValue) {
|
||||
Object value = config.get(key);
|
||||
if (value instanceof Boolean) {
|
||||
return (Boolean) value;
|
||||
} else if (value instanceof String) {
|
||||
return Boolean.parseBoolean((String) value);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public Set<String> getStringSet(String key) {
|
||||
return getStringSetOrDefault(key, null);
|
||||
}
|
||||
|
||||
public Set<String> getStringSetOrDefault(String key, Set<String> defaultValue) {
|
||||
Object value = config.get(key);
|
||||
if (value instanceof Set) {
|
||||
return (Set<String>) value;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public List<String> getStringListOrDefault(String key) {
|
||||
return getStringListOrDefault(key, null);
|
||||
}
|
||||
|
||||
public List<String> getStringListOrDefault(String key, List<String> defaultValue) {
|
||||
Object value = config.get(key);
|
||||
if (value instanceof List) {
|
||||
return (List<String>) value;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get regex Pattern from the configuration. String can be used and it is compiled into Pattern.
|
||||
*
|
||||
* @param key to get
|
||||
* @return Pattern or null
|
||||
*/
|
||||
public Pattern getPattern(String key) {
|
||||
return getPatternOrDefault(key, null);
|
||||
}
|
||||
|
||||
public Pattern getPatternOrDefault(String key, Pattern defaultValue) {
|
||||
Object value = config.get(key);
|
||||
if (value instanceof Pattern) {
|
||||
return (Pattern) value;
|
||||
} else if (value instanceof String) {
|
||||
return Pattern.compile((String) value);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public static ValidatorConfigBuilder builder() {
|
||||
return new ValidatorConfigBuilder();
|
||||
}
|
||||
|
||||
public static class ValidatorConfigBuilder {
|
||||
|
||||
private Map<String, Object> config = new HashMap<>();
|
||||
|
||||
public ValidatorConfig build() {
|
||||
return ValidatorConfig.configFromMap(this.config);
|
||||
}
|
||||
|
||||
public ValidatorConfigBuilder config(String name, Object value) {
|
||||
config.put(name, value);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ValidatorConfig{" + "config=" + config + '}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
/**
|
||||
* A factory for custom {@link Validator} implementations plugged-in through this SPI.
|
||||
*/
|
||||
public interface ValidatorFactory extends ProviderFactory<Validator> {
|
||||
|
||||
/**
|
||||
* Validates the given validation config.
|
||||
* <p>
|
||||
* Implementations can use the {@link KeycloakSession} to validate the given {@link ValidatorConfig}.
|
||||
*
|
||||
* @param session the {@link KeycloakSession}
|
||||
* @param config the config to be validated
|
||||
* @return the validation result
|
||||
*/
|
||||
default ValidationResult validateConfig(KeycloakSession session, ValidatorConfig config) {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when the server shuts down.
|
||||
*/
|
||||
@Override
|
||||
default void close() {
|
||||
// NOOP
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
/**
|
||||
* An {@link Spi} for custom {@link Validator} implementations.
|
||||
*/
|
||||
public class ValidatorSPI implements Spi {
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
// this API is internal for now, but is intended to be public later.
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "validator";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return Validator.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return ValidatorFactory.class;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.validate.validators.EmailValidator;
|
||||
import org.keycloak.validate.validators.IntegerValidator;
|
||||
import org.keycloak.validate.validators.LengthValidator;
|
||||
import org.keycloak.validate.validators.NotBlankValidator;
|
||||
import org.keycloak.validate.validators.NotEmptyValidator;
|
||||
import org.keycloak.validate.validators.DoubleValidator;
|
||||
import org.keycloak.validate.validators.PatternValidator;
|
||||
import org.keycloak.validate.validators.UriValidator;
|
||||
import org.keycloak.validate.validators.ValidatorConfigValidator;
|
||||
|
||||
/**
|
||||
* Facade for Validation functions with support for {@link Validator} implementation lookup by id.
|
||||
*/
|
||||
public class Validators {
|
||||
|
||||
/**
|
||||
* Holds a mapping of internal {@link SimpleValidator} to allow look-up via provider id.
|
||||
*/
|
||||
private static final Map<String, SimpleValidator> INTERNAL_VALIDATORS;
|
||||
|
||||
static {
|
||||
List<SimpleValidator> list = Arrays.asList(
|
||||
LengthValidator.INSTANCE,
|
||||
NotEmptyValidator.INSTANCE,
|
||||
UriValidator.INSTANCE,
|
||||
EmailValidator.INSTANCE,
|
||||
NotBlankValidator.INSTANCE,
|
||||
PatternValidator.INSTANCE,
|
||||
DoubleValidator.INSTANCE,
|
||||
IntegerValidator.INSTANCE,
|
||||
ValidatorConfigValidator.INSTANCE
|
||||
);
|
||||
|
||||
INTERNAL_VALIDATORS = list.stream().collect(Collectors.toMap(SimpleValidator::getId, v -> v));
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the {@link KeycloakSession}.
|
||||
*/
|
||||
private final KeycloakSession session;
|
||||
|
||||
/**
|
||||
* Creates a new {@link Validators} instance with the given {@link KeycloakSession}.
|
||||
*
|
||||
* @param session
|
||||
*/
|
||||
public Validators(KeycloakSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look-up for a built-in or registered {@link Validator} with the given provider {@code id}.
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
* @see #validator(KeycloakSession, String)
|
||||
*/
|
||||
public Validator validator(String id) {
|
||||
return validator(session, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look-up for a built-in or registered {@link ValidatorFactory} with the given provider {@code id}.
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
* @see #validatorFactory(KeycloakSession, String)
|
||||
*/
|
||||
public ValidatorFactory validatorFactory(String id) {
|
||||
return validatorFactory(session, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the {@link ValidatorConfig} of {@link Validator} referenced by the given provider {@code id}.
|
||||
*
|
||||
* @param id
|
||||
* @param config
|
||||
* @return
|
||||
* @see #validateConfig(KeycloakSession, String, ValidatorConfig)
|
||||
*/
|
||||
public ValidationResult validateConfig(String id, ValidatorConfig config) {
|
||||
return validateConfig(session, id, config);
|
||||
}
|
||||
|
||||
/* static import friendly accessor methods for built-in validators */
|
||||
|
||||
public static Validator getInternalValidatorById(String id) {
|
||||
return INTERNAL_VALIDATORS.get(id);
|
||||
}
|
||||
|
||||
public static ValidatorFactory getInternalValidatorFactoryById(String id) {
|
||||
return INTERNAL_VALIDATORS.get(id);
|
||||
}
|
||||
|
||||
public static Map<String, Validator> getInternalValidators() {
|
||||
return Collections.unmodifiableMap(INTERNAL_VALIDATORS);
|
||||
}
|
||||
|
||||
public static NotBlankValidator notBlankValidator() {
|
||||
return NotBlankValidator.INSTANCE;
|
||||
}
|
||||
|
||||
public static NotEmptyValidator notEmptyValidator() {
|
||||
return NotEmptyValidator.INSTANCE;
|
||||
}
|
||||
|
||||
public static LengthValidator lengthValidator() {
|
||||
return LengthValidator.INSTANCE;
|
||||
}
|
||||
|
||||
public static UriValidator uriValidator() {
|
||||
return UriValidator.INSTANCE;
|
||||
}
|
||||
|
||||
public static EmailValidator emailValidator() {
|
||||
return EmailValidator.INSTANCE;
|
||||
}
|
||||
|
||||
public static PatternValidator patternValidator() {
|
||||
return PatternValidator.INSTANCE;
|
||||
}
|
||||
|
||||
public static DoubleValidator doubleValidator() {
|
||||
return DoubleValidator.INSTANCE;
|
||||
}
|
||||
|
||||
public static IntegerValidator integerValidator() {
|
||||
return IntegerValidator.INSTANCE;
|
||||
}
|
||||
|
||||
public static ValidatorConfigValidator validatorConfigValidator() {
|
||||
return ValidatorConfigValidator.INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look-up up for a built-in or registered {@link Validator} with the given validatorId.
|
||||
*
|
||||
* @param session the {@link KeycloakSession}
|
||||
* @param id the id of the validator
|
||||
* @return the {@link Validator} or {@literal null}
|
||||
*/
|
||||
public static Validator validator(KeycloakSession session, String id) {
|
||||
|
||||
// Fast-path for internal Validators
|
||||
Validator validator = getInternalValidatorById(id);
|
||||
if (validator != null) {
|
||||
return validator;
|
||||
}
|
||||
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Lookup validator in registry
|
||||
return session.getProvider(Validator.class, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look-up for a built-in or registered {@link ValidatorFactory} with the given validatorId.
|
||||
* <p>
|
||||
* This is intended for users who want to dynamically create new {@link Validator} instances, validate
|
||||
* {@link ValidatorConfig} configurations or create default configurations for a {@link Validator}.
|
||||
*
|
||||
* @param session the {@link KeycloakSession}
|
||||
* @param id the id of the validator
|
||||
* @return the {@link Validator} or {@literal null}
|
||||
*/
|
||||
public static ValidatorFactory validatorFactory(KeycloakSession session, String id) {
|
||||
|
||||
// Fast-path for internal Validators
|
||||
ValidatorFactory factory = getInternalValidatorFactoryById(id);
|
||||
if (factory != null) {
|
||||
return factory;
|
||||
}
|
||||
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Lookup factory in registry
|
||||
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
|
||||
return (ValidatorFactory) sessionFactory.getProviderFactory(Validator.class, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the {@link ValidatorConfig} of {@link Validator} referenced by the given provider {@code id}.
|
||||
*
|
||||
* @param session
|
||||
* @param id of the validator
|
||||
* @param config to be validated
|
||||
* @return
|
||||
*/
|
||||
public static ValidationResult validateConfig(KeycloakSession session, String id, ValidatorConfig config) {
|
||||
|
||||
ValidatorFactory validatorFactory = validatorFactory(session, id);
|
||||
if (validatorFactory != null) {
|
||||
return validatorFactory.validateConfig(session, config);
|
||||
}
|
||||
|
||||
// We could not find a ValidationFactory to validate that config, so we assume the config is valid.
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate.validators;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.validate.AbstractSimpleValidator;
|
||||
import org.keycloak.validate.ValidationContext;
|
||||
import org.keycloak.validate.ValidationError;
|
||||
import org.keycloak.validate.ValidationResult;
|
||||
import org.keycloak.validate.ValidatorConfig;
|
||||
|
||||
/**
|
||||
* Abstract class for number validator. Supports min and max value validations using {@link #KEY_MIN} and
|
||||
* {@link #KEY_MAX} config options.
|
||||
*
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*/
|
||||
public abstract class AbstractNumberValidator extends AbstractSimpleValidator {
|
||||
|
||||
public static final String MESSAGE_INVALID_NUMBER = "error-invalid-number";
|
||||
public static final String MESSAGE_NUMBER_OUT_OF_RANGE = "error-number-out-of-range";
|
||||
|
||||
public static final String KEY_MIN = "min";
|
||||
public static final String KEY_MAX = "max";
|
||||
|
||||
private final ValidatorConfig defaultConfig;
|
||||
|
||||
public AbstractNumberValidator() {
|
||||
// for reflection
|
||||
this(ValidatorConfig.EMPTY);
|
||||
}
|
||||
|
||||
public AbstractNumberValidator(ValidatorConfig config) {
|
||||
this.defaultConfig = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doValidate(Object value, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||
if (config == null || config.isEmpty()) {
|
||||
config = defaultConfig;
|
||||
}
|
||||
|
||||
Number number;
|
||||
|
||||
try {
|
||||
number = convert(value, config);
|
||||
} catch (NumberFormatException ignore) {
|
||||
context.addError(new ValidationError(getId(), inputHint, MESSAGE_INVALID_NUMBER, value));
|
||||
return;
|
||||
}
|
||||
|
||||
Number min = getMinMaxConfig(config, KEY_MIN);
|
||||
Number max = getMinMaxConfig(config, KEY_MAX);
|
||||
|
||||
if (min != null && isFirstGreaterThanToSecond(min, number)) {
|
||||
context.addError(new ValidationError(getId(), inputHint, MESSAGE_NUMBER_OUT_OF_RANGE, value, min, max));
|
||||
return;
|
||||
}
|
||||
|
||||
if (max != null && isFirstGreaterThanToSecond(number, max)) {
|
||||
context.addError(new ValidationError(getId(), inputHint, MESSAGE_NUMBER_OUT_OF_RANGE, value, min, max));
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationResult validateConfig(KeycloakSession session, ValidatorConfig config) {
|
||||
Set<ValidationError> errors = new LinkedHashSet<>();
|
||||
|
||||
if (config != null) {
|
||||
boolean containsMin = config.containsKey(KEY_MIN);
|
||||
boolean containsMax = config.containsKey(KEY_MAX);
|
||||
|
||||
Number min = getMinMaxConfig(config, KEY_MIN);
|
||||
Number max = getMinMaxConfig(config, KEY_MAX);
|
||||
|
||||
if (containsMin && min == null) {
|
||||
errors.add(new ValidationError(getId(), KEY_MIN, ValidatorConfigValidator.MESSAGE_CONFIG_INVALID_NUMBER_VALUE, config.get(KEY_MIN)));
|
||||
}
|
||||
|
||||
if (containsMax && max == null) {
|
||||
errors.add(new ValidationError(getId(), KEY_MAX, ValidatorConfigValidator.MESSAGE_CONFIG_INVALID_NUMBER_VALUE, config.get(KEY_MAX)));
|
||||
}
|
||||
|
||||
if (errors.isEmpty() && containsMin && containsMax && (!isFirstGreaterThanToSecond(max, min))) {
|
||||
errors.add(new ValidationError(getId(), KEY_MAX, ValidatorConfigValidator.MESSAGE_CONFIG_INVALID_VALUE));
|
||||
}
|
||||
}
|
||||
|
||||
ValidationResult s = super.validateConfig(session, config);
|
||||
|
||||
if (!s.isValid()) {
|
||||
errors.addAll(s.getErrors());
|
||||
}
|
||||
|
||||
return new ValidationResult(errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert input value to instance of Number supported by this validator.
|
||||
*
|
||||
* @param value to convert
|
||||
* @param config
|
||||
* @return value converted to supported Number instance
|
||||
* @throws NumberFormatException if value is not convertible to supported Number instance so
|
||||
* {@link #MESSAGE_INVALID_NUMBER} error is reported.
|
||||
*/
|
||||
protected abstract Number convert(Object value, ValidatorConfig config);
|
||||
|
||||
/**
|
||||
* Get config value for min and max validation bound as a Number supported by this validator
|
||||
*
|
||||
* @param config to get from
|
||||
* @param key of the config value
|
||||
* @return bound value or null
|
||||
*/
|
||||
protected abstract Number getMinMaxConfig(ValidatorConfig config, String key);
|
||||
|
||||
/**
|
||||
* Compare two numbers of supported type (fed by {@link #convert(Object, ValidatorConfig)} and
|
||||
* {@link #getMinMaxConfig(ValidatorConfig, String)} )
|
||||
*
|
||||
* @param n1
|
||||
* @param n2
|
||||
* @return true if first number is greater than second
|
||||
*/
|
||||
protected abstract boolean isFirstGreaterThanToSecond(Number n1, Number n2);
|
||||
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate.validators;
|
||||
|
||||
import org.keycloak.validate.ValidatorConfig;
|
||||
|
||||
/**
|
||||
* Validate input being any kind of {@link Number}. Accepts String also if convertible to {@link Double} by common
|
||||
* {@link Double#parseDouble(String)}. Min and Max validation is based on {@link Double} precision also.
|
||||
*
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*/
|
||||
public class DoubleValidator extends AbstractNumberValidator {
|
||||
|
||||
public static final String ID = "double";
|
||||
|
||||
public static final DoubleValidator INSTANCE = new DoubleValidator();
|
||||
|
||||
public DoubleValidator() {
|
||||
super();
|
||||
}
|
||||
|
||||
public DoubleValidator(ValidatorConfig config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Number convert(Object value, ValidatorConfig config) {
|
||||
if (value instanceof Number) {
|
||||
return (Number) value;
|
||||
}
|
||||
return new Double(value.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Number getMinMaxConfig(ValidatorConfig config, String key) {
|
||||
return config != null ? config.getDouble(key) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isFirstGreaterThanToSecond(Number n1, Number n2) {
|
||||
return n1.doubleValue() > n2.doubleValue();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate.validators;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.keycloak.validate.AbstractStringValidator;
|
||||
import org.keycloak.validate.ValidationContext;
|
||||
import org.keycloak.validate.ValidationError;
|
||||
import org.keycloak.validate.ValidatorConfig;
|
||||
|
||||
/**
|
||||
* Email format validation - accepts plain string and collection of strings, for basic behavior like null/blank values
|
||||
* handling and collections support see {@link AbstractStringValidator}.
|
||||
*/
|
||||
public class EmailValidator extends AbstractStringValidator {
|
||||
|
||||
public static final String ID = "email";
|
||||
|
||||
public static final EmailValidator INSTANCE = new EmailValidator();
|
||||
|
||||
public static final String MESSAGE_INVALID_EMAIL = "error-invalid-email";
|
||||
|
||||
// Actually allow same emails like angular. See ValidationTest.testEmailValidation()
|
||||
private static final Pattern EMAIL_PATTERN = Pattern.compile("[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*");
|
||||
|
||||
private EmailValidator() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doValidate(String value, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||
if (!EMAIL_PATTERN.matcher(value).matches()) {
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_EMAIL, value));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate.validators;
|
||||
|
||||
import org.keycloak.validate.ValidatorConfig;
|
||||
|
||||
/**
|
||||
*
|
||||
* Validate input being integer number {@link Integer} or {@link Long}. Accepts String also if convertible to
|
||||
* {@link Long} by common {@link Long#parseLong(String)} operation.
|
||||
*
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*/
|
||||
public class IntegerValidator extends AbstractNumberValidator {
|
||||
|
||||
public static final String ID = "integer";
|
||||
public static final IntegerValidator INSTANCE = new IntegerValidator();
|
||||
|
||||
public IntegerValidator() {
|
||||
super();
|
||||
}
|
||||
|
||||
public IntegerValidator(ValidatorConfig config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Number convert(Object value, ValidatorConfig config) {
|
||||
if (value instanceof Integer || value instanceof Long) {
|
||||
return (Number) value;
|
||||
}
|
||||
return new Long(value.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Number getMinMaxConfig(ValidatorConfig config, String key) {
|
||||
return config != null ? config.getLong(key) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isFirstGreaterThanToSecond(Number n1, Number n2) {
|
||||
return n1.longValue() > n2.longValue();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate.validators;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.validate.AbstractStringValidator;
|
||||
import org.keycloak.validate.ValidationContext;
|
||||
import org.keycloak.validate.ValidationError;
|
||||
import org.keycloak.validate.ValidationResult;
|
||||
import org.keycloak.validate.ValidatorConfig;
|
||||
|
||||
/**
|
||||
* String value length validation - accepts plain string and collection of strings, for basic behavior like null/blank
|
||||
* values handling and collections support see {@link AbstractStringValidator}. Validator trims String value before the
|
||||
* length validation, can be disabled by {@link #KEY_TRIM_DISABLED} boolean configuration entry set to
|
||||
* <code>true</code>.
|
||||
* <p>
|
||||
* Configuration have to be always provided, with at least one of {@link #KEY_MIN} and {@link #KEY_MAX}.
|
||||
*/
|
||||
public class LengthValidator extends AbstractStringValidator {
|
||||
|
||||
public static final LengthValidator INSTANCE = new LengthValidator();
|
||||
|
||||
public static final String ID = "length";
|
||||
|
||||
public static final String MESSAGE_INVALID_LENGTH = "error-invalid-length";
|
||||
|
||||
public static final String KEY_MIN = "min";
|
||||
public static final String KEY_MAX = "max";
|
||||
public static final String KEY_TRIM_DISABLED = "trim-disabled";
|
||||
|
||||
private LengthValidator() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doValidate(String value, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||
Integer min = config.getInt(KEY_MIN);
|
||||
Integer max = config.getInt(KEY_MAX);
|
||||
|
||||
if (!config.getBooleanOrDefault(KEY_TRIM_DISABLED, Boolean.FALSE)) {
|
||||
value = value.trim();
|
||||
}
|
||||
|
||||
int length = value.length();
|
||||
|
||||
if (config.containsKey(KEY_MIN) && length < min.intValue()) {
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_LENGTH, value, min, max));
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.containsKey(KEY_MAX) && length > max.intValue()) {
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_LENGTH, value, min, max));
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationResult validateConfig(KeycloakSession session, ValidatorConfig config) {
|
||||
|
||||
Set<ValidationError> errors = new LinkedHashSet<>();
|
||||
if (config == null || config == ValidatorConfig.EMPTY) {
|
||||
errors.add(new ValidationError(ID, KEY_MIN, ValidatorConfigValidator.MESSAGE_CONFIG_MISSING_VALUE));
|
||||
errors.add(new ValidationError(ID, KEY_MAX, ValidatorConfigValidator.MESSAGE_CONFIG_MISSING_VALUE));
|
||||
} else {
|
||||
|
||||
if (config.containsKey(KEY_TRIM_DISABLED) && (config.getBoolean(KEY_TRIM_DISABLED) == null)) {
|
||||
errors.add(new ValidationError(ID, KEY_TRIM_DISABLED, ValidatorConfigValidator.MESSAGE_CONFIG_INVALID_BOOLEAN_VALUE, config.get(KEY_TRIM_DISABLED)));
|
||||
}
|
||||
|
||||
boolean containsMin = config.containsKey(KEY_MIN);
|
||||
boolean containsMax = config.containsKey(KEY_MAX);
|
||||
|
||||
if (!(containsMin || containsMax)) {
|
||||
errors.add(new ValidationError(ID, KEY_MIN, ValidatorConfigValidator.MESSAGE_CONFIG_MISSING_VALUE));
|
||||
errors.add(new ValidationError(ID, KEY_MAX, ValidatorConfigValidator.MESSAGE_CONFIG_MISSING_VALUE));
|
||||
} else {
|
||||
|
||||
if (containsMin && config.getInt(KEY_MIN) == null) {
|
||||
errors.add(new ValidationError(ID, KEY_MIN, ValidatorConfigValidator.MESSAGE_CONFIG_INVALID_NUMBER_VALUE, config.get(KEY_MIN)));
|
||||
}
|
||||
|
||||
if (containsMax && config.getInt(KEY_MAX) == null) {
|
||||
errors.add(new ValidationError(ID, KEY_MAX, ValidatorConfigValidator.MESSAGE_CONFIG_INVALID_NUMBER_VALUE, config.get(KEY_MAX)));
|
||||
}
|
||||
|
||||
if (errors.isEmpty() && containsMin && containsMax && (config.getInt(KEY_MIN) > config.getInt(KEY_MAX))) {
|
||||
errors.add(new ValidationError(ID, KEY_MAX, ValidatorConfigValidator.MESSAGE_CONFIG_INVALID_VALUE));
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ValidationResult(errors);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate.validators;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.keycloak.validate.SimpleValidator;
|
||||
import org.keycloak.validate.ValidationContext;
|
||||
import org.keycloak.validate.ValidationError;
|
||||
import org.keycloak.validate.ValidatorConfig;
|
||||
|
||||
/**
|
||||
* Validate that value exists and is not empty nor blank. Supports String and collection of Strings as input. For
|
||||
* collection of Strings input has to contain at least one element and it have to be non-blank to satisfy this
|
||||
* validation. If collection contains something else than String, or if even one String in it is blank, then this
|
||||
* validation fails.
|
||||
*
|
||||
* @see NotEmptyValidator
|
||||
*/
|
||||
public class NotBlankValidator implements SimpleValidator {
|
||||
|
||||
public static final String ID = "blank";
|
||||
|
||||
public static final String MESSAGE_BLANK = "error-invalid-blank";
|
||||
|
||||
public static final NotBlankValidator INSTANCE = new NotBlankValidator();
|
||||
|
||||
private NotBlankValidator() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||
|
||||
if (input == null) {
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_BLANK, input));
|
||||
} else if (input instanceof String) {
|
||||
validateStringValue((String) input, inputHint, context, config);
|
||||
} else if (input instanceof Collection) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Collection<Object> values = (Collection<Object>) input;
|
||||
if (!values.isEmpty()) {
|
||||
for (Object value : values) {
|
||||
if (!(value instanceof String)) {
|
||||
context.addError(new ValidationError(getId(), inputHint, ValidationError.MESSAGE_INVALID_VALUE, input));
|
||||
return context;
|
||||
} else if (!validateStringValue((String) value, inputHint, context, config)) {
|
||||
return context;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_BLANK, input));
|
||||
}
|
||||
} else {
|
||||
context.addError(new ValidationError(ID, inputHint, ValidationError.MESSAGE_INVALID_VALUE, input));
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
protected boolean validateStringValue(String value, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||
if (value == null || value.trim().length() == 0) {
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_BLANK, value));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate.validators;
|
||||
|
||||
import org.keycloak.validate.SimpleValidator;
|
||||
import org.keycloak.validate.ValidationContext;
|
||||
import org.keycloak.validate.ValidationError;
|
||||
import org.keycloak.validate.ValidatorConfig;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Check that input value is not empty. It means it is not null for all data types. For String it also have to be
|
||||
* non-empty string (no trim() performed). For {@link Collection} and {@link Map} it also means it is not empty.
|
||||
*
|
||||
* @see NotBlankValidator
|
||||
*/
|
||||
public class NotEmptyValidator implements SimpleValidator {
|
||||
|
||||
public static final NotEmptyValidator INSTANCE = new NotEmptyValidator();
|
||||
|
||||
public static final String ID = "not-empty";
|
||||
|
||||
public static final String MESSAGE_ERROR_EMPTY = "error-empty";
|
||||
|
||||
private NotEmptyValidator() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||
|
||||
if (input == null) {
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_ERROR_EMPTY, input));
|
||||
return context;
|
||||
}
|
||||
|
||||
if (input instanceof String) {
|
||||
if (((String) input).length() == 0) {
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_ERROR_EMPTY, input));
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
if (input instanceof Collection) {
|
||||
if (((Collection<?>) input).isEmpty()) {
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_ERROR_EMPTY, input));
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
if (input instanceof Map) {
|
||||
if (((Map<?, ?>) input).isEmpty()) {
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_ERROR_EMPTY, input));
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
context.addError(new ValidationError(ID, inputHint, ValidationError.MESSAGE_INVALID_VALUE, input));
|
||||
return context;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate.validators;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.validate.AbstractStringValidator;
|
||||
import org.keycloak.validate.ValidationContext;
|
||||
import org.keycloak.validate.ValidationError;
|
||||
import org.keycloak.validate.ValidationResult;
|
||||
import org.keycloak.validate.ValidatorConfig;
|
||||
|
||||
/**
|
||||
* Validate String against configured RegEx pattern - accepts plain string and collection of strings, for basic behavior
|
||||
* like null/blank values handling and collections support see {@link AbstractStringValidator}.
|
||||
*/
|
||||
public class PatternValidator extends AbstractStringValidator {
|
||||
|
||||
public static final String ID = "pattern";
|
||||
|
||||
public static final PatternValidator INSTANCE = new PatternValidator();
|
||||
|
||||
public static final String KEY_PATTERN = "pattern";
|
||||
|
||||
public static final String MESSAGE_NO_MATCH = "error-pattern-no-match";
|
||||
|
||||
private PatternValidator() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doValidate(String value, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||
Pattern pattern = config.getPattern(KEY_PATTERN);
|
||||
|
||||
if (!pattern.matcher(value).matches()) {
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_NO_MATCH, value, config.getString(KEY_PATTERN)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationResult validateConfig(KeycloakSession session, ValidatorConfig config) {
|
||||
Set<ValidationError> errors = new LinkedHashSet<>();
|
||||
|
||||
if (config == null || config == ValidatorConfig.EMPTY || !config.containsKey(KEY_PATTERN)) {
|
||||
errors.add(new ValidationError(ID, KEY_PATTERN, ValidatorConfigValidator.MESSAGE_CONFIG_MISSING_VALUE));
|
||||
} else {
|
||||
Object maybePattern = config.get(KEY_PATTERN);
|
||||
try {
|
||||
Pattern pattern = config.getPattern(KEY_PATTERN);
|
||||
if (pattern == null) {
|
||||
errors.add(new ValidationError(ID, KEY_PATTERN, ValidatorConfigValidator.MESSAGE_CONFIG_INVALID_VALUE, maybePattern));
|
||||
}
|
||||
} catch (PatternSyntaxException pse) {
|
||||
errors.add(new ValidationError(ID, KEY_PATTERN, ValidatorConfigValidator.MESSAGE_CONFIG_INVALID_VALUE, maybePattern));
|
||||
}
|
||||
}
|
||||
return new ValidationResult(errors);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate.validators;
|
||||
|
||||
import org.keycloak.validate.SimpleValidator;
|
||||
import org.keycloak.validate.ValidationContext;
|
||||
import org.keycloak.validate.ValidationError;
|
||||
import org.keycloak.validate.ValidatorConfig;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* URI validation - accepts {@link URI}, {@link URL} and single String. Null input is valid, use other validators (like
|
||||
* {@link NotBlankValidator} or {@link NotEmptyValidator} to force field as required.
|
||||
*/
|
||||
public class UriValidator implements SimpleValidator {
|
||||
|
||||
public static final UriValidator INSTANCE = new UriValidator();
|
||||
|
||||
public static final String KEY_ALLOWED_SCHEMES = "allowedSchemes";
|
||||
public static final String KEY_ALLOW_FRAGMENT = "allowFragment";
|
||||
public static final String KEY_REQUIRE_VALID_URL = "requireValidUrl";
|
||||
|
||||
public static final Set<String> DEFAULT_ALLOWED_SCHEMES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
|
||||
"http",
|
||||
"https"
|
||||
)));
|
||||
public static final String MESSAGE_INVALID_URI = "error-invalid-uri";
|
||||
public static final String MESSAGE_INVALID_SCHEME = "error-invalid-uri-scheme";
|
||||
public static final String MESSAGE_INVALID_FRAGMENT = "error-invalid-uri-fragment";
|
||||
|
||||
public static boolean DEFAULT_ALLOW_FRAGMENT = true;
|
||||
|
||||
public static boolean DEFAULT_REQUIRE_VALID_URL = true;
|
||||
|
||||
public static final String ID = "uri";
|
||||
|
||||
private UriValidator() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||
|
||||
if(input == null || (input instanceof String && ((String) input).isEmpty())) {
|
||||
return context;
|
||||
}
|
||||
|
||||
try {
|
||||
URI uri = toUri(input);
|
||||
|
||||
if (uri == null) {
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_URI, input));
|
||||
} else {
|
||||
Set<String> allowedSchemes = config.getStringSetOrDefault(KEY_ALLOWED_SCHEMES, DEFAULT_ALLOWED_SCHEMES);
|
||||
boolean allowFragment = config.getBooleanOrDefault(KEY_ALLOW_FRAGMENT, DEFAULT_ALLOW_FRAGMENT);
|
||||
boolean requireValidUrl = config.getBooleanOrDefault(KEY_REQUIRE_VALID_URL, DEFAULT_REQUIRE_VALID_URL);
|
||||
|
||||
validateUri(uri, inputHint, context, allowedSchemes, allowFragment, requireValidUrl);
|
||||
}
|
||||
} catch (MalformedURLException | IllegalArgumentException | URISyntaxException e) {
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_URI, input, e.getMessage()));
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private URI toUri(Object input) throws URISyntaxException {
|
||||
|
||||
if (input instanceof String) {
|
||||
String uriString = (String) input;
|
||||
return new URI(uriString);
|
||||
} else if (input instanceof URI) {
|
||||
return (URI) input;
|
||||
} else if (input instanceof URL) {
|
||||
return ((URL) input).toURI();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean validateUri(URI uri, Set<String> allowedSchemes, boolean allowFragment, boolean requireValidUrl) {
|
||||
try {
|
||||
return validateUri(uri, "url", new ValidationContext(), allowedSchemes, allowFragment, requireValidUrl);
|
||||
} catch (MalformedURLException mue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean validateUri(URI uri, String inputHint, ValidationContext context,
|
||||
Set<String> allowedSchemes, boolean allowFragment, boolean requireValidUrl)
|
||||
throws MalformedURLException {
|
||||
|
||||
boolean valid = true;
|
||||
if (uri.getScheme() != null && !allowedSchemes.contains(uri.getScheme())) {
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_SCHEME, uri, uri.getScheme()));
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (!allowFragment && uri.getFragment() != null) {
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_FRAGMENT, uri, uri.getFragment()));
|
||||
valid = false;
|
||||
}
|
||||
|
||||
// Don't check if URL is valid if there are other problems with it; otherwise it could lead to duplicate errors.
|
||||
// This cannot be moved higher because it acts on differently based on environment (e.g. sometimes it checks
|
||||
// scheme, sometimes it doesn't).
|
||||
if (requireValidUrl && valid) {
|
||||
URL ignored = uri.toURL(); // throws an exception
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate.validators;
|
||||
|
||||
import org.keycloak.validate.SimpleValidator;
|
||||
import org.keycloak.validate.ValidationContext;
|
||||
import org.keycloak.validate.ValidationError;
|
||||
import org.keycloak.validate.ValidatorConfig;
|
||||
import org.keycloak.validate.Validators;
|
||||
|
||||
/**
|
||||
* Validate that input value is {@link ValidatorConfig} and it is correct for validator (<code>inputHint</code> must be
|
||||
* ID of the validator config is for) by
|
||||
* {@link Validators#validateConfig(org.keycloak.models.KeycloakSession, String, ValidatorConfig)}. .
|
||||
*/
|
||||
public class ValidatorConfigValidator implements SimpleValidator {
|
||||
|
||||
/**
|
||||
* Generic error messages for config validations - missing config value
|
||||
*/
|
||||
public static final String MESSAGE_CONFIG_MISSING_VALUE = "error-validator-config-missing-value";
|
||||
/**
|
||||
* Generic error messages for config validations - invalid config value
|
||||
*/
|
||||
public static final String MESSAGE_CONFIG_INVALID_VALUE = "error-validator-config-invalid-value";
|
||||
|
||||
/**
|
||||
* Generic error messages for config validations - invalid config value - number expected
|
||||
*/
|
||||
public static final String MESSAGE_CONFIG_INVALID_NUMBER_VALUE = "error-validator-config-invalid-number-value";
|
||||
|
||||
/**
|
||||
* Generic error messages for config validations - invalid config value - boolean expected
|
||||
*/
|
||||
public static final String MESSAGE_CONFIG_INVALID_BOOLEAN_VALUE = "error-validator-config-invalid-boolean-value";
|
||||
|
||||
/**
|
||||
* Generic error messages for config validations - invalid config value - string expected
|
||||
*/
|
||||
public static final String MESSAGE_CONFIG_INVALID_STRING_VALUE = "error-validator-config-invalid-string-value";
|
||||
|
||||
public static final String ID = "validatorConfig";
|
||||
|
||||
public static final ValidatorConfigValidator INSTANCE = new ValidatorConfigValidator();
|
||||
|
||||
private ValidatorConfigValidator() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||
|
||||
if (input == null || input instanceof ValidatorConfig) {
|
||||
Validators.validateConfig(context.getSession(), inputHint, (ValidatorConfig) input).forEachError(context::addError);
|
||||
} else {
|
||||
context.addError(new ValidationError(ID, inputHint, ValidationError.MESSAGE_INVALID_VALUE));
|
||||
}
|
||||
return context;
|
||||
}
|
||||
}
|
|
@ -94,6 +94,7 @@ org.keycloak.vault.VaultSpi
|
|||
org.keycloak.crypto.CekManagementSpi
|
||||
org.keycloak.crypto.ContentEncryptionSpi
|
||||
org.keycloak.validation.ClientValidationSPI
|
||||
org.keycloak.validate.ValidatorSPI
|
||||
org.keycloak.headers.SecurityHeadersSpi
|
||||
org.keycloak.services.clientpolicy.condition.ClientPolicyConditionSpi
|
||||
org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorSpi
|
||||
|
|
|
@ -0,0 +1,371 @@
|
|||
package org.keycloak.validate;
|
||||
|
||||
import static org.keycloak.validate.ValidatorConfig.configFromMap;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.validate.validators.DoubleValidator;
|
||||
import org.keycloak.validate.validators.IntegerValidator;
|
||||
import org.keycloak.validate.validators.LengthValidator;
|
||||
import org.keycloak.validate.validators.PatternValidator;
|
||||
import org.keycloak.validate.validators.UriValidator;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
public class BuiltinValidatorsTest {
|
||||
|
||||
@Test
|
||||
public void validateLength() {
|
||||
|
||||
Validator validator = Validators.lengthValidator();
|
||||
|
||||
// null and empty values handling
|
||||
Assert.assertTrue(validator.validate(null, "name", configFromMap(ImmutableMap.of(LengthValidator.KEY_MIN, 1))).isValid());
|
||||
Assert.assertFalse(validator.validate("", "name", configFromMap(ImmutableMap.of(LengthValidator.KEY_MIN, 1))).isValid());
|
||||
Assert.assertFalse(validator.validate(" ", "name", configFromMap(ImmutableMap.of(LengthValidator.KEY_MIN, 1))).isValid());
|
||||
Assert.assertTrue(validator.validate(" ", "name", configFromMap(ImmutableMap.of(LengthValidator.KEY_MAX, 10))).isValid());
|
||||
|
||||
// min validation only
|
||||
Assert.assertTrue(validator.validate("tester", "name", configFromMap(ImmutableMap.of(LengthValidator.KEY_MIN, 1))).isValid());
|
||||
Assert.assertFalse(validator.validate("tester", "name", configFromMap(ImmutableMap.of(LengthValidator.KEY_MIN, 7))).isValid());
|
||||
|
||||
// max validation only
|
||||
Assert.assertTrue(validator.validate("tester", "name", configFromMap(ImmutableMap.of(LengthValidator.KEY_MAX, 8))).isValid());
|
||||
Assert.assertFalse(validator.validate("tester", "name", configFromMap(ImmutableMap.of(LengthValidator.KEY_MAX, 4))).isValid());
|
||||
|
||||
// both validations together
|
||||
ValidatorConfig config1 = configFromMap(ImmutableMap.of(LengthValidator.KEY_MIN, 3, LengthValidator.KEY_MAX, 4));
|
||||
Assert.assertFalse(validator.validate("te", "name", config1).isValid());
|
||||
Assert.assertTrue(validator.validate("tes", "name", config1).isValid());
|
||||
Assert.assertTrue(validator.validate("test", "name", config1).isValid());
|
||||
Assert.assertFalse(validator.validate("testr", "name", config1).isValid());
|
||||
|
||||
// test value trimming performed by default
|
||||
Assert.assertFalse("trim not performed", validator.validate("t ", "name", configFromMap(ImmutableMap.of(LengthValidator.KEY_MIN, 2))).isValid());
|
||||
Assert.assertFalse("trim not performed", validator.validate(" t", "name", configFromMap(ImmutableMap.of(LengthValidator.KEY_MIN, 2))).isValid());
|
||||
|
||||
// test value trimming disabled in config
|
||||
Assert.assertTrue("trim disabled but performed", validator.validate("t ", "name", configFromMap(ImmutableMap.of(LengthValidator.KEY_MIN, 2, LengthValidator.KEY_TRIM_DISABLED, true))).isValid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateLength_ConfigValidation() {
|
||||
|
||||
// invalid min and max config values
|
||||
ValidatorConfig config = new ValidatorConfig(ImmutableMap.of(LengthValidator.KEY_MIN, new Object(), LengthValidator.KEY_MAX, "invalid"));
|
||||
|
||||
ValidationResult result = Validators.validatorConfigValidator().validate(config, LengthValidator.ID).toResult();
|
||||
|
||||
Assert.assertFalse(result.isValid());
|
||||
ValidationError[] errors = result.getErrors().toArray(new ValidationError[0]);
|
||||
|
||||
ValidationError error0 = errors[0];
|
||||
Assert.assertNotNull(error0);
|
||||
Assert.assertEquals(LengthValidator.ID, error0.getValidatorId());
|
||||
Assert.assertEquals(LengthValidator.KEY_MIN, error0.getInputHint());
|
||||
|
||||
ValidationError error1 = errors[1];
|
||||
Assert.assertNotNull(error1);
|
||||
Assert.assertEquals(LengthValidator.ID, error1.getValidatorId());
|
||||
Assert.assertEquals(LengthValidator.KEY_MAX, error1.getInputHint());
|
||||
|
||||
// empty config
|
||||
result = Validators.validatorConfigValidator().validate(null, LengthValidator.ID).toResult();
|
||||
Assert.assertEquals(2, result.getErrors().size());
|
||||
result = Validators.validatorConfigValidator().validate(ValidatorConfig.EMPTY, LengthValidator.ID).toResult();
|
||||
Assert.assertEquals(2, result.getErrors().size());
|
||||
|
||||
// correct config
|
||||
Assert.assertTrue(Validators.validatorConfigValidator().validate(new ValidatorConfig(ImmutableMap.of(LengthValidator.KEY_MIN, "10")), LengthValidator.ID).toResult().isValid());
|
||||
Assert.assertTrue(Validators.validatorConfigValidator().validate(new ValidatorConfig(ImmutableMap.of(LengthValidator.KEY_MAX, "10")), LengthValidator.ID).toResult().isValid());
|
||||
Assert.assertTrue(Validators.validatorConfigValidator().validate(new ValidatorConfig(ImmutableMap.of(LengthValidator.KEY_MIN, "10", LengthValidator.KEY_MAX, "10")), LengthValidator.ID).toResult().isValid());
|
||||
|
||||
// max is smaller than min
|
||||
Assert.assertFalse(Validators.validatorConfigValidator().validate(new ValidatorConfig(ImmutableMap.of(LengthValidator.KEY_MIN, "10", LengthValidator.KEY_MAX, "9")), LengthValidator.ID).toResult().isValid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateEmail() {
|
||||
// this also validates StringFormatValidatorBase for simple values
|
||||
|
||||
Validator validator = Validators.emailValidator();
|
||||
|
||||
Assert.assertTrue(validator.validate(null, "email").isValid());
|
||||
Assert.assertFalse(validator.validate("", "email").isValid());
|
||||
Assert.assertTrue(validator.validate("admin@example.org", "email").isValid());
|
||||
Assert.assertTrue(validator.validate("admin+sds@example.org", "email").isValid());
|
||||
|
||||
Assert.assertFalse(validator.validate(" ", "email").isValid());
|
||||
Assert.assertFalse(validator.validate("adminATexample.org", "email").isValid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateStringFormatValidatorBaseForCollections() {
|
||||
|
||||
Validator validator = Validators.emailValidator();
|
||||
|
||||
List<String> valuesCollection = new ArrayList<>();
|
||||
|
||||
Assert.assertTrue(validator.validate(valuesCollection, "email").isValid());
|
||||
|
||||
valuesCollection.add("");
|
||||
Assert.assertFalse(validator.validate(valuesCollection, "email").isValid());
|
||||
valuesCollection.add("admin@example.org");
|
||||
Assert.assertTrue(validator.validate("admin@example.org", "email").isValid());
|
||||
|
||||
// wrong value fails validation even it is not at first position
|
||||
valuesCollection.add(" ");
|
||||
Assert.assertFalse(validator.validate(valuesCollection, "email").isValid());
|
||||
|
||||
valuesCollection.remove(valuesCollection.size() - 1);
|
||||
valuesCollection.add("adminATexample.org");
|
||||
Assert.assertFalse(validator.validate(valuesCollection, "email").isValid());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateNotBlank() {
|
||||
|
||||
Validator validator = Validators.notBlankValidator();
|
||||
|
||||
// simple String value
|
||||
Assert.assertTrue(validator.validate("tester", "username").isValid());
|
||||
Assert.assertFalse(validator.validate("", "username").isValid());
|
||||
Assert.assertFalse(validator.validate(" ", "username").isValid());
|
||||
Assert.assertFalse(validator.validate(null, "username").isValid());
|
||||
|
||||
// collection as input
|
||||
Assert.assertTrue(validator.validate(Arrays.asList("a", "b"), "username").isValid());
|
||||
Assert.assertFalse(validator.validate(new ArrayList<>(), "username").isValid());
|
||||
Assert.assertFalse(validator.validate(Arrays.asList(""), "username").isValid());
|
||||
Assert.assertFalse(validator.validate(Arrays.asList(" "), "username").isValid());
|
||||
Assert.assertFalse(validator.validate(Arrays.asList("a", " "), "username").isValid());
|
||||
Assert.assertFalse(validator.validate(Arrays.asList("a", new Object()), "username").isValid());
|
||||
|
||||
// unsupported input type
|
||||
Assert.assertFalse(validator.validate(new Object(), "username").isValid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateNotEmpty() {
|
||||
|
||||
Validator validator = Validators.notEmptyValidator();
|
||||
|
||||
Assert.assertTrue(validator.validate("tester", "username").isValid());
|
||||
Assert.assertTrue(validator.validate(" ", "username").isValid());
|
||||
Assert.assertTrue(validator.validate(Arrays.asList(1, 2, 3), "numberList").isValid());
|
||||
Assert.assertTrue(validator.validate(Collections.singleton("key"), "stringSet").isValid());
|
||||
Assert.assertTrue(validator.validate(Collections.singletonMap("key", "value"), "stringMap").isValid());
|
||||
|
||||
Assert.assertFalse(validator.validate(null, "username").isValid());
|
||||
Assert.assertFalse(validator.validate("", "username").isValid());
|
||||
Assert.assertFalse(validator.validate(Collections.emptyList(), "emptyList").isValid());
|
||||
Assert.assertFalse(validator.validate(Collections.emptySet(), "emptySet").isValid());
|
||||
Assert.assertFalse(validator.validate(Collections.emptyMap(), "emptyMap").isValid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateDoubleNumber() {
|
||||
|
||||
Validator validator = Validators.doubleValidator();
|
||||
|
||||
// null value and empty String
|
||||
Assert.assertTrue(validator.validate(null, "null").isValid());
|
||||
Assert.assertFalse(validator.validate("", "emptyString").isValid());
|
||||
|
||||
// simple values
|
||||
Assert.assertTrue(validator.validate(10, "age").isValid());
|
||||
Assert.assertTrue(validator.validate("10", "age").isValid());
|
||||
Assert.assertTrue(validator.validate("3.14", "pi").isValid());
|
||||
Assert.assertTrue(validator.validate(" 3.14 ", "piWithBlank").isValid());
|
||||
|
||||
Assert.assertFalse(validator.validate("a", "notAnumber").isValid());
|
||||
Assert.assertFalse(validator.validate(true, "true").isValid());
|
||||
|
||||
// collections
|
||||
Assert.assertTrue(validator.validate(new ArrayList<>(), "age").isValid());
|
||||
Assert.assertFalse(validator.validate(Arrays.asList(""), "age").isValid());
|
||||
Assert.assertTrue(validator.validate(Arrays.asList(10), "age").isValid());
|
||||
Assert.assertTrue(validator.validate(Arrays.asList(" 10 "), "age").isValid());
|
||||
Assert.assertTrue(validator.validate(Arrays.asList("3.14"), "pi").isValid());
|
||||
Assert.assertTrue(validator.validate(Arrays.asList("3.14", 10), "pi").isValid());
|
||||
|
||||
Assert.assertFalse(validator.validate(Arrays.asList("a"), "notAnumber").isValid());
|
||||
Assert.assertFalse(validator.validate(Arrays.asList("3.14", "a"), "notANumberPresent").isValid());
|
||||
Assert.assertFalse(validator.validate(Arrays.asList("3.14", new Object()), "notANumberPresent").isValid());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateDoubleNumber_ConfigValidation() {
|
||||
|
||||
// invalid min and max config values
|
||||
ValidatorConfig config = new ValidatorConfig(ImmutableMap.of(DoubleValidator.KEY_MIN, new Object(), DoubleValidator.KEY_MAX, "invalid"));
|
||||
|
||||
ValidationResult result = Validators.validatorConfigValidator().validate(config, DoubleValidator.ID).toResult();
|
||||
|
||||
Assert.assertFalse(result.isValid());
|
||||
ValidationError[] errors = result.getErrors().toArray(new ValidationError[0]);
|
||||
|
||||
ValidationError error0 = errors[0];
|
||||
Assert.assertNotNull(error0);
|
||||
Assert.assertEquals(DoubleValidator.ID, error0.getValidatorId());
|
||||
Assert.assertEquals(DoubleValidator.KEY_MIN, error0.getInputHint());
|
||||
|
||||
ValidationError error1 = errors[1];
|
||||
Assert.assertNotNull(error1);
|
||||
Assert.assertEquals(DoubleValidator.ID, error1.getValidatorId());
|
||||
Assert.assertEquals(DoubleValidator.KEY_MAX, error1.getInputHint());
|
||||
|
||||
// empty config
|
||||
result = Validators.validatorConfigValidator().validate(null, DoubleValidator.ID).toResult();
|
||||
Assert.assertEquals(0, result.getErrors().size());
|
||||
result = Validators.validatorConfigValidator().validate(ValidatorConfig.EMPTY, DoubleValidator.ID).toResult();
|
||||
Assert.assertEquals(0, result.getErrors().size());
|
||||
|
||||
// correct config
|
||||
Assert.assertTrue(Validators.validatorConfigValidator().validate(new ValidatorConfig(ImmutableMap.of(DoubleValidator.KEY_MIN, "10.1")), DoubleValidator.ID).toResult().isValid());
|
||||
Assert.assertTrue(Validators.validatorConfigValidator().validate(new ValidatorConfig(ImmutableMap.of(DoubleValidator.KEY_MAX, "10.1")), DoubleValidator.ID).toResult().isValid());
|
||||
Assert.assertTrue(Validators.validatorConfigValidator().validate(new ValidatorConfig(ImmutableMap.of(DoubleValidator.KEY_MIN, "10.1", DoubleValidator.KEY_MAX, "11")), DoubleValidator.ID).toResult().isValid());
|
||||
|
||||
// max is smaller than min
|
||||
Assert.assertFalse(Validators.validatorConfigValidator().validate(new ValidatorConfig(ImmutableMap.of(DoubleValidator.KEY_MIN, "10.1", DoubleValidator.KEY_MAX, "10.1")), DoubleValidator.ID).toResult().isValid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateIntegerNumber() {
|
||||
Validator validator = Validators.integerValidator();
|
||||
|
||||
// null value and empty String
|
||||
Assert.assertTrue(validator.validate(null, "null").isValid());
|
||||
Assert.assertFalse(validator.validate("", "emptyString").isValid());
|
||||
|
||||
// simple values
|
||||
Assert.assertTrue(validator.validate(10, "age").isValid());
|
||||
Assert.assertTrue(validator.validate("10", "age").isValid());
|
||||
|
||||
Assert.assertFalse(validator.validate("3.14", "pi").isValid());
|
||||
Assert.assertFalse(validator.validate(" 3.14 ", "piWithBlank").isValid());
|
||||
Assert.assertFalse(validator.validate("a", "notAnumber").isValid());
|
||||
Assert.assertFalse(validator.validate(true, "true").isValid());
|
||||
|
||||
// collections
|
||||
Assert.assertTrue(validator.validate(new ArrayList<>(), "age").isValid());
|
||||
Assert.assertFalse(validator.validate(Arrays.asList(""), "age").isValid());
|
||||
Assert.assertTrue(validator.validate(Arrays.asList(10), "age").isValid());
|
||||
Assert.assertFalse(validator.validate(Arrays.asList(" 10 "), "age").isValid());
|
||||
|
||||
Assert.assertFalse(validator.validate(Arrays.asList("3.14"), "pi").isValid());
|
||||
Assert.assertFalse(validator.validate(Arrays.asList("3.14", 10), "pi").isValid());
|
||||
Assert.assertFalse(validator.validate(Arrays.asList("a"), "notAnumber").isValid());
|
||||
Assert.assertFalse(validator.validate(Arrays.asList("10", "a"), "notANumberPresent").isValid());
|
||||
Assert.assertFalse(validator.validate(Arrays.asList("10", new Object()), "notANumberPresent").isValid());
|
||||
|
||||
// min only
|
||||
Assert.assertTrue(validator.validate("10", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 1).build()).isValid());
|
||||
Assert.assertFalse(validator.validate("10", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 100).build()).isValid());
|
||||
|
||||
// max only
|
||||
Assert.assertFalse(validator.validate("10", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MAX, 1).build()).isValid());
|
||||
Assert.assertTrue(validator.validate("10", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MAX, 100).build()).isValid());
|
||||
|
||||
// min and max
|
||||
Assert.assertFalse(validator.validate("9", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 10).config(DoubleValidator.KEY_MAX, 100).build()).isValid());
|
||||
Assert.assertTrue(validator.validate("10", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 10).config(DoubleValidator.KEY_MAX, 100).build()).isValid());
|
||||
Assert.assertTrue(validator.validate("100", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 10).config(DoubleValidator.KEY_MAX, 100).build()).isValid());
|
||||
Assert.assertFalse(validator.validate("101", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 10).config(DoubleValidator.KEY_MAX, 100).build()).isValid());
|
||||
|
||||
Assert.assertTrue(validator.validate(Long.MIN_VALUE, "name").isValid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateIntegerNumber_ConfigValidation() {
|
||||
|
||||
// invalid min and max config values
|
||||
ValidatorConfig config = new ValidatorConfig(ImmutableMap.of(IntegerValidator.KEY_MIN, new Object(), IntegerValidator.KEY_MAX, "invalid"));
|
||||
|
||||
ValidationResult result = Validators.validatorConfigValidator().validate(config, IntegerValidator.ID).toResult();
|
||||
|
||||
Assert.assertFalse(result.isValid());
|
||||
ValidationError[] errors = result.getErrors().toArray(new ValidationError[0]);
|
||||
|
||||
ValidationError error0 = errors[0];
|
||||
Assert.assertNotNull(error0);
|
||||
Assert.assertEquals(IntegerValidator.ID, error0.getValidatorId());
|
||||
Assert.assertEquals(IntegerValidator.KEY_MIN, error0.getInputHint());
|
||||
|
||||
ValidationError error1 = errors[1];
|
||||
Assert.assertNotNull(error1);
|
||||
Assert.assertEquals(IntegerValidator.ID, error1.getValidatorId());
|
||||
Assert.assertEquals(IntegerValidator.KEY_MAX, error1.getInputHint());
|
||||
|
||||
// empty config
|
||||
result = Validators.validatorConfigValidator().validate(null, IntegerValidator.ID).toResult();
|
||||
Assert.assertEquals(0, result.getErrors().size());
|
||||
result = Validators.validatorConfigValidator().validate(ValidatorConfig.EMPTY, IntegerValidator.ID).toResult();
|
||||
Assert.assertEquals(0, result.getErrors().size());
|
||||
|
||||
// correct config
|
||||
Assert.assertTrue(Validators.validatorConfigValidator().validate(new ValidatorConfig(ImmutableMap.of(IntegerValidator.KEY_MIN, "10")), IntegerValidator.ID).toResult().isValid());
|
||||
Assert.assertTrue(Validators.validatorConfigValidator().validate(new ValidatorConfig(ImmutableMap.of(IntegerValidator.KEY_MAX, "10")), IntegerValidator.ID).toResult().isValid());
|
||||
Assert.assertTrue(Validators.validatorConfigValidator().validate(new ValidatorConfig(ImmutableMap.of(IntegerValidator.KEY_MIN, "10", IntegerValidator.KEY_MAX, "11")), IntegerValidator.ID).toResult().isValid());
|
||||
|
||||
// max is smaller than min
|
||||
Assert.assertFalse(Validators.validatorConfigValidator().validate(new ValidatorConfig(ImmutableMap.of(IntegerValidator.KEY_MIN, "10", IntegerValidator.KEY_MAX, "10")), IntegerValidator.ID).toResult().isValid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validatePattern() {
|
||||
|
||||
Validator validator = Validators.patternValidator();
|
||||
|
||||
// Pattern object in the configuration
|
||||
ValidatorConfig config = configFromMap(Collections.singletonMap(PatternValidator.KEY_PATTERN, Pattern.compile("^start-.*-end$")));
|
||||
Assert.assertTrue(validator.validate("start-1234-end", "value", config).isValid());
|
||||
Assert.assertFalse(validator.validate("start___end", "value", config).isValid());
|
||||
|
||||
// String in the configuration
|
||||
config = configFromMap(Collections.singletonMap(PatternValidator.KEY_PATTERN, "^start-.*-end$"));
|
||||
Assert.assertTrue(validator.validate("start-1234-end", "value", config).isValid());
|
||||
Assert.assertFalse(validator.validate("start___end", "value", config).isValid());
|
||||
|
||||
// null and empty values handling
|
||||
// pattern not applied to null or empty string
|
||||
Assert.assertTrue(validator.validate(null, "value", config).isValid());
|
||||
Assert.assertFalse(validator.validate("", "value", config).isValid());
|
||||
// pattern is applied to blank string
|
||||
Assert.assertFalse(validator.validate(" ", "value", config).isValid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateUri() throws Exception {
|
||||
|
||||
Validator validator = Validators.uriValidator();
|
||||
|
||||
Assert.assertTrue(validator.validate(null, "baseUrl").isValid());
|
||||
Assert.assertTrue(validator.validate("", "baseUrl").isValid());
|
||||
Assert.assertTrue(validator.validate("http://localhost:3000/", "baseUrl").isValid());
|
||||
Assert.assertTrue(validator.validate("https://localhost:3000/", "baseUrl").isValid());
|
||||
Assert.assertTrue(validator.validate("https://localhost:3000/#someFragment", "baseUrl").isValid());
|
||||
|
||||
Assert.assertFalse(validator.validate(" ", "baseUrl").isValid());
|
||||
Assert.assertFalse(validator.validate("file:///somefile.txt", "baseUrl").isValid());
|
||||
Assert.assertFalse(validator.validate("invalidUrl++@23", "invalidUri").isValid());
|
||||
|
||||
ValidatorConfig config = configFromMap(ImmutableMap.of(UriValidator.KEY_ALLOW_FRAGMENT, false));
|
||||
Assert.assertFalse(validator.validate("https://localhost:3000/#someFragment", "baseUrl", config).isValid());
|
||||
|
||||
// it is also possible to call dedicated validation methods on a built-in validator
|
||||
Assert.assertTrue(Validators.uriValidator().validateUri(new URI("https://customurl"), Collections.singleton("https"), true, true));
|
||||
|
||||
Assert.assertFalse(Validators.uriValidator().validateUri(new URI("http://customurl"), Collections.singleton("https"), true, true));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,336 @@
|
|||
package org.keycloak.validate;
|
||||
|
||||
import static org.keycloak.validate.ValidatorConfig.configFromMap;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.validate.validators.LengthValidator;
|
||||
import org.keycloak.validate.validators.NotBlankValidator;
|
||||
import org.keycloak.validate.validators.ValidatorConfigValidator;
|
||||
|
||||
public class ValidatorTest {
|
||||
|
||||
KeycloakSession session = null;
|
||||
|
||||
@Test
|
||||
public void simpleValidation() {
|
||||
|
||||
Validator validator = Validators.notEmptyValidator();
|
||||
|
||||
Assert.assertTrue(validator.validate("a").isValid());
|
||||
Assert.assertFalse(validator.validate("").isValid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleValidationWithContext() {
|
||||
|
||||
Validator validator = Validators.lengthValidator();
|
||||
|
||||
ValidationContext context = new ValidationContext(session);
|
||||
validator.validate("a", "username", context);
|
||||
ValidationResult result = context.toResult();
|
||||
|
||||
Assert.assertTrue(result.isValid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleValidationFluent() {
|
||||
|
||||
ValidationContext context = new ValidationContext(session);
|
||||
|
||||
ValidationResult result = Validators.lengthValidator().validate("a", "username", context).toResult();
|
||||
|
||||
Assert.assertTrue(result.isValid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleValidationLookup() {
|
||||
|
||||
// later: session.validators().validator(LengthValidator.ID);
|
||||
Validator validator = Validators.validator(session, LengthValidator.ID);
|
||||
|
||||
ValidationContext context = new ValidationContext(session);
|
||||
validator.validate("a", "username", context);
|
||||
ValidationResult result = context.toResult();
|
||||
|
||||
Assert.assertTrue(result.isValid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void simpleValidationError() {
|
||||
|
||||
Validator validator = LengthValidator.INSTANCE;
|
||||
|
||||
String input = "a";
|
||||
String inputHint = "username";
|
||||
|
||||
ValidationContext context = new ValidationContext(session);
|
||||
validator.validate(input, inputHint, context, configFromMap(Collections.singletonMap("min", "2")));
|
||||
ValidationResult result = context.toResult();
|
||||
|
||||
Assert.assertFalse(result.isValid());
|
||||
Assert.assertEquals(1, result.getErrors().size());
|
||||
|
||||
ValidationError[] errors = result.getErrors().toArray(new ValidationError[0]);
|
||||
ValidationError error = errors[0];
|
||||
Assert.assertNotNull(error);
|
||||
Assert.assertEquals(LengthValidator.ID, error.getValidatorId());
|
||||
Assert.assertEquals(inputHint, error.getInputHint());
|
||||
Assert.assertEquals(LengthValidator.MESSAGE_INVALID_LENGTH, error.getMessage());
|
||||
Assert.assertEquals(input, error.getMessageParameters()[0]);
|
||||
|
||||
Assert.assertTrue(result.hasErrorsForValidatorId(LengthValidator.ID));
|
||||
Assert.assertFalse(result.hasErrorsForValidatorId("unknown"));
|
||||
|
||||
Assert.assertEquals(result.getErrors(), result.getErrorsForValidatorId(LengthValidator.ID));
|
||||
Assert.assertEquals(result.getErrors(), result.getErrorsForInputHint(inputHint));
|
||||
|
||||
Assert.assertTrue(result.hasErrorsForInputHint(inputHint));
|
||||
Assert.assertFalse(result.hasErrorsForInputHint("email"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptOnError() {
|
||||
|
||||
AtomicBoolean bool1 = new AtomicBoolean();
|
||||
Validators.notEmptyValidator().validate("a").toResult().ifNotValidAccept(r -> bool1.set(true));
|
||||
Assert.assertFalse(bool1.get());
|
||||
|
||||
AtomicBoolean bool2 = new AtomicBoolean();
|
||||
Validators.notEmptyValidator().validate("").toResult().ifNotValidAccept(r -> bool2.set(true));
|
||||
Assert.assertTrue(bool2.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forEachError() {
|
||||
|
||||
List<String> errors = new ArrayList<>();
|
||||
MockAddress faultyAddress = new MockAddress("", "Saint-Maur-des-Fossés", null, "Germany");
|
||||
MockAddressValidator.INSTANCE.validate(faultyAddress, "address").toResult().forEachError(e -> {
|
||||
errors.add(e.getMessage());
|
||||
});
|
||||
|
||||
Assert.assertEquals(Arrays.asList(NotBlankValidator.MESSAGE_BLANK, NotBlankValidator.MESSAGE_BLANK), errors);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void formatError() {
|
||||
|
||||
Map<String, String> miniResourceBundle = new HashMap<>();
|
||||
miniResourceBundle.put("error-invalid-blank", "{0} is blank: <{1}>");
|
||||
miniResourceBundle.put("error-invalid-value", "{0} is invalid: <{1}>");
|
||||
|
||||
List<String> errors = new ArrayList<>();
|
||||
MockAddress faultyAddress = new MockAddress("", "Saint-Maur-des-Fossés", null, "Germany");
|
||||
MockAddressValidator.INSTANCE.validate(faultyAddress, "address").toResult().forEachError(e -> {
|
||||
errors.add(e.formatMessage((message, args) -> MessageFormat.format(miniResourceBundle.getOrDefault(message, message), args)));
|
||||
});
|
||||
|
||||
Assert.assertEquals(Arrays.asList("address.street is blank: <>", "address.zip is blank: <null>"), errors);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleValidations() {
|
||||
|
||||
ValidationContext context = new ValidationContext(session);
|
||||
|
||||
String input = "aaa";
|
||||
String inputHint = "username";
|
||||
|
||||
Validators.lengthValidator().validate(input, inputHint, context);
|
||||
Validators.notEmptyValidator().validate(input, inputHint, context);
|
||||
|
||||
ValidationResult result = context.toResult();
|
||||
|
||||
Assert.assertTrue(result.isValid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleValidationsError() {
|
||||
|
||||
ValidationContext context = new ValidationContext(session);
|
||||
|
||||
String input = " ";
|
||||
String inputHint = "username";
|
||||
|
||||
Validators.lengthValidator().validate(input, inputHint, context, configFromMap(Collections.singletonMap(LengthValidator.KEY_MIN, 1)));
|
||||
Validators.notBlankValidator().validate(input, inputHint, context);
|
||||
|
||||
ValidationResult result = context.toResult();
|
||||
|
||||
Assert.assertFalse(result.isValid());
|
||||
Assert.assertEquals(2, result.getErrors().size());
|
||||
|
||||
ValidationError[] errors = result.getErrors().toArray(new ValidationError[0]);
|
||||
|
||||
ValidationError error1 = errors[1];
|
||||
|
||||
Assert.assertNotNull(error1);
|
||||
Assert.assertEquals(NotBlankValidator.ID, error1.getValidatorId());
|
||||
Assert.assertEquals(inputHint, error1.getInputHint());
|
||||
Assert.assertEquals(NotBlankValidator.MESSAGE_BLANK, error1.getMessage());
|
||||
Assert.assertEquals(input, error1.getMessageParameters()[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateValidatorConfigSimple() {
|
||||
|
||||
SimpleValidator validator = LengthValidator.INSTANCE;
|
||||
|
||||
Assert.assertFalse(validator.validateConfig(session, null).isValid());
|
||||
Assert.assertTrue(validator.validateConfig(session, configFromMap(Collections.singletonMap("min", 1))).isValid());
|
||||
Assert.assertTrue(validator.validateConfig(session, configFromMap(Collections.singletonMap("max", 100))).isValid());
|
||||
Assert.assertFalse(validator.validateConfig(session, configFromMap(Collections.singletonMap("min", null))).isValid());
|
||||
Assert.assertFalse(validator.validateConfig(session, configFromMap(Collections.singletonMap("min", "a"))).isValid());
|
||||
Assert.assertTrue(validator.validateConfig(session, configFromMap(Collections.singletonMap("min", "123"))).isValid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateValidatorConfigMultipleOptions() {
|
||||
|
||||
SimpleValidator validator = LengthValidator.INSTANCE;
|
||||
|
||||
Map<String, Object> config = new HashMap<>();
|
||||
config.put("min", 1);
|
||||
config.put("max", 10);
|
||||
|
||||
ValidatorConfig validatorConfig = configFromMap(config);
|
||||
|
||||
Assert.assertTrue(validator.validateConfig(session, validatorConfig).isValid());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateValidatorConfigMultipleOptionsInvalidValues() {
|
||||
|
||||
SimpleValidator validator = LengthValidator.INSTANCE;
|
||||
|
||||
Map<String, Object> config = new HashMap<>();
|
||||
config.put("min", "a");
|
||||
config.put("max", new ArrayList<>());
|
||||
|
||||
ValidationResult result = validator.validateConfig(session, configFromMap(config));
|
||||
|
||||
Assert.assertFalse(result.isValid());
|
||||
Assert.assertEquals(2, result.getErrors().size());
|
||||
|
||||
ValidationError[] errors = result.getErrors().toArray(new ValidationError[0]);
|
||||
ValidationError error1 = errors[1];
|
||||
|
||||
Assert.assertNotNull(error1);
|
||||
Assert.assertEquals(LengthValidator.ID, error1.getValidatorId());
|
||||
Assert.assertEquals("max", error1.getInputHint());
|
||||
Assert.assertEquals(ValidatorConfigValidator.MESSAGE_CONFIG_INVALID_NUMBER_VALUE, error1.getMessage());
|
||||
Assert.assertEquals(new ArrayList<>(), error1.getMessageParameters()[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateValidatorConfigViaValidatorFactory() {
|
||||
|
||||
Map<String, Object> config = new HashMap<>();
|
||||
config.put("min", "a");
|
||||
config.put("max", new ArrayList<>());
|
||||
|
||||
ValidatorConfig validatorConfig = configFromMap(config);
|
||||
|
||||
ValidationResult result = Validators.validateConfig(session, LengthValidator.ID, validatorConfig);
|
||||
Assert.assertEquals(2, result.getErrors().size());
|
||||
|
||||
ValidationError[] errors = result.getErrors().toArray(new ValidationError[0]);
|
||||
ValidationError error1 = errors[1];
|
||||
|
||||
Assert.assertNotNull(error1);
|
||||
Assert.assertEquals(LengthValidator.ID, error1.getValidatorId());
|
||||
Assert.assertEquals("max", error1.getInputHint());
|
||||
Assert.assertEquals(ValidatorConfigValidator.MESSAGE_CONFIG_INVALID_NUMBER_VALUE, error1.getMessage());
|
||||
Assert.assertEquals(new ArrayList<>(), error1.getMessageParameters()[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nestedValidation() {
|
||||
|
||||
Assert.assertTrue(MockAddressValidator.INSTANCE.validate(
|
||||
new MockAddress("4848 Arcu St.", "Saint-Maur-des-Fossés", "02206", "Germany")
|
||||
, "address").isValid());
|
||||
|
||||
ValidationResult result = MockAddressValidator.INSTANCE.validate(
|
||||
new MockAddress("", "Saint-Maur-des-Fossés", null, "Germany")
|
||||
, "address").toResult();
|
||||
Assert.assertFalse(result.isValid());
|
||||
Assert.assertEquals(2, result.getErrors().size());
|
||||
|
||||
ValidationError[] errors = result.getErrors().toArray(new ValidationError[0]);
|
||||
|
||||
ValidationError error0 = errors[0];
|
||||
|
||||
Assert.assertNotNull(error0);
|
||||
Assert.assertEquals(NotBlankValidator.ID, error0.getValidatorId());
|
||||
Assert.assertEquals("address.street", error0.getInputHint());
|
||||
Assert.assertEquals(NotBlankValidator.MESSAGE_BLANK, error0.getMessage());
|
||||
Assert.assertEquals("", error0.getMessageParameters()[0]);
|
||||
|
||||
ValidationError error1 = errors[1];
|
||||
|
||||
Assert.assertNotNull(error1);
|
||||
Assert.assertEquals(NotBlankValidator.ID, error1.getValidatorId());
|
||||
Assert.assertEquals("address.zip", error1.getInputHint());
|
||||
Assert.assertEquals(NotBlankValidator.MESSAGE_BLANK, error1.getMessage());
|
||||
|
||||
}
|
||||
|
||||
static class MockAddress {
|
||||
|
||||
private final String street;
|
||||
private final String city;
|
||||
private final String zip;
|
||||
private final String country;
|
||||
|
||||
public MockAddress(String street, String city, String zip, String country) {
|
||||
this.street = street;
|
||||
this.city = city;
|
||||
this.zip = zip;
|
||||
this.country = country;
|
||||
}
|
||||
}
|
||||
|
||||
static class MockAddressValidator implements SimpleValidator {
|
||||
|
||||
public static MockAddressValidator INSTANCE = new MockAddressValidator();
|
||||
|
||||
public static final String ID = "address";
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||
|
||||
if (!(input instanceof MockAddress)) {
|
||||
context.addError(new ValidationError(ID, inputHint, ValidationError.MESSAGE_INVALID_VALUE, input));
|
||||
return context;
|
||||
}
|
||||
|
||||
MockAddress address = (MockAddress) input;
|
||||
// Access validator statically
|
||||
NotBlankValidator.INSTANCE.validate(address.street, inputHint + ".street", context);
|
||||
NotBlankValidator.INSTANCE.validate(address.city, inputHint + ".city", context);
|
||||
NotBlankValidator.INSTANCE.validate(address.country, inputHint + ".country", context);
|
||||
|
||||
// Access validator via lookup (could be built-in or user-provided Validator)
|
||||
context.validator(NotBlankValidator.ID).validate(address.zip, inputHint + ".zip", context);
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -207,6 +207,19 @@ missingTotpMessage=Please specify authenticator code.
|
|||
missingTotpDeviceNameMessage=Please specify device name.
|
||||
notMatchPasswordMessage=Passwords don''t match.
|
||||
|
||||
error-invalid-value=Invalid value.
|
||||
error-invalid-blank=Please specify value.
|
||||
error-empty=Please specify value.
|
||||
error-invalid-length=Length must be between {1} and {2}.
|
||||
error-invalid-email=Invalid email address.
|
||||
error-invalid-number=Invalid number.
|
||||
error-number-out-of-range=Number must be between {1} and {2}.
|
||||
error-pattern-no-match=Invalid value.
|
||||
error-invalid-uri=Invalid URL.
|
||||
error-invalid-uri-scheme=Invalid URL scheme.
|
||||
error-invalid-uri-fragment=Invalid URL fragment.
|
||||
|
||||
|
||||
invalidPasswordExistingMessage=Invalid existing password.
|
||||
invalidPasswordBlacklistedMessage=Invalid password: password is blacklisted.
|
||||
invalidPasswordConfirmMessage=Password confirmation doesn''t match.
|
||||
|
|
Loading…
Reference in a new issue