[KEYCLOAK-17399] UserProfile SPI - Validation SPI integration
This commit is contained in:
parent
7c2341f1ed
commit
4ad1687f2b
34 changed files with 1521 additions and 713 deletions
|
@ -19,28 +19,53 @@
|
||||||
|
|
||||||
package org.keycloak.userprofile;
|
package org.keycloak.userprofile;
|
||||||
|
|
||||||
import org.keycloak.userprofile.AttributeContext;
|
import org.keycloak.validate.ValidationContext;
|
||||||
import org.keycloak.userprofile.validation.Validator;
|
import org.keycloak.validate.Validator;
|
||||||
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
import org.keycloak.validate.Validators;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
*/
|
*/
|
||||||
public final class AttributeValidatorMetadata implements Validator {
|
public final class AttributeValidatorMetadata {
|
||||||
|
|
||||||
private final String message;
|
private final String validatorId;
|
||||||
private final Validator validator;
|
private final ValidatorConfig validatorConfig;
|
||||||
|
|
||||||
public AttributeValidatorMetadata(String message, Validator validator) {
|
public AttributeValidatorMetadata(String validatorId) {
|
||||||
this.message = message;
|
this.validatorId = validatorId;
|
||||||
this.validator = validator;
|
this.validatorConfig = ValidatorConfig.configFromMap(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getMessage() {
|
public AttributeValidatorMetadata(String validatorId, ValidatorConfig validatorConfig) {
|
||||||
return message;
|
this.validatorId = validatorId;
|
||||||
|
this.validatorConfig = validatorConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
public boolean validate(AttributeContext context) {
|
* Getters so we can collect validation configurations and provide them to GUI for dynamic client side validations.
|
||||||
return validator.validate(context);
|
*
|
||||||
|
* @return the validatorId
|
||||||
|
*/
|
||||||
|
public String getValidatorId() {
|
||||||
|
return validatorId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run validation for given AttributeContext.
|
||||||
|
*
|
||||||
|
* @param context to validate
|
||||||
|
* @return context containing errors if any found
|
||||||
|
*/
|
||||||
|
public ValidationContext validate(AttributeContext context) {
|
||||||
|
|
||||||
|
Validator validator = Validators.validator(context.getSession(), validatorId);
|
||||||
|
if (validator == null) {
|
||||||
|
throw new RuntimeException("No validator with id " + validatorId + " found to validate UserProfile attribute " + context.getMetadata().getName() + " in realm " + context.getSession().getContext().getRealm().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return validator.validate(context.getAttribute().getValue(), context.getMetadata().getName(), new UserProfileAttributeValidationContext(context), validatorConfig);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,9 +23,10 @@ import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.BiConsumer;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.keycloak.validate.ValidationError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>This interface wraps the attributes associated with a user profile. Different operations are provided to access and
|
* <p>This interface wraps the attributes associated with a user profile. Different operations are provided to access and
|
||||||
* manage these attributes.
|
* manage these attributes.
|
||||||
|
@ -75,31 +76,15 @@ public interface Attributes {
|
||||||
boolean isReadOnly(String key);
|
boolean isReadOnly(String key);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <Validates the attribute with the given {@code name}.
|
* Validates the attribute with the given {@code name}.
|
||||||
*
|
*
|
||||||
* @param name the name of the attribute
|
* @param name the name of the attribute
|
||||||
* @param listeners the listeners for listening for errors
|
* @param listeners the listeners for listening for errors. <code>ValidationError.inputHint</code> contains name of the attribute in error.
|
||||||
*
|
*
|
||||||
* @return {@code true} if validation is successful. Otherwise, {@code false}. In case there is no attribute with the given {@code name},
|
* @return {@code true} if validation is successful. Otherwise, {@code false}. In case there is no attribute with the given {@code name},
|
||||||
* {@code false} is also returned but without triggering listeners
|
* {@code false} is also returned but without triggering listeners
|
||||||
*/
|
*/
|
||||||
boolean validate(String name, BiConsumer<Map.Entry<String, List<String>>, String>... listeners);
|
boolean validate(String name, Consumer<ValidationError>... listeners);
|
||||||
|
|
||||||
/**
|
|
||||||
* A simpler variant of {@link #validate(String, BiConsumer[])} for those only interested on error messages.
|
|
||||||
*
|
|
||||||
* @param name the name of the attribute
|
|
||||||
* @param listeners the listeners for listening for errors
|
|
||||||
* @return {@code true} if validation is successful. Otherwise, {@code false}. In case there is no attribute with the given {@code name},
|
|
||||||
* {@code false} is also returned but without triggering listeners
|
|
||||||
*/
|
|
||||||
default boolean validate(String name, Consumer<String>... listeners) {
|
|
||||||
return validate(name, (attribute, error) -> {
|
|
||||||
for (Consumer<String> consumer : listeners) {
|
|
||||||
consumer.accept(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether an attribute with the given {@code name} is defined.
|
* Checks whether an attribute with the given {@code name} is defined.
|
||||||
|
|
|
@ -26,13 +26,15 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.validate.ValidationContext;
|
||||||
|
import org.keycloak.validate.ValidationError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>The default implementation for {@link Attributes}. Should be reused as much as possible by the different implementations
|
* <p>The default implementation for {@link Attributes}. Should be reused as much as possible by the different implementations
|
||||||
|
@ -84,7 +86,7 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean validate(String name, BiConsumer<Entry<String, List<String>>, String>... listeners) {
|
public boolean validate(String name, Consumer<ValidationError>... listeners) {
|
||||||
Entry<String, List<String>> attribute = createAttribute(name);
|
Entry<String, List<String>> attribute = createAttribute(name);
|
||||||
List<AttributeMetadata> metadatas = new ArrayList<>();
|
List<AttributeMetadata> metadatas = new ArrayList<>();
|
||||||
|
|
||||||
|
@ -93,23 +95,26 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
||||||
metadatas.addAll(Optional.ofNullable(this.metadataByAttribute.get(READ_ONLY_ATTRIBUTE_KEY))
|
metadatas.addAll(Optional.ofNullable(this.metadataByAttribute.get(READ_ONLY_ATTRIBUTE_KEY))
|
||||||
.map(Collections::singletonList).orElse(Collections.emptyList()));
|
.map(Collections::singletonList).orElse(Collections.emptyList()));
|
||||||
|
|
||||||
List<AttributeValidatorMetadata> failingValidators = Collections.emptyList();
|
List<ValidationContext> failingValidators = Collections.emptyList();
|
||||||
|
|
||||||
for (AttributeMetadata metadata : metadatas) {
|
for (AttributeMetadata metadata : metadatas) {
|
||||||
for (AttributeValidatorMetadata validator : metadata.getValidators()) {
|
for (AttributeValidatorMetadata validator : metadata.getValidators()) {
|
||||||
if (!validator.validate(createAttributeContext(attribute, metadata))) {
|
ValidationContext vc = validator.validate(createAttributeContext(attribute, metadata));
|
||||||
|
if (!vc.isValid()) {
|
||||||
if (failingValidators.equals(Collections.emptyList())) {
|
if (failingValidators.equals(Collections.emptyList())) {
|
||||||
failingValidators = new ArrayList<>();
|
failingValidators = new ArrayList<>();
|
||||||
}
|
}
|
||||||
failingValidators.add(validator);
|
failingValidators.add(vc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (listeners != null) {
|
if (listeners != null) {
|
||||||
for (AttributeValidatorMetadata failingValidator : failingValidators) {
|
for (ValidationContext failingValidator : failingValidators) {
|
||||||
for (BiConsumer<Entry<String, List<String>>, String> consumer : listeners) {
|
for (Consumer<ValidationError> consumer : listeners) {
|
||||||
consumer.accept(attribute, failingValidator.getMessage());
|
for(ValidationError err: failingValidator.getErrors()) {
|
||||||
|
consumer.accept(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -296,7 +301,8 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
||||||
SimpleImmutableEntry<String, List<String>> attribute = createAttribute(attributeName);
|
SimpleImmutableEntry<String, List<String>> attribute = createAttribute(attributeName);
|
||||||
|
|
||||||
for (AttributeValidatorMetadata validator : readonlyMetadata.getValidators()) {
|
for (AttributeValidatorMetadata validator : readonlyMetadata.getValidators()) {
|
||||||
if (!validator.validate(createAttributeContext(attribute, readonlyMetadata))) {
|
ValidationContext vc = validator.validate(createAttributeContext(attribute, readonlyMetadata));
|
||||||
|
if (!vc.isValid()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,7 @@ public final class DefaultUserProfile implements UserProfile {
|
||||||
|
|
||||||
for (String attributeName : attributes.nameSet()) {
|
for (String attributeName : attributes.nameSet()) {
|
||||||
this.attributes.validate(attributeName,
|
this.attributes.validate(attributeName,
|
||||||
(attribute, message) -> validationException.addError(new ValidationException.Error(attribute, message)));
|
(error) -> validationException.addError(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validationException.hasError()) {
|
if (validationException.hasError()) {
|
||||||
|
|
|
@ -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.userprofile;
|
||||||
|
|
||||||
|
import org.keycloak.validate.ValidationContext;
|
||||||
|
import org.keycloak.validate.Validator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension of the {@link ValidationContext} used when validators are called for {@link UserProfile} attribute validation. Allows
|
||||||
|
* easy access to UserProfile related bits, like {@link AttributeContext}
|
||||||
|
*
|
||||||
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class UserProfileAttributeValidationContext extends ValidationContext {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Easy way to cast me from {@link ValidationContext} in {@link Validator} implementation
|
||||||
|
*/
|
||||||
|
public static UserProfileAttributeValidationContext from(ValidationContext vc) {
|
||||||
|
return (UserProfileAttributeValidationContext) vc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AttributeContext attributeContext;
|
||||||
|
|
||||||
|
public UserProfileAttributeValidationContext(AttributeContext attributeContext) {
|
||||||
|
super(attributeContext.getSession());
|
||||||
|
this.attributeContext = attributeContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AttributeContext getAttributeContext() {
|
||||||
|
return attributeContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
package org.keycloak.userprofile;
|
package org.keycloak.userprofile;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
@ -26,6 +27,8 @@ import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.keycloak.validate.ValidationError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
*/
|
*/
|
||||||
|
@ -34,8 +37,7 @@ public final class ValidationException extends RuntimeException {
|
||||||
private final Map<String, List<Error>> errors = new HashMap<>();
|
private final Map<String, List<Error>> errors = new HashMap<>();
|
||||||
|
|
||||||
public List<Error> getErrors() {
|
public List<Error> getErrors() {
|
||||||
return errors.values().stream().reduce(new ArrayList<>(),
|
return errors.values().stream().reduce(new ArrayList<>(), (l, r) -> {
|
||||||
(l, r) -> {
|
|
||||||
l.addAll(r);
|
l.addAll(r);
|
||||||
return l;
|
return l;
|
||||||
}, (l, r) -> l);
|
}, (l, r) -> l);
|
||||||
|
@ -67,32 +69,48 @@ public final class ValidationException extends RuntimeException {
|
||||||
|
|
||||||
List<String> names = Arrays.asList(name);
|
List<String> names = Arrays.asList(name);
|
||||||
|
|
||||||
return errors.values().stream().flatMap(Collection::stream)
|
return errors.values().stream().flatMap(Collection::stream).anyMatch(error -> names.contains(error.getAttribute()));
|
||||||
.anyMatch(error -> names.contains(error.attribute.getKey()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void addError(Error error) {
|
void addError(ValidationError error) {
|
||||||
List<Error> errors = this.errors.computeIfAbsent(error.getMessage(), (k) -> new ArrayList<>());
|
List<Error> errors = this.errors.computeIfAbsent(error.getMessage(), (k) -> new ArrayList<>());
|
||||||
errors.add(error);
|
errors.add(new Error(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Error {
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ValidationException [errors=" + errors + "]";
|
||||||
|
}
|
||||||
|
|
||||||
private final Map.Entry<String, List<String>> attribute;
|
@Override
|
||||||
private final String message;
|
public String getMessage() {
|
||||||
|
return toString();
|
||||||
|
}
|
||||||
|
|
||||||
public Error(Map.Entry<String, List<String>> attribute, String message) {
|
public static class Error implements Serializable {
|
||||||
this.attribute = attribute;
|
|
||||||
this.message = message;
|
private final ValidationError error;
|
||||||
|
|
||||||
|
public Error(ValidationError error) {
|
||||||
|
this.error = error;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAttribute() {
|
public String getAttribute() {
|
||||||
return attribute.getKey();
|
return error.getInputHint();
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: support parameters to messsages for formatting purposes. Message key and parameters.
|
|
||||||
public String getMessage() {
|
public String getMessage() {
|
||||||
return message;
|
return error.getMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Object[] getMessageParameters() {
|
||||||
|
return error.getMessageParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Error [error=" + error + "]";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
/*
|
|
||||||
*
|
|
||||||
* * 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.userprofile.validation;
|
|
||||||
|
|
||||||
import org.keycloak.userprofile.AttributeContext;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
|
||||||
*/
|
|
||||||
public interface Validator {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns true if validation success, false if validation fails
|
|
||||||
*/
|
|
||||||
boolean validate(AttributeContext context);
|
|
||||||
|
|
||||||
}
|
|
|
@ -18,15 +18,13 @@ package org.keycloak.validate;
|
||||||
|
|
||||||
import java.util.Collection;
|
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:
|
* Base class for arbitrary value type validators. Functionality covered in this base class:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>accepts supported type, collection of supported type.
|
* <li>accepts supported type, collection of supported type.
|
||||||
* <li>null values are always treated as valid to support optional fields! Use other validators (like
|
* <li>behavior around null and empty values is controlled by {@link #IGNORE_EMPTY_VALUE} configuration option which is
|
||||||
* {@link NotBlankValidator} or {@link NotEmptyValidator} to force field as required.
|
* boolean. Error should be produced for them by default, but they should be ignored if that option is
|
||||||
|
* <code>true</code>. Logic must be implemented in {@link #skipValidation(Object, ValidatorConfig)}.
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @author Vlastimil Elias <velias@redhat.com>
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
|
@ -34,25 +32,32 @@ import org.keycloak.validate.validators.NotEmptyValidator;
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractSimpleValidator implements SimpleValidator {
|
public abstract class AbstractSimpleValidator implements SimpleValidator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config option which allows to switch validator to ignore null, empty string and even blank string value - not to
|
||||||
|
* produce error for them. Used eg. in UserProfile where we have optional attributes and required concern is checked
|
||||||
|
* by separate validators.
|
||||||
|
*/
|
||||||
|
public static final String IGNORE_EMPTY_VALUE = "ignore.empty.value";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||||
|
|
||||||
if (input != null) {
|
|
||||||
if (input instanceof Collection) {
|
if (input instanceof Collection) {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Collection<Object> values = (Collection<Object>) input;
|
Collection<Object> values = (Collection<Object>) input;
|
||||||
|
|
||||||
if (values.isEmpty()) {
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Object value : values) {
|
for (Object value : values) {
|
||||||
validate(value, inputHint, context, config);
|
validate(value, inputHint, context, config);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skipValidation(input, config)) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
doValidate(input, inputHint, context, config);
|
doValidate(input, inputHint, context, config);
|
||||||
}
|
|
||||||
}
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,6 +69,31 @@ public abstract class AbstractSimpleValidator implements SimpleValidator {
|
||||||
* @param inputHint
|
* @param inputHint
|
||||||
* @param context for the validation. Add errors into it.
|
* @param context for the validation. Add errors into it.
|
||||||
* @param config of the validation if provided
|
* @param config of the validation if provided
|
||||||
|
*
|
||||||
|
* @see #skipValidation(Object, ValidatorConfig)
|
||||||
*/
|
*/
|
||||||
protected abstract void doValidate(Object value, String inputHint, ValidationContext context, ValidatorConfig config);
|
protected abstract void doValidate(Object value, String inputHint, ValidationContext context, ValidatorConfig config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decide if validation of individual value should be skipped or not. It should be controlled by
|
||||||
|
* {@link #IGNORE_EMPTY_VALUE} configuration option, see {@link #isIgnoreEmptyValuesConfigured(ValidatorConfig)}.
|
||||||
|
*
|
||||||
|
* @param value currently validated we make decision for
|
||||||
|
* @param config to look for options in
|
||||||
|
* @return true if validation should be skipped for this value -
|
||||||
|
* {@link #doValidate(Object, String, ValidationContext, ValidatorConfig)} is not called in this case.
|
||||||
|
*
|
||||||
|
* @see #doValidate(Object, String, ValidationContext, ValidatorConfig)
|
||||||
|
*/
|
||||||
|
protected abstract boolean skipValidation(Object value, ValidatorConfig config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation only looks for {@link #IGNORE_EMPTY_VALUE} configuration option.
|
||||||
|
*
|
||||||
|
* @param config to get option from
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
protected boolean isIgnoreEmptyValuesConfigured(ValidatorConfig config) {
|
||||||
|
return config != null && config.getBooleanOrDefault(IGNORE_EMPTY_VALUE, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,16 +16,14 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.validate;
|
package org.keycloak.validate;
|
||||||
|
|
||||||
import org.keycloak.validate.validators.NotBlankValidator;
|
import org.keycloak.utils.StringUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for String value format validators. Functionality covered in this base class:
|
* Base class for String value format validators. Functionality covered in this base class:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>accepts plain string and collections of strings as input
|
* <li>accepts plain string and collections of strings as input
|
||||||
* <li>each item is validated for collections of strings, see
|
* <li>each item is validated for collections of strings by {@link #doValidate(String, String, ValidationContext, ValidatorConfig)}
|
||||||
* {@link #validateFormat(String, String, ValidationContext, ValidatorConfig)}
|
* <li>null and empty values behavior should follow config, see {@link AbstractSimpleValidator} javadoc.
|
||||||
* <li>null values are always treated as valid to support optional fields! Use other validators (like
|
|
||||||
* {@link NotBlankValidator} to force field as required.
|
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @author Vlastimil Elias <velias@redhat.com>
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
|
@ -43,4 +41,12 @@ public abstract class AbstractStringValidator extends AbstractSimpleValidator {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract void doValidate(String value, String inputHint, ValidationContext context, ValidatorConfig config);
|
protected abstract void doValidate(String value, String inputHint, ValidationContext context, ValidatorConfig config);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean skipValidation(Object value, ValidatorConfig config) {
|
||||||
|
if (isIgnoreEmptyValuesConfigured(config) && (value == null || value instanceof String)) {
|
||||||
|
return value == null || StringUtil.isBlank(value.toString());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -226,6 +226,26 @@ public class ValidatorConfig {
|
||||||
config.put(name, value);
|
config.put(name, value);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add all configurations from map
|
||||||
|
*/
|
||||||
|
public ValidatorConfigBuilder config(Map<String, Object> values) {
|
||||||
|
if(values!=null) {
|
||||||
|
config.putAll(values);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add all configurations from other config
|
||||||
|
*/
|
||||||
|
public ValidatorConfigBuilder config(ValidatorConfig values) {
|
||||||
|
if(values != null && values.config != null) {
|
||||||
|
config.putAll(values.config);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -20,6 +20,7 @@ import java.util.LinkedHashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.utils.StringUtil;
|
||||||
import org.keycloak.validate.AbstractSimpleValidator;
|
import org.keycloak.validate.AbstractSimpleValidator;
|
||||||
import org.keycloak.validate.ValidationContext;
|
import org.keycloak.validate.ValidationContext;
|
||||||
import org.keycloak.validate.ValidationError;
|
import org.keycloak.validate.ValidationError;
|
||||||
|
@ -51,17 +52,31 @@ public abstract class AbstractNumberValidator extends AbstractSimpleValidator {
|
||||||
this.defaultConfig = config;
|
this.defaultConfig = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean skipValidation(Object value, ValidatorConfig config) {
|
||||||
|
if (isIgnoreEmptyValuesConfigured(config) && (value == null || value instanceof String)) {
|
||||||
|
return value == null || StringUtil.isBlank(value.toString());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doValidate(Object value, String inputHint, ValidationContext context, ValidatorConfig config) {
|
protected void doValidate(Object value, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||||
if (config == null || config.isEmpty()) {
|
if (config == null || config.isEmpty()) {
|
||||||
config = defaultConfig;
|
config = defaultConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
Number number;
|
Number number = null;
|
||||||
|
|
||||||
|
if (value != null) {
|
||||||
try {
|
try {
|
||||||
number = convert(value, config);
|
number = convert(value, config);
|
||||||
} catch (NumberFormatException ignore) {
|
} catch (NumberFormatException ignore) {
|
||||||
|
// N/A
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (number == null) {
|
||||||
context.addError(new ValidationError(getId(), inputHint, MESSAGE_INVALID_NUMBER, value));
|
context.addError(new ValidationError(getId(), inputHint, MESSAGE_INVALID_NUMBER, value));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,17 +21,24 @@ import com.google.common.collect.ImmutableMap;
|
||||||
|
|
||||||
public class BuiltinValidatorsTest {
|
public class BuiltinValidatorsTest {
|
||||||
|
|
||||||
|
private static final ValidatorConfig valConfigIgnoreEmptyValues = ValidatorConfig.builder().config(AbstractSimpleValidator.IGNORE_EMPTY_VALUE, true).build();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void validateLength() {
|
public void validateLength() {
|
||||||
|
|
||||||
Validator validator = Validators.lengthValidator();
|
Validator validator = Validators.lengthValidator();
|
||||||
|
|
||||||
// null and empty values handling
|
// null and empty values handling
|
||||||
Assert.assertTrue(validator.validate(null, "name", configFromMap(ImmutableMap.of(LengthValidator.KEY_MIN, 1))).isValid());
|
Assert.assertFalse(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.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());
|
Assert.assertTrue(validator.validate(" ", "name", configFromMap(ImmutableMap.of(LengthValidator.KEY_MAX, 10))).isValid());
|
||||||
|
|
||||||
|
// empty value ignoration configured
|
||||||
|
Assert.assertTrue(validator.validate(null, "name", valConfigIgnoreEmptyValues).isValid());
|
||||||
|
Assert.assertTrue(validator.validate("", "name", valConfigIgnoreEmptyValues).isValid());
|
||||||
|
Assert.assertTrue(validator.validate(" ", "name", valConfigIgnoreEmptyValues).isValid());
|
||||||
|
|
||||||
// min validation only
|
// min validation only
|
||||||
Assert.assertTrue(validator.validate("tester", "name", configFromMap(ImmutableMap.of(LengthValidator.KEY_MIN, 1))).isValid());
|
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());
|
Assert.assertFalse(validator.validate("tester", "name", configFromMap(ImmutableMap.of(LengthValidator.KEY_MIN, 7))).isValid());
|
||||||
|
@ -97,8 +104,15 @@ public class BuiltinValidatorsTest {
|
||||||
|
|
||||||
Validator validator = Validators.emailValidator();
|
Validator validator = Validators.emailValidator();
|
||||||
|
|
||||||
Assert.assertTrue(validator.validate(null, "email").isValid());
|
Assert.assertFalse(validator.validate(null, "email").isValid());
|
||||||
Assert.assertFalse(validator.validate("", "email").isValid());
|
Assert.assertFalse(validator.validate("", "email").isValid());
|
||||||
|
|
||||||
|
// empty value ignoration configured
|
||||||
|
Assert.assertTrue(validator.validate(null, "emptyString", valConfigIgnoreEmptyValues).isValid());
|
||||||
|
Assert.assertTrue(validator.validate("", "emptyString", valConfigIgnoreEmptyValues).isValid());
|
||||||
|
Assert.assertTrue(validator.validate(" ", "blankString", valConfigIgnoreEmptyValues).isValid());
|
||||||
|
|
||||||
|
|
||||||
Assert.assertTrue(validator.validate("admin@example.org", "email").isValid());
|
Assert.assertTrue(validator.validate("admin@example.org", "email").isValid());
|
||||||
Assert.assertTrue(validator.validate("admin+sds@example.org", "email").isValid());
|
Assert.assertTrue(validator.validate("admin+sds@example.org", "email").isValid());
|
||||||
|
|
||||||
|
@ -177,8 +191,14 @@ public class BuiltinValidatorsTest {
|
||||||
Validator validator = Validators.doubleValidator();
|
Validator validator = Validators.doubleValidator();
|
||||||
|
|
||||||
// null value and empty String
|
// null value and empty String
|
||||||
Assert.assertTrue(validator.validate(null, "null").isValid());
|
Assert.assertFalse(validator.validate(null, "null").isValid());
|
||||||
Assert.assertFalse(validator.validate("", "emptyString").isValid());
|
Assert.assertFalse(validator.validate("", "emptyString").isValid());
|
||||||
|
Assert.assertFalse(validator.validate(" ", "blankString").isValid());
|
||||||
|
|
||||||
|
// empty value ignoration configured
|
||||||
|
Assert.assertTrue(validator.validate(null, "emptyString", valConfigIgnoreEmptyValues).isValid());
|
||||||
|
Assert.assertTrue(validator.validate("", "emptyString", valConfigIgnoreEmptyValues).isValid());
|
||||||
|
Assert.assertTrue(validator.validate(" ", "blankString", valConfigIgnoreEmptyValues).isValid());
|
||||||
|
|
||||||
// simple values
|
// simple values
|
||||||
Assert.assertTrue(validator.validate(10, "age").isValid());
|
Assert.assertTrue(validator.validate(10, "age").isValid());
|
||||||
|
@ -190,8 +210,9 @@ public class BuiltinValidatorsTest {
|
||||||
Assert.assertFalse(validator.validate(true, "true").isValid());
|
Assert.assertFalse(validator.validate(true, "true").isValid());
|
||||||
|
|
||||||
// collections
|
// collections
|
||||||
Assert.assertTrue(validator.validate(new ArrayList<>(), "age").isValid());
|
|
||||||
Assert.assertFalse(validator.validate(Arrays.asList(""), "age").isValid());
|
Assert.assertFalse(validator.validate(Arrays.asList(""), "age").isValid());
|
||||||
|
Assert.assertTrue(validator.validate(Arrays.asList(""), "age",valConfigIgnoreEmptyValues).isValid());
|
||||||
|
Assert.assertTrue(validator.validate(new ArrayList<>(), "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(" 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"), "pi").isValid());
|
||||||
|
@ -201,6 +222,28 @@ public class BuiltinValidatorsTest {
|
||||||
Assert.assertFalse(validator.validate(Arrays.asList("3.14", "a"), "notANumberPresent").isValid());
|
Assert.assertFalse(validator.validate(Arrays.asList("3.14", "a"), "notANumberPresent").isValid());
|
||||||
Assert.assertFalse(validator.validate(Arrays.asList("3.14", new Object()), "notANumberPresent").isValid());
|
Assert.assertFalse(validator.validate(Arrays.asList("3.14", new Object()), "notANumberPresent").isValid());
|
||||||
|
|
||||||
|
// min only
|
||||||
|
Assert.assertTrue(validator.validate("10.1", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 1.4).build()).isValid());
|
||||||
|
Assert.assertFalse(validator.validate("10.1", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 100.5).build()).isValid());
|
||||||
|
// min behavior around empty values
|
||||||
|
Assert.assertFalse(validator.validate(null, "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 1.1).build()).isValid());
|
||||||
|
Assert.assertFalse(validator.validate("", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 1.1).build()).isValid());
|
||||||
|
Assert.assertFalse(validator.validate(" ", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 1.1).build()).isValid());
|
||||||
|
Assert.assertTrue(validator.validate(null, "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 1.1).config(valConfigIgnoreEmptyValues).build()).isValid());
|
||||||
|
Assert.assertTrue(validator.validate("", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 1.1).config(valConfigIgnoreEmptyValues).build()).isValid());
|
||||||
|
Assert.assertTrue(validator.validate(" ", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 1.1).config(valConfigIgnoreEmptyValues).build()).isValid());
|
||||||
|
|
||||||
|
// max only
|
||||||
|
Assert.assertFalse(validator.validate("10.5", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MAX, 1.1).build()).isValid());
|
||||||
|
Assert.assertTrue(validator.validate("10.5", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MAX, 100.1).build()).isValid());
|
||||||
|
|
||||||
|
// min and max
|
||||||
|
Assert.assertFalse(validator.validate("10.09", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 10.1).config(DoubleValidator.KEY_MAX, 100).build()).isValid());
|
||||||
|
Assert.assertTrue(validator.validate("10.1", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 10.1).config(DoubleValidator.KEY_MAX, 100).build()).isValid());
|
||||||
|
Assert.assertTrue(validator.validate("100.1", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 10.1).config(DoubleValidator.KEY_MAX, 100.1).build()).isValid());
|
||||||
|
Assert.assertFalse(validator.validate("100.2", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 10.1).config(DoubleValidator.KEY_MAX, 100.1).build()).isValid());
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -244,9 +287,14 @@ public class BuiltinValidatorsTest {
|
||||||
Validator validator = Validators.integerValidator();
|
Validator validator = Validators.integerValidator();
|
||||||
|
|
||||||
// null value and empty String
|
// null value and empty String
|
||||||
Assert.assertTrue(validator.validate(null, "null").isValid());
|
Assert.assertFalse(validator.validate(null, "null").isValid());
|
||||||
Assert.assertFalse(validator.validate("", "emptyString").isValid());
|
Assert.assertFalse(validator.validate("", "emptyString").isValid());
|
||||||
|
|
||||||
|
// empty value ignoration configured
|
||||||
|
Assert.assertTrue(validator.validate(null, "emptyString", valConfigIgnoreEmptyValues).isValid());
|
||||||
|
Assert.assertTrue(validator.validate("", "emptyString", valConfigIgnoreEmptyValues).isValid());
|
||||||
|
Assert.assertTrue(validator.validate(" ", "blankString", valConfigIgnoreEmptyValues).isValid());
|
||||||
|
|
||||||
// simple values
|
// simple values
|
||||||
Assert.assertTrue(validator.validate(10, "age").isValid());
|
Assert.assertTrue(validator.validate(10, "age").isValid());
|
||||||
Assert.assertTrue(validator.validate("10", "age").isValid());
|
Assert.assertTrue(validator.validate("10", "age").isValid());
|
||||||
|
@ -259,6 +307,7 @@ public class BuiltinValidatorsTest {
|
||||||
// collections
|
// collections
|
||||||
Assert.assertTrue(validator.validate(new ArrayList<>(), "age").isValid());
|
Assert.assertTrue(validator.validate(new ArrayList<>(), "age").isValid());
|
||||||
Assert.assertFalse(validator.validate(Arrays.asList(""), "age").isValid());
|
Assert.assertFalse(validator.validate(Arrays.asList(""), "age").isValid());
|
||||||
|
Assert.assertTrue(validator.validate(Arrays.asList(""), "age",valConfigIgnoreEmptyValues).isValid());
|
||||||
Assert.assertTrue(validator.validate(Arrays.asList(10), "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(" 10 "), "age").isValid());
|
||||||
|
|
||||||
|
@ -271,6 +320,13 @@ public class BuiltinValidatorsTest {
|
||||||
// min only
|
// min only
|
||||||
Assert.assertTrue(validator.validate("10", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 1).build()).isValid());
|
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());
|
Assert.assertFalse(validator.validate("10", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 100).build()).isValid());
|
||||||
|
// min behavior around empty values
|
||||||
|
Assert.assertFalse(validator.validate(null, "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 1).build()).isValid());
|
||||||
|
Assert.assertFalse(validator.validate("", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 1).build()).isValid());
|
||||||
|
Assert.assertFalse(validator.validate(" ", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 1).build()).isValid());
|
||||||
|
Assert.assertTrue(validator.validate(null, "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 1).config(valConfigIgnoreEmptyValues).build()).isValid());
|
||||||
|
Assert.assertTrue(validator.validate("", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 1).config(valConfigIgnoreEmptyValues).build()).isValid());
|
||||||
|
Assert.assertTrue(validator.validate(" ", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 1).config(valConfigIgnoreEmptyValues).build()).isValid());
|
||||||
|
|
||||||
// max only
|
// max only
|
||||||
Assert.assertFalse(validator.validate("10", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MAX, 1).build()).isValid());
|
Assert.assertFalse(validator.validate("10", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MAX, 1).build()).isValid());
|
||||||
|
@ -283,6 +339,7 @@ public class BuiltinValidatorsTest {
|
||||||
Assert.assertFalse(validator.validate("101", "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());
|
Assert.assertTrue(validator.validate(Long.MIN_VALUE, "name").isValid());
|
||||||
|
Assert.assertTrue(validator.validate(Long.MAX_VALUE, "name").isValid());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -337,11 +394,15 @@ public class BuiltinValidatorsTest {
|
||||||
Assert.assertFalse(validator.validate("start___end", "value", config).isValid());
|
Assert.assertFalse(validator.validate("start___end", "value", config).isValid());
|
||||||
|
|
||||||
// null and empty values handling
|
// null and empty values handling
|
||||||
// pattern not applied to null or empty string
|
Assert.assertFalse(validator.validate(null, "value", config).isValid());
|
||||||
Assert.assertTrue(validator.validate(null, "value", config).isValid());
|
|
||||||
Assert.assertFalse(validator.validate("", "value", config).isValid());
|
Assert.assertFalse(validator.validate("", "value", config).isValid());
|
||||||
// pattern is applied to blank string
|
|
||||||
Assert.assertFalse(validator.validate(" ", "value", config).isValid());
|
Assert.assertFalse(validator.validate(" ", "value", config).isValid());
|
||||||
|
|
||||||
|
// empty value ignoration configured
|
||||||
|
Assert.assertTrue(validator.validate(null, "value", valConfigIgnoreEmptyValues).isValid());
|
||||||
|
Assert.assertTrue(validator.validate("", "value", valConfigIgnoreEmptyValues).isValid());
|
||||||
|
Assert.assertTrue(validator.validate(" ", "value", valConfigIgnoreEmptyValues).isValid());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -18,17 +18,10 @@
|
||||||
package org.keycloak.services.validation;
|
package org.keycloak.services.validation;
|
||||||
|
|
||||||
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
|
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
|
||||||
import org.keycloak.models.KeycloakSession;
|
|
||||||
import org.keycloak.models.PasswordPolicy;
|
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.utils.FormMessage;
|
import org.keycloak.models.utils.FormMessage;
|
||||||
import org.keycloak.policy.PasswordPolicyManagerProvider;
|
|
||||||
import org.keycloak.policy.PolicyError;
|
|
||||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
|
||||||
import org.keycloak.services.messages.Messages;
|
|
||||||
import org.keycloak.userprofile.ValidationException;
|
import org.keycloak.userprofile.ValidationException;
|
||||||
|
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
@ -45,8 +38,8 @@ public class Validation {
|
||||||
// Actually allow same emails like angular. See ValidationTest.testEmailValidation()
|
// 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 static final Pattern EMAIL_PATTERN = Pattern.compile("[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*");
|
||||||
|
|
||||||
private static void addError(List<FormMessage> errors, String field, String message){
|
private static void addError(List<FormMessage> errors, String field, String message, Object... parameters){
|
||||||
errors.add(new FormMessage(field, message));
|
errors.add(new FormMessage(field, message, parameters));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -88,7 +81,7 @@ public class Validation {
|
||||||
public static List<FormMessage> getFormErrorsFromValidation(List<ValidationException.Error> errors) {
|
public static List<FormMessage> getFormErrorsFromValidation(List<ValidationException.Error> errors) {
|
||||||
List<FormMessage> messages = new ArrayList<>();
|
List<FormMessage> messages = new ArrayList<>();
|
||||||
for (ValidationException.Error error : errors) {
|
for (ValidationException.Error error : errors) {
|
||||||
addError(messages, error.getAttribute(), error.getMessage());
|
addError(messages, error.getAttribute(), error.getMessage(), error.getMessageParameters());
|
||||||
}
|
}
|
||||||
return messages;
|
return messages;
|
||||||
|
|
||||||
|
|
|
@ -20,12 +20,13 @@
|
||||||
package org.keycloak.userprofile.legacy;
|
package org.keycloak.userprofile.legacy;
|
||||||
|
|
||||||
import static org.keycloak.userprofile.DefaultAttributes.READ_ONLY_ATTRIBUTE_KEY;
|
import static org.keycloak.userprofile.DefaultAttributes.READ_ONLY_ATTRIBUTE_KEY;
|
||||||
import static org.keycloak.userprofile.UserProfileContext.*;
|
|
||||||
import static org.keycloak.userprofile.UserProfileContext.ACCOUNT;
|
import static org.keycloak.userprofile.UserProfileContext.ACCOUNT;
|
||||||
import static org.keycloak.userprofile.UserProfileContext.ACCOUNT_OLD;
|
import static org.keycloak.userprofile.UserProfileContext.ACCOUNT_OLD;
|
||||||
import static org.keycloak.userprofile.UserProfileContext.IDP_REVIEW;
|
import static org.keycloak.userprofile.UserProfileContext.IDP_REVIEW;
|
||||||
import static org.keycloak.userprofile.UserProfileContext.REGISTRATION_PROFILE;
|
import static org.keycloak.userprofile.UserProfileContext.REGISTRATION_PROFILE;
|
||||||
|
import static org.keycloak.userprofile.UserProfileContext.REGISTRATION_USER_CREATION;
|
||||||
import static org.keycloak.userprofile.UserProfileContext.UPDATE_PROFILE;
|
import static org.keycloak.userprofile.UserProfileContext.UPDATE_PROFILE;
|
||||||
|
import static org.keycloak.userprofile.UserProfileContext.USER_API;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
@ -36,15 +37,12 @@ import java.util.function.Function;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.common.util.ObjectUtil;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.RealmModel;
|
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.validation.Validation;
|
import org.keycloak.userprofile.AttributeValidatorMetadata;
|
||||||
import org.keycloak.userprofile.Attributes;
|
import org.keycloak.userprofile.Attributes;
|
||||||
import org.keycloak.userprofile.DefaultAttributes;
|
import org.keycloak.userprofile.DefaultAttributes;
|
||||||
import org.keycloak.userprofile.DefaultUserProfile;
|
import org.keycloak.userprofile.DefaultUserProfile;
|
||||||
|
@ -53,8 +51,19 @@ import org.keycloak.userprofile.UserProfileContext;
|
||||||
import org.keycloak.userprofile.UserProfileMetadata;
|
import org.keycloak.userprofile.UserProfileMetadata;
|
||||||
import org.keycloak.userprofile.UserProfileProvider;
|
import org.keycloak.userprofile.UserProfileProvider;
|
||||||
import org.keycloak.userprofile.UserProfileProviderFactory;
|
import org.keycloak.userprofile.UserProfileProviderFactory;
|
||||||
import org.keycloak.userprofile.AttributeValidatorMetadata;
|
import org.keycloak.userprofile.validator.BlankAttributeValidator;
|
||||||
import org.keycloak.userprofile.validation.Validator;
|
import org.keycloak.userprofile.validator.BrokeringFederatedUsernameHasValueValidator;
|
||||||
|
import org.keycloak.userprofile.validator.DuplicateEmailValidator;
|
||||||
|
import org.keycloak.userprofile.validator.DuplicateUsernameValidator;
|
||||||
|
import org.keycloak.userprofile.validator.EmailExistsAsUsernameValidator;
|
||||||
|
import org.keycloak.userprofile.validator.ReadOnlyAttributeUnchangedValidator;
|
||||||
|
import org.keycloak.userprofile.validator.RegistrationEmailAsUsernameEmailValueValidator;
|
||||||
|
import org.keycloak.userprofile.validator.RegistrationEmailAsUsernameUsernameValueValidator;
|
||||||
|
import org.keycloak.userprofile.validator.RegistrationUsernameExistsValidator;
|
||||||
|
import org.keycloak.userprofile.validator.UsernameHasValueValidator;
|
||||||
|
import org.keycloak.userprofile.validator.UsernameMutationValidator;
|
||||||
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
import org.keycloak.validate.validators.EmailValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>A base class for {@link UserProfileProvider} implementations providing the main hooks for customizations.
|
* <p>A base class for {@link UserProfileProvider} implementations providing the main hooks for customizations.
|
||||||
|
@ -63,8 +72,6 @@ import org.keycloak.userprofile.validation.Validator;
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractUserProfileProvider<U extends UserProfileProvider> implements UserProfileProvider, UserProfileProviderFactory<U> {
|
public abstract class AbstractUserProfileProvider<U extends UserProfileProvider> implements UserProfileProvider, UserProfileProviderFactory<U> {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(DefaultAttributes.class);
|
|
||||||
|
|
||||||
public static Pattern getRegexPatternString(String[] builtinReadOnlyAttributes) {
|
public static Pattern getRegexPatternString(String[] builtinReadOnlyAttributes) {
|
||||||
if (builtinReadOnlyAttributes != null) {
|
if (builtinReadOnlyAttributes != null) {
|
||||||
List<String> readOnlyAttributes = new ArrayList<>(Arrays.asList(builtinReadOnlyAttributes));
|
List<String> readOnlyAttributes = new ArrayList<>(Arrays.asList(builtinReadOnlyAttributes));
|
||||||
|
@ -82,55 +89,10 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Validator isReadOnlyAttributeUnchanged(Pattern pattern) {
|
|
||||||
return (context) -> {
|
|
||||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
|
||||||
String key = attribute.getKey();
|
|
||||||
|
|
||||||
if (!pattern.matcher(key).find()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> values = attribute.getValue();
|
|
||||||
|
|
||||||
if (values == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
UserModel user = context.getUser();
|
|
||||||
|
|
||||||
List<String> existingAttrValues = user == null ? null : user.getAttribute(key);
|
|
||||||
String existingValue = null;
|
|
||||||
|
|
||||||
if (existingAttrValues != null && !existingAttrValues.isEmpty()) {
|
|
||||||
existingValue = existingAttrValues.get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values.isEmpty() && existingValue != null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String value = null;
|
|
||||||
|
|
||||||
if (!values.isEmpty()) {
|
|
||||||
value = values.get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean result = ObjectUtil.isEqualOrBothNull(value, existingValue);
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
logger.warnf("Attempt to edit denied attribute '%s' of user '%s'", pattern, user == null ? "new user" : user.getFirstAttribute(UserModel.USERNAME));
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* There are the declarations for creating the built-in validations for read-only attributes. Regardless of the context where
|
* There are the declarations for creating the built-in validations for read-only attributes. Regardless of the context where
|
||||||
* user profiles are used. They are related to internal attributes with hard conditions on them in terms of management.
|
* user profiles are used. They are related to internal attributes with hard conditions on them in terms of management.
|
||||||
*/
|
*/
|
||||||
private static String UPDATE_READ_ONLY_ATTRIBUTES_REJECTED = "updateReadOnlyAttributesRejectedMessage";
|
|
||||||
private static String[] DEFAULT_READ_ONLY_ATTRIBUTES = { "KERBEROS_PRINCIPAL", "LDAP_ID", "LDAP_ENTRY_DN", "CREATED_TIMESTAMP", "createTimestamp", "modifyTimestamp", "userCertificate", "saml.persistent.name.id.for.*", "ENABLED", "EMAIL_VERIFIED", "disabledReason" };
|
private static String[] DEFAULT_READ_ONLY_ATTRIBUTES = { "KERBEROS_PRINCIPAL", "LDAP_ID", "LDAP_ENTRY_DN", "CREATED_TIMESTAMP", "createTimestamp", "modifyTimestamp", "userCertificate", "saml.persistent.name.id.for.*", "ENABLED", "EMAIL_VERIFIED", "disabledReason" };
|
||||||
private static String[] DEFAULT_ADMIN_READ_ONLY_ATTRIBUTES = { "KERBEROS_PRINCIPAL", "LDAP_ID", "LDAP_ENTRY_DN", "CREATED_TIMESTAMP", "createTimestamp", "modifyTimestamp" };
|
private static String[] DEFAULT_ADMIN_READ_ONLY_ATTRIBUTES = { "KERBEROS_PRINCIPAL", "LDAP_ID", "LDAP_ENTRY_DN", "CREATED_TIMESTAMP", "createTimestamp", "modifyTimestamp" };
|
||||||
private static Pattern readOnlyAttributesPattern = getRegexPatternString(DEFAULT_READ_ONLY_ATTRIBUTES);
|
private static Pattern readOnlyAttributesPattern = getRegexPatternString(DEFAULT_READ_ONLY_ATTRIBUTES);
|
||||||
|
@ -175,7 +137,7 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
||||||
AttributeValidatorMetadata readOnlyValidator = null;
|
AttributeValidatorMetadata readOnlyValidator = null;
|
||||||
|
|
||||||
if (pattern != null) {
|
if (pattern != null) {
|
||||||
readOnlyValidator = Validators.create(Messages.UPDATE_READ_ONLY_ATTRIBUTES_REJECTED, isReadOnlyAttributeUnchanged(pattern));
|
readOnlyValidator = createReadOnlyAttributeUnchangedValidator(pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
addContextualProfileMetadata(configureUserProfile(createBrokeringProfile(readOnlyValidator)));
|
addContextualProfileMetadata(configureUserProfile(createBrokeringProfile(readOnlyValidator)));
|
||||||
|
@ -187,6 +149,12 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
||||||
addContextualProfileMetadata(configureUserProfile(createUserResourceValidation(config)));
|
addContextualProfileMetadata(configureUserProfile(createUserResourceValidation(config)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AttributeValidatorMetadata createReadOnlyAttributeUnchangedValidator(Pattern pattern) {
|
||||||
|
return new AttributeValidatorMetadata(ReadOnlyAttributeUnchangedValidator.ID,
|
||||||
|
ValidatorConfig.builder().config(ReadOnlyAttributeUnchangedValidator.CFG_PATTERN, pattern)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void postInit(KeycloakSessionFactory factory) {
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
}
|
}
|
||||||
|
@ -279,56 +247,11 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
||||||
private UserProfileMetadata createRegistrationUserCreationProfile() {
|
private UserProfileMetadata createRegistrationUserCreationProfile() {
|
||||||
UserProfileMetadata metadata = new UserProfileMetadata(REGISTRATION_USER_CREATION);
|
UserProfileMetadata metadata = new UserProfileMetadata(REGISTRATION_USER_CREATION);
|
||||||
|
|
||||||
metadata.addAttribute(UserModel.USERNAME, Validators.create(Messages.MISSING_USERNAME, (context) -> {
|
metadata.addAttribute(UserModel.USERNAME, new AttributeValidatorMetadata(RegistrationEmailAsUsernameUsernameValueValidator.ID), new AttributeValidatorMetadata(RegistrationUsernameExistsValidator.ID));
|
||||||
RealmModel realm = context.getSession().getContext().getRealm();
|
|
||||||
|
|
||||||
if (!realm.isRegistrationEmailAsUsername()) {
|
metadata.addAttribute(UserModel.EMAIL, new AttributeValidatorMetadata(RegistrationEmailAsUsernameEmailValueValidator.ID));
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Validators.isBlank().validate(context);
|
metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, createReadOnlyAttributeUnchangedValidator(readOnlyAttributesPattern));
|
||||||
}), Validators.create(Messages.USERNAME_EXISTS,
|
|
||||||
(context) -> {
|
|
||||||
KeycloakSession session = context.getSession();
|
|
||||||
RealmModel realm = session.getContext().getRealm();
|
|
||||||
|
|
||||||
if (realm.isRegistrationEmailAsUsername()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
|
||||||
List<String> values = attribute.getValue();
|
|
||||||
|
|
||||||
if (values.isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String value = values.get(0);
|
|
||||||
|
|
||||||
UserModel existing = session.users().getUserByUsername(realm, value);
|
|
||||||
return existing == null;
|
|
||||||
}));
|
|
||||||
|
|
||||||
metadata.addAttribute(UserModel.EMAIL, Validators.create(Messages.INVALID_EMAIL, (context) -> {
|
|
||||||
RealmModel realm = context.getSession().getContext().getRealm();
|
|
||||||
|
|
||||||
if (!realm.isRegistrationEmailAsUsername()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
|
||||||
List<String> values = attribute.getValue();
|
|
||||||
|
|
||||||
if (values.isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String value = values.get(0);
|
|
||||||
|
|
||||||
return Validation.isBlank(value) || Validation.isEmailValid(value);
|
|
||||||
}));
|
|
||||||
|
|
||||||
metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, new AttributeValidatorMetadata(UPDATE_READ_ONLY_ATTRIBUTES_REJECTED, isReadOnlyAttributeUnchanged(readOnlyAttributesPattern)));
|
|
||||||
|
|
||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
|
@ -336,23 +259,23 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
||||||
private UserProfileMetadata createDefaultProfile(UserProfileContext context, AttributeValidatorMetadata readOnlyValidator) {
|
private UserProfileMetadata createDefaultProfile(UserProfileContext context, AttributeValidatorMetadata readOnlyValidator) {
|
||||||
UserProfileMetadata metadata = new UserProfileMetadata(context);
|
UserProfileMetadata metadata = new UserProfileMetadata(context);
|
||||||
|
|
||||||
metadata.addAttribute(UserModel.USERNAME, Validators.create(Messages.MISSING_USERNAME, Validators.checkUsernameExists()),
|
metadata.addAttribute(UserModel.USERNAME, new AttributeValidatorMetadata(UsernameHasValueValidator.ID),
|
||||||
Validators.create(Messages.USERNAME_EXISTS, Validators.userNameExists()),
|
new AttributeValidatorMetadata(DuplicateUsernameValidator.ID),
|
||||||
Validators.create(Messages.READ_ONLY_USERNAME, Validators.isUserMutable()));
|
new AttributeValidatorMetadata(UsernameMutationValidator.ID));
|
||||||
|
|
||||||
metadata.addAttribute(UserModel.FIRST_NAME, Validators.create(Messages.MISSING_FIRST_NAME, Validators.isBlank()));
|
metadata.addAttribute(UserModel.FIRST_NAME, new AttributeValidatorMetadata(BlankAttributeValidator.ID,
|
||||||
|
BlankAttributeValidator.createConfig(Messages.MISSING_FIRST_NAME)));
|
||||||
|
|
||||||
metadata.addAttribute(UserModel.LAST_NAME, Validators.create(Messages.MISSING_LAST_NAME, Validators.isBlank()));
|
metadata.addAttribute(UserModel.LAST_NAME, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_LAST_NAME)));
|
||||||
|
|
||||||
metadata.addAttribute(UserModel.EMAIL, Validators.create(Messages.MISSING_EMAIL, Validators.isBlank()),
|
metadata.addAttribute(UserModel.EMAIL, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_EMAIL)),
|
||||||
Validators.create(Messages.INVALID_EMAIL, Validators.isEmailValid()),
|
new AttributeValidatorMetadata(EmailValidator.ID, ValidatorConfig.builder().config(EmailValidator.IGNORE_EMPTY_VALUE, true).build()),
|
||||||
Validators.create(Messages.EMAIL_EXISTS, Validators.isEmailDuplicated()),
|
new AttributeValidatorMetadata(DuplicateEmailValidator.ID),
|
||||||
Validators.create(Messages.USERNAME_EXISTS, Validators.doesEmailExistAsUsername()));
|
new AttributeValidatorMetadata(EmailExistsAsUsernameValidator.ID));
|
||||||
|
|
||||||
List<AttributeValidatorMetadata> readonlyValidators = new ArrayList<>();
|
List<AttributeValidatorMetadata> readonlyValidators = new ArrayList<>();
|
||||||
|
|
||||||
readonlyValidators.add(new AttributeValidatorMetadata(UPDATE_READ_ONLY_ATTRIBUTES_REJECTED,
|
readonlyValidators.add(createReadOnlyAttributeUnchangedValidator(readOnlyAttributesPattern));
|
||||||
isReadOnlyAttributeUnchanged(readOnlyAttributesPattern)));
|
|
||||||
|
|
||||||
if (readOnlyValidator != null) {
|
if (readOnlyValidator != null) {
|
||||||
readonlyValidators.add(readOnlyValidator);
|
readonlyValidators.add(readOnlyValidator);
|
||||||
|
@ -366,22 +289,18 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
||||||
private UserProfileMetadata createBrokeringProfile(AttributeValidatorMetadata readOnlyValidator) {
|
private UserProfileMetadata createBrokeringProfile(AttributeValidatorMetadata readOnlyValidator) {
|
||||||
UserProfileMetadata metadata = new UserProfileMetadata(IDP_REVIEW);
|
UserProfileMetadata metadata = new UserProfileMetadata(IDP_REVIEW);
|
||||||
|
|
||||||
metadata.addAttribute(UserModel.USERNAME, Validators
|
metadata.addAttribute(UserModel.USERNAME, new AttributeValidatorMetadata(BrokeringFederatedUsernameHasValueValidator.ID));
|
||||||
.create(Messages.MISSING_USERNAME, Validators.checkFederatedUsernameExists()));
|
|
||||||
|
|
||||||
metadata.addAttribute(UserModel.FIRST_NAME,
|
metadata.addAttribute(UserModel.FIRST_NAME, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_FIRST_NAME)));
|
||||||
Validators.create(Messages.MISSING_FIRST_NAME, Validators.isBlank()));
|
|
||||||
|
|
||||||
metadata.addAttribute(UserModel.LAST_NAME,
|
metadata.addAttribute(UserModel.LAST_NAME, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_LAST_NAME)));
|
||||||
Validators.create(Messages.MISSING_LAST_NAME, Validators.isBlank()));
|
|
||||||
|
|
||||||
metadata.addAttribute(UserModel.EMAIL, Validators.create(Messages.MISSING_EMAIL, Validators.isBlank()),
|
metadata.addAttribute(UserModel.EMAIL, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_EMAIL)),
|
||||||
Validators.create(Messages.INVALID_EMAIL, Validators.isEmailValid()));
|
new AttributeValidatorMetadata(EmailValidator.ID));
|
||||||
|
|
||||||
List<AttributeValidatorMetadata> readonlyValidators = new ArrayList<>();
|
List<AttributeValidatorMetadata> readonlyValidators = new ArrayList<>();
|
||||||
|
|
||||||
readonlyValidators.add(new AttributeValidatorMetadata(UPDATE_READ_ONLY_ATTRIBUTES_REJECTED,
|
readonlyValidators.add(createReadOnlyAttributeUnchangedValidator(readOnlyAttributesPattern));
|
||||||
isReadOnlyAttributeUnchanged(readOnlyAttributesPattern)));
|
|
||||||
|
|
||||||
if (readOnlyValidator != null) {
|
if (readOnlyValidator != null) {
|
||||||
readonlyValidators.add(readOnlyValidator);
|
readonlyValidators.add(readOnlyValidator);
|
||||||
|
@ -398,11 +317,10 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
||||||
List<AttributeValidatorMetadata> readonlyValidators = new ArrayList<>();
|
List<AttributeValidatorMetadata> readonlyValidators = new ArrayList<>();
|
||||||
|
|
||||||
if (p != null) {
|
if (p != null) {
|
||||||
readonlyValidators.add(Validators.create(Messages.UPDATE_READ_ONLY_ATTRIBUTES_REJECTED, isReadOnlyAttributeUnchanged(p)));
|
readonlyValidators.add(createReadOnlyAttributeUnchangedValidator(p));
|
||||||
}
|
}
|
||||||
|
|
||||||
readonlyValidators.add(new AttributeValidatorMetadata(UPDATE_READ_ONLY_ATTRIBUTES_REJECTED,
|
readonlyValidators.add(createReadOnlyAttributeUnchangedValidator(adminReadOnlyAttributesPattern));
|
||||||
isReadOnlyAttributeUnchanged(adminReadOnlyAttributesPattern)));
|
|
||||||
|
|
||||||
metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, readonlyValidators);
|
metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, readonlyValidators);
|
||||||
|
|
||||||
|
|
|
@ -1,278 +0,0 @@
|
||||||
/*
|
|
||||||
*
|
|
||||||
* * 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.userprofile.legacy;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
|
||||||
import org.keycloak.models.RealmModel;
|
|
||||||
import org.keycloak.models.UserModel;
|
|
||||||
import org.keycloak.services.validation.Validation;
|
|
||||||
import org.keycloak.userprofile.AttributeContext;
|
|
||||||
import org.keycloak.userprofile.AttributeMetadata;
|
|
||||||
import org.keycloak.userprofile.AttributeValidatorMetadata;
|
|
||||||
import org.keycloak.userprofile.UserProfileContext;
|
|
||||||
import org.keycloak.userprofile.validation.Validator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Functions are supposed to return:
|
|
||||||
* - true if validation success
|
|
||||||
* - false if validation fails
|
|
||||||
*
|
|
||||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
|
||||||
*/
|
|
||||||
public class Validators {
|
|
||||||
|
|
||||||
public static final AttributeValidatorMetadata create(String message, Validator validator) {
|
|
||||||
return new AttributeValidatorMetadata(message, validator);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final Validator isBlank() {
|
|
||||||
return (context) -> {
|
|
||||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
|
||||||
List<String> values = attribute.getValue();
|
|
||||||
|
|
||||||
if (values.isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String value = values.get(0);
|
|
||||||
|
|
||||||
return value == null || !Validation.isBlank(value);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final Validator isEmailValid() {
|
|
||||||
return (context) -> {
|
|
||||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
|
||||||
List<String> values = attribute.getValue();
|
|
||||||
|
|
||||||
if (values.isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String value = values.get(0);
|
|
||||||
|
|
||||||
return Validation.isBlank(value) || Validation.isEmailValid(value);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final Validator userNameExists() {
|
|
||||||
return (context) -> {
|
|
||||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
|
||||||
List<String> values = attribute.getValue();
|
|
||||||
|
|
||||||
if (values.isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String value = values.get(0);
|
|
||||||
|
|
||||||
if (Validation.isBlank(value)) return true;
|
|
||||||
|
|
||||||
KeycloakSession session = context.getSession();
|
|
||||||
UserModel existing = session.users().getUserByUsername(session.getContext().getRealm(), value);
|
|
||||||
UserModel user = context.getUser();
|
|
||||||
|
|
||||||
return !(user != null
|
|
||||||
&& !value.equals(user.getFirstAttribute(UserModel.USERNAME))
|
|
||||||
&& (existing != null && !existing.getId().equals(user.getId())));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final Validator isUserMutable() {
|
|
||||||
return (context) -> {
|
|
||||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
|
||||||
List<String> values = attribute.getValue();
|
|
||||||
|
|
||||||
if (values.isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String value = values.get(0);
|
|
||||||
|
|
||||||
if (Validation.isBlank(value)) return true;
|
|
||||||
|
|
||||||
UserModel user = context.getUser();
|
|
||||||
RealmModel realm = context.getSession().getContext().getRealm();
|
|
||||||
|
|
||||||
return !(!realm.isEditUsernameAllowed()
|
|
||||||
&& user != null
|
|
||||||
&& !value.equals(user.getFirstAttribute(UserModel.USERNAME))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final Validator checkFederatedUsernameExists() {
|
|
||||||
return (context) -> {
|
|
||||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
|
||||||
List<String> values = attribute.getValue();
|
|
||||||
String value = null;
|
|
||||||
|
|
||||||
if (!values.isEmpty()) {
|
|
||||||
value = values.get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
RealmModel realm = context.getSession().getContext().getRealm();
|
|
||||||
|
|
||||||
return !(!realm.isRegistrationEmailAsUsername() && Validation.isBlank(value));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final Validator checkUsernameExists() {
|
|
||||||
return (context) -> {
|
|
||||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
|
||||||
List<String> values = attribute.getValue();
|
|
||||||
String value = null;
|
|
||||||
|
|
||||||
if (!values.isEmpty()) {
|
|
||||||
value = values.get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return !Validation.isBlank(value);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final Validator doesEmailExistAsUsername() {
|
|
||||||
return (context) -> {
|
|
||||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
|
||||||
List<String> values = attribute.getValue();
|
|
||||||
|
|
||||||
if (values.isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String value = values.get(0);
|
|
||||||
|
|
||||||
if (Validation.isBlank(value)) return true;
|
|
||||||
|
|
||||||
KeycloakSession session = context.getSession();
|
|
||||||
RealmModel realm = session.getContext().getRealm();
|
|
||||||
UserModel user = context.getUser();
|
|
||||||
|
|
||||||
if (!realm.isDuplicateEmailsAllowed()) {
|
|
||||||
UserModel userByEmail = session.users().getUserByEmail(realm, value);
|
|
||||||
return !(realm.isRegistrationEmailAsUsername() && userByEmail != null && user != null && !userByEmail.getId().equals(user.getId()));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final Validator isEmailDuplicated() {
|
|
||||||
return (context) -> {
|
|
||||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
|
||||||
List<String> values = attribute.getValue();
|
|
||||||
|
|
||||||
if (values.isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String value = values.get(0);
|
|
||||||
|
|
||||||
if (Validation.isBlank(value)) return true;
|
|
||||||
|
|
||||||
KeycloakSession session = context.getSession();
|
|
||||||
RealmModel realm = session.getContext().getRealm();
|
|
||||||
|
|
||||||
if (!realm.isDuplicateEmailsAllowed()) {
|
|
||||||
UserModel userByEmail = session.users().getUserByEmail(realm, value);
|
|
||||||
UserModel user = context.getUser();
|
|
||||||
// check for duplicated email
|
|
||||||
return !(userByEmail != null && (user == null || !userByEmail.getId().equals(user.getId())));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final Validator doesEmailExist(KeycloakSession session) {
|
|
||||||
return (context) -> {
|
|
||||||
if (UserProfileContext.REGISTRATION_USER_CREATION.equals(context.getContext())) {
|
|
||||||
RealmModel realm = context.getSession().getContext().getRealm();
|
|
||||||
|
|
||||||
if (!realm.isRegistrationEmailAsUsername()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
|
||||||
List<String> values = attribute.getValue();
|
|
||||||
String value = values.get(0);
|
|
||||||
|
|
||||||
return !(value != null
|
|
||||||
&& !session.getContext().getRealm().isDuplicateEmailsAllowed()
|
|
||||||
&& session.users().getUserByEmail(session.getContext().getRealm(), value) != null);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate String length based on the configuration if string is not blank.
|
|
||||||
*
|
|
||||||
* @param config can contain "max" and "min" keys with integer values
|
|
||||||
* @return true if string is blank or conforms min and max configurations
|
|
||||||
*/
|
|
||||||
public static final Validator length(final Map<String, Object> config) {
|
|
||||||
return (context) -> {
|
|
||||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
|
||||||
List<String> values = attribute.getValue();
|
|
||||||
|
|
||||||
if (values == null || values.isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String value = values.get(0);
|
|
||||||
|
|
||||||
if (Validation.isBlank(value))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (config.containsKey("min") && value.length() < (Integer) config.get("min")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (config.containsKey("max") && value.length() > (Integer) config.get("max")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validator for "required" validation based on evaluation of the {@link AttributeMetadata#isRequired(AttributeContext)}.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public static final Validator requiredByAttributeMetadata() {
|
|
||||||
return (context) -> {
|
|
||||||
if(!context.getMetadata().isRequired(context)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
|
||||||
List<String> values = attribute.getValue();
|
|
||||||
|
|
||||||
if (values == null || values.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String value = values.get(0);
|
|
||||||
|
|
||||||
return !Validation.isBlank(value);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* 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.userprofile.validator;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.services.validation.Validation;
|
||||||
|
import org.keycloak.userprofile.AttributeContext;
|
||||||
|
import org.keycloak.userprofile.UserProfileAttributeValidationContext;
|
||||||
|
import org.keycloak.validate.SimpleValidator;
|
||||||
|
import org.keycloak.validate.ValidationContext;
|
||||||
|
import org.keycloak.validate.ValidationError;
|
||||||
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator to check that User Profile attribute value is not blank (nor null) if the attribute is required based on
|
||||||
|
* AttributeMetadata predicate. Expects List of Strings as input.
|
||||||
|
*
|
||||||
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class AttributeRequiredByMetadataValidator implements SimpleValidator {
|
||||||
|
|
||||||
|
public static final String ERROR_USER_ATTRIBUTE_REQUIRED = "error-user-attribute-required";
|
||||||
|
|
||||||
|
public static final String ID = "up-attribute-required-by-metadata-value";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||||
|
|
||||||
|
AttributeContext attContext = UserProfileAttributeValidationContext.from(context).getAttributeContext();
|
||||||
|
|
||||||
|
if (!attContext.getMetadata().isRequired(attContext)) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> values = (List<String>) input;
|
||||||
|
|
||||||
|
if (values == null || values.isEmpty()) {
|
||||||
|
context.addError(new ValidationError(ID, inputHint, ERROR_USER_ATTRIBUTE_REQUIRED));
|
||||||
|
} else {
|
||||||
|
for (String value : values) {
|
||||||
|
if (value == null || Validation.isBlank(value)) {
|
||||||
|
context.addError(new ValidationError(ID, inputHint, ERROR_USER_ATTRIBUTE_REQUIRED));
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
* 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.userprofile.validator;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.services.validation.Validation;
|
||||||
|
import org.keycloak.validate.SimpleValidator;
|
||||||
|
import org.keycloak.validate.ValidationContext;
|
||||||
|
import org.keycloak.validate.ValidationError;
|
||||||
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator to check that User Profile attribute value is not blank (null value is OK!). Expects List of Strings as
|
||||||
|
* input.
|
||||||
|
*
|
||||||
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class BlankAttributeValidator implements SimpleValidator {
|
||||||
|
|
||||||
|
public static final String ID = "up-blank-attribute-value";
|
||||||
|
|
||||||
|
public static final String CFG_ERROR_MESSAGE = "error-message";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> values = (List<String>) input;
|
||||||
|
|
||||||
|
if (values.isEmpty()) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
String value = values.get(0);
|
||||||
|
|
||||||
|
if (value != null && Validation.isBlank(value)) {
|
||||||
|
context.addError(new ValidationError(ID, inputHint, config.getStringOrDefault(CFG_ERROR_MESSAGE, AttributeRequiredByMetadataValidator.ERROR_USER_ATTRIBUTE_REQUIRED)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create config for this validator to get customized error message
|
||||||
|
*
|
||||||
|
* @param errorMessage to be used if validation fails
|
||||||
|
* @return config
|
||||||
|
*/
|
||||||
|
public static ValidatorConfig createConfig(String errorMessage) {
|
||||||
|
if (errorMessage != null) {
|
||||||
|
return ValidatorConfig.builder().config(CFG_ERROR_MESSAGE, errorMessage).build();
|
||||||
|
}
|
||||||
|
return ValidatorConfig.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* 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.userprofile.validator;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.services.validation.Validation;
|
||||||
|
import org.keycloak.validate.SimpleValidator;
|
||||||
|
import org.keycloak.validate.ValidationContext;
|
||||||
|
import org.keycloak.validate.ValidationError;
|
||||||
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator to check that User Profile username is provided during Brokerin/Federation. Expects List of Strings as
|
||||||
|
* input.
|
||||||
|
*
|
||||||
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class BrokeringFederatedUsernameHasValueValidator implements SimpleValidator {
|
||||||
|
|
||||||
|
public static final String ID = "up-brokering-federated-username-has-value";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> values = (List<String>) input;
|
||||||
|
|
||||||
|
String value = null;
|
||||||
|
|
||||||
|
if (!values.isEmpty()) {
|
||||||
|
value = values.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
RealmModel realm = context.getSession().getContext().getRealm();
|
||||||
|
|
||||||
|
if (!realm.isRegistrationEmailAsUsername() && Validation.isBlank(value)) {
|
||||||
|
context.addError(new ValidationError(ID, inputHint, Messages.MISSING_USERNAME));
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* 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.userprofile.validator;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.services.validation.Validation;
|
||||||
|
import org.keycloak.userprofile.UserProfileAttributeValidationContext;
|
||||||
|
import org.keycloak.validate.SimpleValidator;
|
||||||
|
import org.keycloak.validate.ValidationContext;
|
||||||
|
import org.keycloak.validate.ValidationError;
|
||||||
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator to check User Profile email duplication conditions based on realm settings like isDuplicateEmailsAllowed.
|
||||||
|
* Expects List of Strings as input.
|
||||||
|
*
|
||||||
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class DuplicateEmailValidator implements SimpleValidator {
|
||||||
|
|
||||||
|
public static final String ID = "up-duplicate-email";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> values = (List<String>) input;
|
||||||
|
|
||||||
|
if (values == null || values.isEmpty()) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
String value = values.get(0);
|
||||||
|
|
||||||
|
if (Validation.isBlank(value))
|
||||||
|
return context;
|
||||||
|
|
||||||
|
KeycloakSession session = context.getSession();
|
||||||
|
RealmModel realm = session.getContext().getRealm();
|
||||||
|
|
||||||
|
if (!realm.isDuplicateEmailsAllowed()) {
|
||||||
|
UserModel userByEmail = session.users().getUserByEmail(realm, value);
|
||||||
|
UserModel user = UserProfileAttributeValidationContext.from(context).getAttributeContext().getUser();
|
||||||
|
// check for duplicated email
|
||||||
|
if (userByEmail != null && (user == null || !userByEmail.getId().equals(user.getId()))) {
|
||||||
|
context.addError(new ValidationError(ID, inputHint, Messages.EMAIL_EXISTS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* 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.userprofile.validator;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.services.validation.Validation;
|
||||||
|
import org.keycloak.userprofile.UserProfileAttributeValidationContext;
|
||||||
|
import org.keycloak.validate.SimpleValidator;
|
||||||
|
import org.keycloak.validate.ValidationContext;
|
||||||
|
import org.keycloak.validate.ValidationError;
|
||||||
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator to check that User Profile username already exists in database for another user in case of it's change, and
|
||||||
|
* fail in this case. Expects List of Strings as input.
|
||||||
|
*
|
||||||
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class DuplicateUsernameValidator implements SimpleValidator {
|
||||||
|
|
||||||
|
public static final String ID = "up-duplicate-username";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> values = (List<String>) input;
|
||||||
|
|
||||||
|
if (values.isEmpty()) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
String value = values.get(0);
|
||||||
|
|
||||||
|
if (Validation.isBlank(value))
|
||||||
|
return context;
|
||||||
|
|
||||||
|
KeycloakSession session = context.getSession();
|
||||||
|
UserModel existing = session.users().getUserByUsername(session.getContext().getRealm(), value);
|
||||||
|
UserModel user = UserProfileAttributeValidationContext.from(context).getAttributeContext().getUser();
|
||||||
|
|
||||||
|
if (user != null && !value.equals(user.getFirstAttribute(UserModel.USERNAME)) && (existing != null && !existing.getId().equals(user.getId()))) {
|
||||||
|
context.addError(new ValidationError(ID, inputHint, Messages.USERNAME_EXISTS));
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
* 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.userprofile.validator;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.services.validation.Validation;
|
||||||
|
import org.keycloak.userprofile.UserProfileAttributeValidationContext;
|
||||||
|
import org.keycloak.validate.SimpleValidator;
|
||||||
|
import org.keycloak.validate.ValidationContext;
|
||||||
|
import org.keycloak.validate.ValidationError;
|
||||||
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator to check User Profile email duplication conditions if isDuplicateEmailsAllowed is false but
|
||||||
|
* isRegistrationEmailAsUsername is true. Expects List of Strings as input.
|
||||||
|
*
|
||||||
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class EmailExistsAsUsernameValidator implements SimpleValidator {
|
||||||
|
|
||||||
|
public static final String ID = "up-email-exists-as-username";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> values = (List<String>) input;
|
||||||
|
|
||||||
|
if (values == null || values.isEmpty()) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
String value = values.get(0);
|
||||||
|
|
||||||
|
if (Validation.isBlank(value))
|
||||||
|
return context;
|
||||||
|
|
||||||
|
KeycloakSession session = context.getSession();
|
||||||
|
RealmModel realm = session.getContext().getRealm();
|
||||||
|
|
||||||
|
if (!realm.isDuplicateEmailsAllowed() && realm.isRegistrationEmailAsUsername()) {
|
||||||
|
UserModel user = UserProfileAttributeValidationContext.from(context).getAttributeContext().getUser();
|
||||||
|
UserModel userByEmail = session.users().getUserByEmail(realm, value);
|
||||||
|
if (userByEmail != null && user != null && !userByEmail.getId().equals(user.getId())) {
|
||||||
|
context.addError(new ValidationError(ID, inputHint, Messages.USERNAME_EXISTS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
* 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.userprofile.validator;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.common.util.ObjectUtil;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.userprofile.AttributeContext;
|
||||||
|
import org.keycloak.userprofile.UserProfileAttributeValidationContext;
|
||||||
|
import org.keycloak.validate.SimpleValidator;
|
||||||
|
import org.keycloak.validate.ValidationContext;
|
||||||
|
import org.keycloak.validate.ValidationError;
|
||||||
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator to check that User Profile attribute value is not changed if attribute is read-only. Expects List of
|
||||||
|
* Strings as input.
|
||||||
|
*
|
||||||
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class ReadOnlyAttributeUnchangedValidator implements SimpleValidator {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(ReadOnlyAttributeUnchangedValidator.class);
|
||||||
|
|
||||||
|
public static final String ID = "up-readonly-attribute-unchanged";
|
||||||
|
|
||||||
|
public static final String CFG_PATTERN = "pattern";
|
||||||
|
|
||||||
|
public static String UPDATE_READ_ONLY_ATTRIBUTES_REJECTED_MSG = "updateReadOnlyAttributesRejectedMessage";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||||
|
|
||||||
|
AttributeContext attributeContext = UserProfileAttributeValidationContext.from(context).getAttributeContext();
|
||||||
|
Map.Entry<String, List<String>> attribute = attributeContext.getAttribute();
|
||||||
|
String key = attribute.getKey();
|
||||||
|
|
||||||
|
Pattern pattern = (Pattern) config.get(CFG_PATTERN);
|
||||||
|
if (!pattern.matcher(key).find()) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> values = (List<String>) input;
|
||||||
|
|
||||||
|
if (values == null) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserModel user = attributeContext.getUser();
|
||||||
|
|
||||||
|
List<String> existingAttrValues = user == null ? null : user.getAttribute(key);
|
||||||
|
String existingValue = null;
|
||||||
|
|
||||||
|
if (existingAttrValues != null && !existingAttrValues.isEmpty()) {
|
||||||
|
existingValue = existingAttrValues.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.isEmpty() && existingValue != null) {
|
||||||
|
context.addError(new ValidationError(ID, key, UPDATE_READ_ONLY_ATTRIBUTES_REJECTED_MSG));
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
String value = null;
|
||||||
|
|
||||||
|
if (!values.isEmpty()) {
|
||||||
|
value = values.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean unchanged = ObjectUtil.isEqualOrBothNull(value, existingValue);
|
||||||
|
|
||||||
|
if (!unchanged) {
|
||||||
|
logger.warnf("Attempt to edit denied attribute '%s' of user '%s'", pattern, user == null ? "new user" : user.getFirstAttribute(UserModel.USERNAME));
|
||||||
|
context.addError(new ValidationError(ID, key, UPDATE_READ_ONLY_ATTRIBUTES_REJECTED_MSG));
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* 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.userprofile.validator;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.services.validation.Validation;
|
||||||
|
import org.keycloak.validate.SimpleValidator;
|
||||||
|
import org.keycloak.validate.ValidationContext;
|
||||||
|
import org.keycloak.validate.ValidationError;
|
||||||
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator to check User Profile email attribute value during Registration when "RegistrationEmailAsUsername()" is
|
||||||
|
* enabled. Expects List of Strings as input.
|
||||||
|
*
|
||||||
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class RegistrationEmailAsUsernameEmailValueValidator implements SimpleValidator {
|
||||||
|
|
||||||
|
public static final String ID = "up-registration-email-as-username-email-value";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||||
|
|
||||||
|
RealmModel realm = context.getSession().getContext().getRealm();
|
||||||
|
|
||||||
|
if (!realm.isRegistrationEmailAsUsername()) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> values = (List<String>) input;
|
||||||
|
|
||||||
|
if (values == null || values.isEmpty()) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
String value = values.get(0);
|
||||||
|
|
||||||
|
if (!(Validation.isBlank(value) || Validation.isEmailValid(value))) {
|
||||||
|
context.addError(new ValidationError(ID, inputHint, Messages.INVALID_EMAIL));
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* 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.userprofile.validator;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.services.validation.Validation;
|
||||||
|
import org.keycloak.validate.SimpleValidator;
|
||||||
|
import org.keycloak.validate.ValidationContext;
|
||||||
|
import org.keycloak.validate.ValidationError;
|
||||||
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator to check User Profile username attribute value during Registration when "RegistrationEmailAsUsername()" is
|
||||||
|
* enabled. Expects List of Strings as input.
|
||||||
|
*
|
||||||
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class RegistrationEmailAsUsernameUsernameValueValidator implements SimpleValidator {
|
||||||
|
|
||||||
|
public static final String ID = "up-registration-email-as-username-username-value";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||||
|
|
||||||
|
RealmModel realm = context.getSession().getContext().getRealm();
|
||||||
|
|
||||||
|
if (!realm.isRegistrationEmailAsUsername()) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> values = (List<String>) input;
|
||||||
|
|
||||||
|
if (values == null || values.isEmpty()) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
String value = values.get(0);
|
||||||
|
|
||||||
|
if (value != null && Validation.isBlank(value)) {
|
||||||
|
context.addError(new ValidationError(ID, inputHint, Messages.MISSING_USERNAME));
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* 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.userprofile.validator;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.validate.SimpleValidator;
|
||||||
|
import org.keycloak.validate.ValidationContext;
|
||||||
|
import org.keycloak.validate.ValidationError;
|
||||||
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator to check User Profile username attribute uniqueness during registration (when
|
||||||
|
* "RegistrationEmailAsUsername()" is NOT enabled). Expects List of Strings as input.
|
||||||
|
*
|
||||||
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class RegistrationUsernameExistsValidator implements SimpleValidator {
|
||||||
|
|
||||||
|
public static final String ID = "up-registration-username-exists";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||||
|
|
||||||
|
KeycloakSession session = context.getSession();
|
||||||
|
RealmModel realm = session.getContext().getRealm();
|
||||||
|
|
||||||
|
if (realm.isRegistrationEmailAsUsername()) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> values = (List<String>) input;
|
||||||
|
|
||||||
|
if (values == null || values.isEmpty()) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
String value = values.get(0);
|
||||||
|
|
||||||
|
UserModel existing = session.users().getUserByUsername(realm, value);
|
||||||
|
if (existing != null) {
|
||||||
|
context.addError(new ValidationError(ID, inputHint, Messages.USERNAME_EXISTS));
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* 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.userprofile.validator;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.services.validation.Validation;
|
||||||
|
import org.keycloak.validate.SimpleValidator;
|
||||||
|
import org.keycloak.validate.ValidationContext;
|
||||||
|
import org.keycloak.validate.ValidationError;
|
||||||
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator to check that User Profile username is provided. Expects List of Strings as input.
|
||||||
|
*
|
||||||
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class UsernameHasValueValidator implements SimpleValidator {
|
||||||
|
|
||||||
|
public static final String ID = "up-username-has-value";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> values = (List<String>) input;
|
||||||
|
|
||||||
|
String value = null;
|
||||||
|
|
||||||
|
if (values != null && !values.isEmpty()) {
|
||||||
|
value = values.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Validation.isBlank(value)) {
|
||||||
|
context.addError(new ValidationError(ID, inputHint, Messages.MISSING_USERNAME));
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* 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.userprofile.validator;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.services.validation.Validation;
|
||||||
|
import org.keycloak.userprofile.UserProfileAttributeValidationContext;
|
||||||
|
import org.keycloak.validate.SimpleValidator;
|
||||||
|
import org.keycloak.validate.ValidationContext;
|
||||||
|
import org.keycloak.validate.ValidationError;
|
||||||
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator to check User Profile username change and prevent it if not allowed in realm. Expects List of Strings as
|
||||||
|
* input.
|
||||||
|
*
|
||||||
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class UsernameMutationValidator implements SimpleValidator {
|
||||||
|
|
||||||
|
public static final String ID = "up-username-mutation";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> values = (List<String>) input;
|
||||||
|
|
||||||
|
if (values.isEmpty()) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
String value = values.get(0);
|
||||||
|
|
||||||
|
if (Validation.isBlank(value)) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserModel user = UserProfileAttributeValidationContext.from(context).getAttributeContext().getUser();
|
||||||
|
RealmModel realm = context.getSession().getContext().getRealm();
|
||||||
|
|
||||||
|
if (!realm.isEditUsernameAllowed() && user != null && !value.equals(user.getFirstAttribute(UserModel.USERNAME))) {
|
||||||
|
context.addError(new ValidationError(ID, inputHint, Messages.READ_ONLY_USERNAME));
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
org.keycloak.userprofile.validator.BlankAttributeValidator
|
||||||
|
org.keycloak.userprofile.validator.AttributeRequiredByMetadataValidator
|
||||||
|
org.keycloak.userprofile.validator.ReadOnlyAttributeUnchangedValidator
|
||||||
|
org.keycloak.userprofile.validator.DuplicateUsernameValidator
|
||||||
|
org.keycloak.userprofile.validator.UsernameHasValueValidator
|
||||||
|
org.keycloak.userprofile.validator.UsernameMutationValidator
|
||||||
|
org.keycloak.userprofile.validator.DuplicateEmailValidator
|
||||||
|
org.keycloak.userprofile.validator.EmailExistsAsUsernameValidator
|
||||||
|
org.keycloak.userprofile.validator.RegistrationEmailAsUsernameUsernameValueValidator
|
||||||
|
org.keycloak.userprofile.validator.RegistrationUsernameExistsValidator
|
||||||
|
org.keycloak.userprofile.validator.RegistrationEmailAsUsernameEmailValueValidator
|
||||||
|
org.keycloak.userprofile.validator.BrokeringFederatedUsernameHasValueValidator
|
|
@ -54,17 +54,18 @@ import org.keycloak.userprofile.UserProfileContext;
|
||||||
import org.keycloak.userprofile.UserProfileMetadata;
|
import org.keycloak.userprofile.UserProfileMetadata;
|
||||||
import org.keycloak.userprofile.UserProfileProvider;
|
import org.keycloak.userprofile.UserProfileProvider;
|
||||||
import org.keycloak.userprofile.legacy.AbstractUserProfileProvider;
|
import org.keycloak.userprofile.legacy.AbstractUserProfileProvider;
|
||||||
import org.keycloak.userprofile.legacy.Validators;
|
import org.keycloak.userprofile.validator.AttributeRequiredByMetadataValidator;
|
||||||
|
import org.keycloak.validate.AbstractSimpleValidator;
|
||||||
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link UserProfileProvider} loading configuration from the changeable JSON
|
* {@link UserProfileProvider} loading configuration from the changeable JSON file stored in component config. Parsed
|
||||||
* file stored in component config. Parsed configuration is cached.
|
* configuration is cached.
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
* @author Vlastimil Elias <velias@redhat.com>
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
*/
|
*/
|
||||||
public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<DeclarativeUserProfileProvider>
|
public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<DeclarativeUserProfileProvider> implements AmphibianProviderFactory<DeclarativeUserProfileProvider> {
|
||||||
implements AmphibianProviderFactory<DeclarativeUserProfileProvider> {
|
|
||||||
|
|
||||||
public static final String ID = "declarative-userprofile-provider";
|
public static final String ID = "declarative-userprofile-provider";
|
||||||
public static final String UP_PIECES_COUNT_COMPONENT_CONFIG_KEY = "config-pieces-count";
|
public static final String UP_PIECES_COUNT_COMPONENT_CONFIG_KEY = "config-pieces-count";
|
||||||
|
@ -78,8 +79,7 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
// for reflection
|
// for reflection
|
||||||
}
|
}
|
||||||
|
|
||||||
public DeclarativeUserProfileProvider(KeycloakSession session,
|
public DeclarativeUserProfileProvider(KeycloakSession session, Map<UserProfileContext, UserProfileMetadata> metadataRegistry) {
|
||||||
Map<UserProfileContext, UserProfileMetadata> metadataRegistry) {
|
|
||||||
super(session, metadataRegistry);
|
super(session, metadataRegistry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,8 +89,7 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected DeclarativeUserProfileProvider create(KeycloakSession session,
|
protected DeclarativeUserProfileProvider create(KeycloakSession session, Map<UserProfileContext, UserProfileMetadata> metadataRegistry) {
|
||||||
Map<UserProfileContext, UserProfileMetadata> metadataRegistry) {
|
|
||||||
return new DeclarativeUserProfileProvider(session, metadataRegistry);
|
return new DeclarativeUserProfileProvider(session, metadataRegistry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,8 +104,7 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
model.setNote(PARSED_CONFIG_COMPONENT_KEY, metadataMap);
|
model.setNote(PARSED_CONFIG_COMPONENT_KEY, metadataMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadataMap.computeIfAbsent(metadata.getContext(),
|
return metadataMap.computeIfAbsent(metadata.getContext(), (context) -> decorateUserProfileForCache(metadata, model));
|
||||||
(context) -> decorateUserProfileForCache(metadata, model));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -115,22 +113,19 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model)
|
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
|
||||||
throws ComponentValidationException {
|
|
||||||
String upConfigJson = getConfigJsonFromComponentModel(model);
|
String upConfigJson = getConfigJsonFromComponentModel(model);
|
||||||
|
|
||||||
if (!isBlank(upConfigJson)) {
|
if (!isBlank(upConfigJson)) {
|
||||||
try {
|
try {
|
||||||
UPConfig upc = readConfig(new ByteArrayInputStream(upConfigJson.getBytes("UTF-8")));
|
UPConfig upc = readConfig(new ByteArrayInputStream(upConfigJson.getBytes("UTF-8")));
|
||||||
List<String> errors = UPConfigUtils.validate(upc);
|
List<String> errors = UPConfigUtils.validate(session, upc);
|
||||||
|
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
throw new ComponentValidationException(
|
throw new ComponentValidationException("UserProfile configuration is invalid: " + errors.toString());
|
||||||
"UserProfile configuration is invalid: " + errors.toString());
|
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ComponentValidationException(
|
throw new ComponentValidationException("UserProfile configuration is invalid due to JSON parsing error: " + e.getMessage(), e);
|
||||||
"UserProfile configuration is invalid due to JSON parsing error: " + e.getMessage(), e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,7 +172,7 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void postInit(KeycloakSessionFactory factory) {
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
//TODO: We should avoid blocking operations during startup. Need to review this.
|
// TODO: We should avoid blocking operations during startup. Need to review this.
|
||||||
try (InputStream is = getClass().getResourceAsStream(SYSTEM_DEFAULT_CONFIG_RESOURCE)) {
|
try (InputStream is = getClass().getResourceAsStream(SYSTEM_DEFAULT_CONFIG_RESOURCE)) {
|
||||||
defaultRawConfig = StreamUtil.readString(is, Charset.defaultCharset());
|
defaultRawConfig = StreamUtil.readString(is, Charset.defaultCharset());
|
||||||
} catch (IOException cause) {
|
} catch (IOException cause) {
|
||||||
|
@ -195,17 +190,15 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decorate basic metadata provided from {@link AbstractUserProfileProvider}
|
* Decorate basic metadata provided from {@link AbstractUserProfileProvider} based on 'per realm' configuration.
|
||||||
* based on 'per realm' configuration. This method is called for each
|
* This method is called for each {@link UserProfileContext} in each realm, and metadata are cached then and this
|
||||||
* {@link UserProfileContext} in each realm, and metadata are cached then and
|
* method is called again only if configuration changes.
|
||||||
* this method is called again only if configuration changes.
|
|
||||||
*
|
*
|
||||||
* @param metadata base to be decorated based on configuration loaded from
|
* @param metadata base to be decorated based on configuration loaded from component model
|
||||||
* component model
|
|
||||||
* @param model component model to get "per realm" configuration from
|
* @param model component model to get "per realm" configuration from
|
||||||
* @return decorated metadata
|
* @return decorated metadata
|
||||||
*/
|
*/
|
||||||
private UserProfileMetadata decorateUserProfileForCache(UserProfileMetadata metadata, ComponentModel model) {
|
protected UserProfileMetadata decorateUserProfileForCache(UserProfileMetadata metadata, ComponentModel model) {
|
||||||
UserProfileContext context = metadata.getContext();
|
UserProfileContext context = metadata.getContext();
|
||||||
UPConfig parsedConfig = getParsedConfig(model);
|
UPConfig parsedConfig = getParsedConfig(model);
|
||||||
|
|
||||||
|
@ -224,7 +217,7 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
|
|
||||||
if (validationsConfig != null) {
|
if (validationsConfig != null) {
|
||||||
for (Map.Entry<String, Map<String, Object>> vc : validationsConfig.entrySet()) {
|
for (Map.Entry<String, Map<String, Object>> vc : validationsConfig.entrySet()) {
|
||||||
validators.add(createConfiguredValidator(attrConfig, vc.getKey(), vc.getValue()));
|
validators.add(createConfiguredValidator(vc.getKey(), vc.getValue()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,8 +231,7 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
if (rc.isAlways() || UPConfigUtils.isRoleForContext(context, rc.getRoles())) {
|
if (rc.isAlways() || UPConfigUtils.isRoleForContext(context, rc.getRoles())) {
|
||||||
validators.add(createRequiredValidator(attrConfig));
|
validators.add(createRequiredValidator(attrConfig));
|
||||||
required = AttributeMetadata.ALWAYS_TRUE;
|
required = AttributeMetadata.ALWAYS_TRUE;
|
||||||
} else if (UPConfigUtils.canBeAuthFlowContext(context) && rc.getScopes() != null
|
} else if (UPConfigUtils.canBeAuthFlowContext(context) && rc.getScopes() != null && !rc.getScopes().isEmpty()) {
|
||||||
&& !rc.getScopes().isEmpty()) {
|
|
||||||
// for contexts executed from auth flow and with configured scopes requirement
|
// for contexts executed from auth flow and with configured scopes requirement
|
||||||
// we have to create required validation with scopes based selector
|
// we have to create required validation with scopes based selector
|
||||||
required = (c) -> attributePredicateAuthFlowRequestedScope(rc.getScopes());
|
required = (c) -> attributePredicateAuthFlowRequestedScope(rc.getScopes());
|
||||||
|
@ -265,7 +257,8 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
if (!validators.isEmpty()) {
|
if (!validators.isEmpty()) {
|
||||||
List<AttributeMetadata> atts = decoratedMetadata.getAttribute(attributeName);
|
List<AttributeMetadata> atts = decoratedMetadata.getAttribute(attributeName);
|
||||||
if (atts.isEmpty()) {
|
if (atts.isEmpty()) {
|
||||||
// attribute metadata doesn't exist so we have to add it. We keep it optional as Abstract base doesn't require it.
|
// attribute metadata doesn't exist so we have to add it. We keep it optional as Abstract base
|
||||||
|
// doesn't require it.
|
||||||
decoratedMetadata.addAttribute(attributeName, validators, readOnly).addAnnotations(annotations);
|
decoratedMetadata.addAttribute(attributeName, validators, readOnly).addAnnotations(annotations);
|
||||||
} else {
|
} else {
|
||||||
// only add configured validators and annotations if attribute metadata exist
|
// only add configured validators and annotations if attribute metadata exist
|
||||||
|
@ -282,21 +275,27 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get parsed config file configured in model. Default one used if not
|
* Get parsed config file configured in model. Default one used if not configured.
|
||||||
* configured.
|
|
||||||
*
|
*
|
||||||
* @param model to take config from
|
* @param model to take config from
|
||||||
* @return parsed configuration
|
* @return parsed configuration
|
||||||
*/
|
*/
|
||||||
private UPConfig getParsedConfig(ComponentModel model) {
|
protected UPConfig getParsedConfig(ComponentModel model) {
|
||||||
String rawConfig = getConfigJsonFromComponentModel(model);
|
String rawConfig = getConfigJsonFromComponentModel(model);
|
||||||
|
|
||||||
if (!isBlank(rawConfig)) {
|
if (!isBlank(rawConfig)) {
|
||||||
try {
|
try {
|
||||||
return readConfig(new ByteArrayInputStream(rawConfig.getBytes("UTF-8")));
|
UPConfig upc = readConfig(new ByteArrayInputStream(rawConfig.getBytes("UTF-8")));
|
||||||
|
|
||||||
|
//validate configuration to catch things like changed/removed validators etc, and warn early and clearly about this problem
|
||||||
|
List<String> errors = UPConfigUtils.validate(session, upc);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
throw new RuntimeException("UserProfile configuration for realm '" + session.getContext().getRealm().getName() + "' is invalid: " + errors.toString());
|
||||||
|
}
|
||||||
|
return upc;
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException("UserProfile config for realm " + session.getContext().getRealm().getName()
|
throw new RuntimeException("UserProfile configuration for realm '" + session.getContext().getRealm().getName() + "' is invalid:" + e.getMessage(), e);
|
||||||
+ " is invalid:" + e.getMessage(), e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -304,16 +303,14 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Predicate to select attributes for Authentication flow cases where requested
|
* Predicate to select attributes for Authentication flow cases where requested scopes (including configured Default
|
||||||
* scopes (including configured Default client scopes) are compared to set of
|
* client scopes) are compared to set of scopes from user profile configuration.
|
||||||
* scopes from user profile configuration.
|
|
||||||
* <p>
|
* <p>
|
||||||
* This patches problem with some auth flows (eg. register) where
|
* This patches problem with some auth flows (eg. register) where authSession.getClientScopes() doesn't work
|
||||||
* authSession.getClientScopes() doesn't work correctly!
|
* correctly!
|
||||||
*
|
*
|
||||||
* @param scopesConfigured to match
|
* @param scopesConfigured to match
|
||||||
* @return true if at least one requested scope matches at least one configured
|
* @return true if at least one requested scope matches at least one configured scope
|
||||||
* scope
|
|
||||||
*/
|
*/
|
||||||
private boolean attributePredicateAuthFlowRequestedScope(List<String> scopesConfigured) {
|
private boolean attributePredicateAuthFlowRequestedScope(List<String> scopesConfigured) {
|
||||||
// never match out of auth flow
|
// never match out of auth flow
|
||||||
|
@ -325,12 +322,8 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
}
|
}
|
||||||
|
|
||||||
private Set<String> getAuthFlowRequestedScopeNames() {
|
private Set<String> getAuthFlowRequestedScopeNames() {
|
||||||
String requestedScopesString = session.getContext().getAuthenticationSession()
|
String requestedScopesString = session.getContext().getAuthenticationSession().getClientNote(OIDCLoginProtocol.SCOPE_PARAM);
|
||||||
.getClientNote(OIDCLoginProtocol.SCOPE_PARAM);
|
return TokenManager.getRequestedClientScopes(requestedScopesString, session.getContext().getAuthenticationSession().getClient()).map((csm) -> csm.getName()).collect(Collectors.toSet());
|
||||||
return TokenManager
|
|
||||||
.getRequestedClientScopes(requestedScopesString, session.getContext().getAuthenticationSession().getClient())
|
|
||||||
.map((csm) -> csm.getName())
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -341,36 +334,27 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
*/
|
*/
|
||||||
private ComponentModel getComponentModelOrCreate(KeycloakSession session) {
|
private ComponentModel getComponentModelOrCreate(KeycloakSession session) {
|
||||||
RealmModel realm = session.getContext().getRealm();
|
RealmModel realm = session.getContext().getRealm();
|
||||||
return realm.getComponentsStream(realm.getId(), UserProfileProvider.class.getName()).findAny()
|
return realm.getComponentsStream(realm.getId(), UserProfileProvider.class.getName()).findAny().orElseGet(() -> realm.addComponentModel(new DeclarativeUserProfileModel()));
|
||||||
.orElseGet(() -> realm.addComponentModel(new DeclarativeUserProfileModel()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create validator for 'required' validation.
|
* Create validator for 'required' validation.
|
||||||
*
|
*
|
||||||
* @return validator
|
* @return validator metadata to run given validation
|
||||||
*/
|
*/
|
||||||
private AttributeValidatorMetadata createRequiredValidator(UPAttribute attrConfig) {
|
protected AttributeValidatorMetadata createRequiredValidator(UPAttribute attrConfig) {
|
||||||
String msg = "missing" + UPConfigUtils.capitalizeFirstLetter(attrConfig.getName()) + "Message";
|
return new AttributeValidatorMetadata(AttributeRequiredByMetadataValidator.ID);
|
||||||
return Validators.create(msg, Validators.requiredByAttributeMetadata());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create validator for validation configured in the user profile config.
|
* Create validator for validation configured in the user profile config.
|
||||||
*
|
*
|
||||||
* @param attrConfig to create validator for
|
* @param validator id to create validator for
|
||||||
* @return validator
|
* @param validatorConfig of the validator
|
||||||
|
* @return validator metadata to run given validation
|
||||||
*/
|
*/
|
||||||
private AttributeValidatorMetadata createConfiguredValidator(UPAttribute attrConfig,
|
protected AttributeValidatorMetadata createConfiguredValidator(String validator, Map<String, Object> validatorConfig) {
|
||||||
String validator, Map<String, Object> validatorConfig) {
|
return new AttributeValidatorMetadata(validator, ValidatorConfig.builder().config(validatorConfig).config(AbstractSimpleValidator.IGNORE_EMPTY_VALUE, true).build());
|
||||||
// TODO UserProfile - integrate Validation SPI
|
|
||||||
if ("length".equals(validator))
|
|
||||||
return Validators.create("badLenght" + UPConfigUtils.capitalizeFirstLetter(attrConfig.getName()) + "Message",
|
|
||||||
Validators.length(validatorConfig));
|
|
||||||
else if ("emailFormat".equals(validator))
|
|
||||||
return Validators.create("invalidEmailMessage", Validators.isEmailValid());
|
|
||||||
else
|
|
||||||
throw new RuntimeException("Unsupported UserProfile validator " + validator);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getConfigJsonFromComponentModel(ComponentModel model) {
|
private String getConfigJsonFromComponentModel(ComponentModel model) {
|
||||||
|
|
|
@ -27,8 +27,12 @@ import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.userprofile.UserProfileContext;
|
import org.keycloak.userprofile.UserProfileContext;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
import org.keycloak.validate.ValidationResult;
|
||||||
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
import org.keycloak.validate.Validators;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility methods to work with User Profile Configurations
|
* Utility methods to work with User Profile Configurations
|
||||||
|
@ -71,15 +75,16 @@ public class UPConfigUtils {
|
||||||
* <li>validator (from Validator SPI) exists for validation and it's config is correct
|
* <li>validator (from Validator SPI) exists for validation and it's config is correct
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
|
* @param session to be used for Validator SPI integration
|
||||||
* @param config to validate
|
* @param config to validate
|
||||||
* @return list of errors, empty if no error found
|
* @return list of errors, empty if no error found
|
||||||
*/
|
*/
|
||||||
public static List<String> validate(UPConfig config) {
|
public static List<String> validate(KeycloakSession session, UPConfig config) {
|
||||||
List<String> errors = new ArrayList<>();
|
List<String> errors = new ArrayList<>();
|
||||||
|
|
||||||
if (config.getAttributes() != null) {
|
if (config.getAttributes() != null) {
|
||||||
Set<String> attNamesCache = new HashSet<>();
|
Set<String> attNamesCache = new HashSet<>();
|
||||||
config.getAttributes().forEach((attribute) -> validate(attribute, errors, attNamesCache));
|
config.getAttributes().forEach((attribute) -> validate(session, attribute, errors, attNamesCache));
|
||||||
} else {
|
} else {
|
||||||
errors.add("UserProfile configuration without 'attributes' section is not allowed");
|
errors.add("UserProfile configuration without 'attributes' section is not allowed");
|
||||||
}
|
}
|
||||||
|
@ -90,10 +95,12 @@ public class UPConfigUtils {
|
||||||
/**
|
/**
|
||||||
* Validate attribute configuration
|
* Validate attribute configuration
|
||||||
*
|
*
|
||||||
|
* @param session to be used for Validator SPI integration
|
||||||
* @param attributeConfig config to be validated
|
* @param attributeConfig config to be validated
|
||||||
* @param errors to add error message in if something is invalid
|
* @param errors to add error message in if something is invalid
|
||||||
|
* @param attNamesCache cache of already existing attribute names so we can check uniqueness
|
||||||
*/
|
*/
|
||||||
private static void validate(UPAttribute attributeConfig, List<String> errors, Set<String> attNamesCache) {
|
private static void validate(KeycloakSession session, UPAttribute attributeConfig, List<String> errors, Set<String> attNamesCache) {
|
||||||
String attributeName = attributeConfig.getName();
|
String attributeName = attributeConfig.getName();
|
||||||
if (isBlank(attributeName)) {
|
if (isBlank(attributeName)) {
|
||||||
errors.add("Attribute configuration without 'name' is not allowed");
|
errors.add("Attribute configuration without 'name' is not allowed");
|
||||||
|
@ -108,7 +115,7 @@ public class UPConfigUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (attributeConfig.getValidations() != null) {
|
if (attributeConfig.getValidations() != null) {
|
||||||
attributeConfig.getValidations().forEach((validator, validatorConfig) -> validateValidationConfig(validator, validatorConfig, attributeName, errors));
|
attributeConfig.getValidations().forEach((validator, validatorConfig) -> validateValidationConfig(session, validator, validatorConfig, attributeName, errors));
|
||||||
}
|
}
|
||||||
if (attributeConfig.getPermissions() != null) {
|
if (attributeConfig.getPermissions() != null) {
|
||||||
if (attributeConfig.getPermissions().getView() != null) {
|
if (attributeConfig.getPermissions().getView() != null) {
|
||||||
|
@ -150,23 +157,34 @@ public class UPConfigUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate that validation configuration is correct
|
* Validate that validation configuration is correct.
|
||||||
*
|
*
|
||||||
|
* @param session to be used for Validator SPI integration
|
||||||
* @param validatorConfig config to be checked
|
* @param validatorConfig config to be checked
|
||||||
* @param errors to add error message in if something is invalid
|
* @param errors to add error message in if something is invalid
|
||||||
*/
|
*/
|
||||||
private static void validateValidationConfig(String validator, Map<String, Object> validatorConfig, String attributeName, List<String> errors) {
|
private static void validateValidationConfig(KeycloakSession session, String validator, Map<String, Object> validatorConfig, String attributeName, List<String> errors) {
|
||||||
|
|
||||||
if (isBlank(validator)) {
|
if (isBlank(validator)) {
|
||||||
errors.add("Validation without 'validator' is defined for attribute '" + attributeName + "'");
|
errors.add("Validation without validator id is defined for attribute '" + attributeName + "'");
|
||||||
} else {
|
} else {
|
||||||
// TODO UserProfile - Validation SPI integration - check that the validator exists using Validation SPI
|
if(session!=null) {
|
||||||
// TODO UserProfile - Validation SPI integration - check that the validation configuration is correct for given validator using Validation SPI
|
if(Validators.validator(session, validator) == null) {
|
||||||
|
errors.add("Validator '" + validator + "' defined for attribute '" + attributeName + "' doesn't exist");
|
||||||
|
} else {
|
||||||
|
ValidationResult result = Validators.validateConfig(session, validator, ValidatorConfig.configFromMap(validatorConfig));
|
||||||
|
if(!result.isValid()) {
|
||||||
|
final StringBuilder sb = new StringBuilder();
|
||||||
|
result.forEachError(err -> sb.append(err.toString()+", "));
|
||||||
|
errors.add("Validator '" + validator + "' defined for attribute '" + attributeName + "' has incorrect configuration: " + sb.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Break string to substrings of given length
|
* Break string to substrings of given length.
|
||||||
*
|
*
|
||||||
* @param src to break
|
* @param src to break
|
||||||
* @param partLength
|
* @param partLength
|
||||||
|
|
|
@ -49,6 +49,7 @@ import org.keycloak.userprofile.UserProfile;
|
||||||
import org.keycloak.userprofile.UserProfileContext;
|
import org.keycloak.userprofile.UserProfileContext;
|
||||||
import org.keycloak.userprofile.ValidationException;
|
import org.keycloak.userprofile.ValidationException;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
import org.keycloak.validate.validators.LengthValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
@ -204,7 +205,7 @@ public class UserProfileConfigTest extends AbstractUserProfileTest {
|
||||||
|
|
||||||
validatorConfig.put("min", 4);
|
validatorConfig.put("min", 4);
|
||||||
|
|
||||||
attribute.addValidation("length", validatorConfig);
|
attribute.addValidation(LengthValidator.ID, validatorConfig);
|
||||||
|
|
||||||
config.addAttribute(attribute);
|
config.addAttribute(attribute);
|
||||||
|
|
||||||
|
@ -221,7 +222,7 @@ public class UserProfileConfigTest extends AbstractUserProfileTest {
|
||||||
fail("Should fail validation");
|
fail("Should fail validation");
|
||||||
} catch (ValidationException ve) {
|
} catch (ValidationException ve) {
|
||||||
assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
|
assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
|
||||||
assertTrue(ve.hasError("badLenghtUsernameMessage"));
|
assertTrue(ve.hasError(LengthValidator.MESSAGE_INVALID_LENGTH));
|
||||||
}
|
}
|
||||||
|
|
||||||
attributes.put(UserModel.USERNAME, "user");
|
attributes.put(UserModel.USERNAME, "user");
|
||||||
|
@ -256,13 +257,13 @@ public class UserProfileConfigTest extends AbstractUserProfileTest {
|
||||||
UPAttribute attribute = new UPAttribute();
|
UPAttribute attribute = new UPAttribute();
|
||||||
attribute.setName(UserModel.FIRST_NAME);
|
attribute.setName(UserModel.FIRST_NAME);
|
||||||
Map<String, Object> validatorConfig = new HashMap<>();
|
Map<String, Object> validatorConfig = new HashMap<>();
|
||||||
validatorConfig.put("max", 4);
|
validatorConfig.put(LengthValidator.KEY_MAX, 4);
|
||||||
attribute.addValidation("length", validatorConfig);
|
attribute.addValidation(LengthValidator.ID, validatorConfig);
|
||||||
config.addAttribute(attribute);
|
config.addAttribute(attribute);
|
||||||
|
|
||||||
attribute = new UPAttribute();
|
attribute = new UPAttribute();
|
||||||
attribute.setName(UserModel.LAST_NAME);
|
attribute.setName(UserModel.LAST_NAME);
|
||||||
attribute.addValidation("length", validatorConfig);
|
attribute.addValidation(LengthValidator.ID, validatorConfig);
|
||||||
config.addAttribute(attribute);
|
config.addAttribute(attribute);
|
||||||
|
|
||||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||||
|
@ -301,11 +302,11 @@ public class UserProfileConfigTest extends AbstractUserProfileTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCustomAttribute() {
|
public void testCustomAttribute_Required() {
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testCustomAttribute);
|
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testCustomAttribute_Required);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void testCustomAttribute(KeycloakSession session) throws IOException {
|
private static void testCustomAttribute_Required(KeycloakSession session) throws IOException {
|
||||||
configureSessionRealm(session);
|
configureSessionRealm(session);
|
||||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||||
ComponentModel component = provider.getComponentModel();
|
ComponentModel component = provider.getComponentModel();
|
||||||
|
@ -319,9 +320,9 @@ public class UserProfileConfigTest extends AbstractUserProfileTest {
|
||||||
|
|
||||||
Map<String, Object> validatorConfig = new HashMap<>();
|
Map<String, Object> validatorConfig = new HashMap<>();
|
||||||
|
|
||||||
validatorConfig.put("min", 4);
|
validatorConfig.put(LengthValidator.KEY_MIN, 4);
|
||||||
|
|
||||||
attribute.addValidation("length", validatorConfig);
|
attribute.addValidation(LengthValidator.ID, validatorConfig);
|
||||||
|
|
||||||
// make it ALWAYS required
|
// make it ALWAYS required
|
||||||
UPAttributeRequired requirements = new UPAttributeRequired();
|
UPAttributeRequired requirements = new UPAttributeRequired();
|
||||||
|
@ -355,6 +356,59 @@ public class UserProfileConfigTest extends AbstractUserProfileTest {
|
||||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// all OK
|
||||||
|
attributes.put(ATT_ADDRESS, "adress ok");
|
||||||
|
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
profile.validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCustomAttribute_Optional() {
|
||||||
|
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testCustomAttribute_Optional);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testCustomAttribute_Optional(KeycloakSession session) throws IOException {
|
||||||
|
configureSessionRealm(session);
|
||||||
|
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||||
|
ComponentModel component = provider.getComponentModel();
|
||||||
|
|
||||||
|
assertNotNull(component);
|
||||||
|
|
||||||
|
UPConfig config = new UPConfig();
|
||||||
|
UPAttribute attribute = new UPAttribute();
|
||||||
|
|
||||||
|
attribute.setName(ATT_ADDRESS);
|
||||||
|
|
||||||
|
Map<String, Object> validatorConfig = new HashMap<>();
|
||||||
|
validatorConfig.put(LengthValidator.KEY_MIN, 4);
|
||||||
|
attribute.addValidation(LengthValidator.ID, validatorConfig);
|
||||||
|
|
||||||
|
config.addAttribute(attribute);
|
||||||
|
|
||||||
|
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||||
|
|
||||||
|
Map<String, Object> attributes = new HashMap<>();
|
||||||
|
attributes.put(UserModel.USERNAME, "user");
|
||||||
|
|
||||||
|
// null is OK as attribute is optional
|
||||||
|
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
profile.validate();
|
||||||
|
|
||||||
|
//blank String have to be OK as it is what UI forms send for not filled in optional attributes
|
||||||
|
attributes.put(ATT_ADDRESS, "");
|
||||||
|
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
profile.validate();
|
||||||
|
|
||||||
|
// fails on length validation
|
||||||
|
attributes.put(ATT_ADDRESS, "adr");
|
||||||
|
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
try {
|
||||||
|
profile.validate();
|
||||||
|
fail("Should fail validation");
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||||
|
}
|
||||||
|
|
||||||
// all OK
|
// all OK
|
||||||
attributes.put(ATT_ADDRESS, "adress ok");
|
attributes.put(ATT_ADDRESS, "adress ok");
|
||||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
|
|
@ -41,16 +41,12 @@ import java.util.function.Consumer;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.models.ClientModel;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
|
||||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
|
||||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
|
||||||
import org.keycloak.testsuite.runonserver.RunOnServer;
|
import org.keycloak.testsuite.runonserver.RunOnServer;
|
||||||
import org.keycloak.testsuite.user.profile.config.UPAttribute;
|
import org.keycloak.testsuite.user.profile.config.UPAttribute;
|
||||||
import org.keycloak.testsuite.user.profile.config.UPAttributeRequired;
|
import org.keycloak.testsuite.user.profile.config.UPAttributeRequired;
|
||||||
|
@ -63,6 +59,8 @@ import org.keycloak.userprofile.UserProfileContext;
|
||||||
import org.keycloak.userprofile.UserProfileProvider;
|
import org.keycloak.userprofile.UserProfileProvider;
|
||||||
import org.keycloak.userprofile.ValidationException;
|
import org.keycloak.userprofile.ValidationException;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
import org.keycloak.validate.ValidationError;
|
||||||
|
import org.keycloak.validate.validators.EmailValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
@ -229,19 +227,29 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||||
|
|
||||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
List<String> errors = new ArrayList<>();
|
List<ValidationError> errors = new ArrayList<>();
|
||||||
|
|
||||||
assertFalse(profile.getAttributes().validate(UserModel.USERNAME, (Consumer<String>) errors::add));
|
assertFalse(profile.getAttributes().validate(UserModel.USERNAME, (Consumer<ValidationError>) errors::add));
|
||||||
assertTrue(errors.contains(Messages.MISSING_USERNAME));
|
assertTrue(containsErrorMessage(errors, Messages.MISSING_USERNAME));
|
||||||
|
|
||||||
errors.clear();
|
errors.clear();
|
||||||
attributes.clear();
|
attributes.clear();
|
||||||
attributes.put(UserModel.EMAIL, "invalid");
|
attributes.put(UserModel.EMAIL, "invalid");
|
||||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
assertFalse(profile.getAttributes().validate(UserModel.EMAIL, (Consumer<String>) errors::add));
|
assertFalse(profile.getAttributes().validate(UserModel.EMAIL, (Consumer<ValidationError>) errors::add));
|
||||||
assertTrue(errors.contains(Messages.INVALID_EMAIL));
|
assertTrue(containsErrorMessage(errors, EmailValidator.MESSAGE_INVALID_EMAIL));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean containsErrorMessage(List<ValidationError> errors, String message){
|
||||||
|
for(ValidationError err : errors) {
|
||||||
|
if(err.getMessage().equals(message)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testValidateComplianceWithUserProfile() {
|
public void testValidateComplianceWithUserProfile() {
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testValidateComplianceWithUserProfile);
|
getTestingClient().server().run((RunOnServer) UserProfileTest::testValidateComplianceWithUserProfile);
|
||||||
|
|
|
@ -23,11 +23,16 @@ import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||||
|
import org.keycloak.testsuite.runonserver.RunOnServer;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||||
|
|
||||||
|
@ -37,7 +42,11 @@ import com.fasterxml.jackson.databind.JsonMappingException;
|
||||||
* @author Vlastimil Elias <velias@redhat.com>
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public class UPConfigParserTest {
|
public class UPConfigParserTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void attributeNameIsValid() {
|
public void attributeNameIsValid() {
|
||||||
|
@ -111,12 +120,12 @@ public class UPConfigParserTest {
|
||||||
* @return valid config
|
* @return valid config
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
private UPConfig loadValidConfig() throws IOException {
|
private static UPConfig loadValidConfig() throws IOException {
|
||||||
return readConfig(getValidConfigFileIS());
|
return readConfig(getValidConfigFileIS());
|
||||||
}
|
}
|
||||||
|
|
||||||
private InputStream getValidConfigFileIS() {
|
private static InputStream getValidConfigFileIS() {
|
||||||
return getClass().getResourceAsStream("test-OK.json");
|
return UPConfigParserTest.class.getResourceAsStream("test-OK.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = JsonMappingException.class)
|
@Test(expected = JsonMappingException.class)
|
||||||
|
@ -136,57 +145,59 @@ public class UPConfigParserTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void validateConfiguration_OK() throws IOException {
|
public void validateConfiguration_OK() throws IOException {
|
||||||
List<String> errors = validate(loadValidConfig());
|
List<String> errors = validate(null, loadValidConfig());
|
||||||
Assert.assertTrue(errors.isEmpty());
|
Assert.assertTrue(errors.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void validateConfiguration_attributeNameErrors() throws IOException {
|
public void validateConfiguration_attributeNameErrors() throws IOException {
|
||||||
UPConfig config = loadValidConfig();
|
UPConfig config = loadValidConfig();
|
||||||
|
//we run this test without KeycloakSession so validator configs are not validated here
|
||||||
|
|
||||||
UPAttribute attConfig = config.getAttributes().get(1);
|
UPAttribute attConfig = config.getAttributes().get(1);
|
||||||
|
|
||||||
attConfig.setName(null);
|
attConfig.setName(null);
|
||||||
List<String> errors = validate(config);
|
List<String> errors = validate(null, config);
|
||||||
Assert.assertEquals(1, errors.size());
|
Assert.assertEquals(1, errors.size());
|
||||||
|
|
||||||
attConfig.setName(" ");
|
attConfig.setName(" ");
|
||||||
errors = validate(config);
|
errors = validate(null, config);
|
||||||
Assert.assertEquals(1, errors.size());
|
Assert.assertEquals(1, errors.size());
|
||||||
|
|
||||||
// duplicate attribute name
|
// duplicate attribute name
|
||||||
attConfig.setName("firstName");
|
attConfig.setName("firstName");
|
||||||
errors = validate(config);
|
errors = validate(null, config);
|
||||||
Assert.assertEquals(1, errors.size());
|
Assert.assertEquals(1, errors.size());
|
||||||
|
|
||||||
// attribute name format error - unallowed character
|
// attribute name format error - unallowed character
|
||||||
attConfig.setName("ema il");
|
attConfig.setName("ema il");
|
||||||
errors = validate(config);
|
errors = validate(null, config);
|
||||||
Assert.assertEquals(1, errors.size());
|
Assert.assertEquals(1, errors.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void validateConfiguration_attributePermissionsErrors() throws IOException {
|
public void validateConfiguration_attributePermissionsErrors() throws IOException {
|
||||||
UPConfig config = loadValidConfig();
|
UPConfig config = loadValidConfig();
|
||||||
|
//we run this test without KeycloakSession so validator configs are not validated here
|
||||||
|
|
||||||
UPAttribute attConfig = config.getAttributes().get(1);
|
UPAttribute attConfig = config.getAttributes().get(1);
|
||||||
|
|
||||||
// no permissions configures at all
|
// no permissions configures at all
|
||||||
attConfig.setPermissions(null);
|
attConfig.setPermissions(null);
|
||||||
List<String> errors = validate(config);
|
List<String> errors = validate(null, config);
|
||||||
Assert.assertEquals(0, errors.size());
|
Assert.assertEquals(0, errors.size());
|
||||||
|
|
||||||
// no permissions structure fields configured
|
// no permissions structure fields configured
|
||||||
UPAttributePermissions permsConfig = new UPAttributePermissions();
|
UPAttributePermissions permsConfig = new UPAttributePermissions();
|
||||||
attConfig.setPermissions(permsConfig);
|
attConfig.setPermissions(permsConfig);
|
||||||
errors = validate(config);
|
errors = validate(null, config);
|
||||||
Assert.assertTrue(errors.isEmpty());
|
Assert.assertTrue(errors.isEmpty());
|
||||||
|
|
||||||
// valid if both are present, even empty
|
// valid if both are present, even empty
|
||||||
permsConfig.setEdit(Collections.emptyList());
|
permsConfig.setEdit(Collections.emptyList());
|
||||||
permsConfig.setView(Collections.emptyList());
|
permsConfig.setView(Collections.emptyList());
|
||||||
attConfig.setPermissions(permsConfig);
|
attConfig.setPermissions(permsConfig);
|
||||||
errors = validate(config);
|
errors = validate(null, config);
|
||||||
Assert.assertEquals(0, errors.size());
|
Assert.assertEquals(0, errors.size());
|
||||||
|
|
||||||
List<String> withInvRole = new ArrayList<>();
|
List<String> withInvRole = new ArrayList<>();
|
||||||
|
@ -194,30 +205,31 @@ public class UPConfigParserTest {
|
||||||
|
|
||||||
// invalid role used for view
|
// invalid role used for view
|
||||||
permsConfig.setView(withInvRole);
|
permsConfig.setView(withInvRole);
|
||||||
errors = validate(config);
|
errors = validate(null, config);
|
||||||
Assert.assertEquals(1, errors.size());
|
Assert.assertEquals(1, errors.size());
|
||||||
|
|
||||||
// invalid role used for edit also
|
// invalid role used for edit also
|
||||||
permsConfig.setEdit(withInvRole);
|
permsConfig.setEdit(withInvRole);
|
||||||
errors = validate(config);
|
errors = validate(null, config);
|
||||||
Assert.assertEquals(2, errors.size());
|
Assert.assertEquals(2, errors.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void validateConfiguration_attributeRequirementsErrors() throws IOException {
|
public void validateConfiguration_attributeRequirementsErrors() throws IOException {
|
||||||
UPConfig config = loadValidConfig();
|
UPConfig config = loadValidConfig();
|
||||||
|
//we run this test without KeycloakSession so validator configs are not validated here
|
||||||
|
|
||||||
UPAttribute attConfig = config.getAttributes().get(1);
|
UPAttribute attConfig = config.getAttributes().get(1);
|
||||||
|
|
||||||
// it is OK without requirements configures at all
|
// it is OK without requirements configures at all
|
||||||
attConfig.setRequired(null);
|
attConfig.setRequired(null);
|
||||||
List<String> errors = validate(config);
|
List<String> errors = validate(null, config);
|
||||||
Assert.assertEquals(0, errors.size());
|
Assert.assertEquals(0, errors.size());
|
||||||
|
|
||||||
// it is OK with empty config as it means ALWAYS required
|
// it is OK with empty config as it means ALWAYS required
|
||||||
UPAttributeRequired reqConfig = new UPAttributeRequired();
|
UPAttributeRequired reqConfig = new UPAttributeRequired();
|
||||||
attConfig.setRequired(reqConfig);
|
attConfig.setRequired(reqConfig);
|
||||||
errors = validate(config);
|
errors = validate(null, config);
|
||||||
Assert.assertEquals(0, errors.size());
|
Assert.assertEquals(0, errors.size());
|
||||||
Assert.assertTrue(reqConfig.isAlways());
|
Assert.assertTrue(reqConfig.isAlways());
|
||||||
|
|
||||||
|
@ -226,25 +238,45 @@ public class UPConfigParserTest {
|
||||||
|
|
||||||
// invalid role used
|
// invalid role used
|
||||||
reqConfig.setRoles(withInvRole);;
|
reqConfig.setRoles(withInvRole);;
|
||||||
errors = validate(config);
|
errors = validate(null, config);
|
||||||
Assert.assertEquals(1, errors.size());
|
Assert.assertEquals(1, errors.size());
|
||||||
Assert.assertFalse(reqConfig.isAlways());
|
Assert.assertFalse(reqConfig.isAlways());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void validateConfiguration_attributeValidationsErrors() throws IOException {
|
public void validateConfiguration_attributeValidationsErrors() {
|
||||||
|
getTestingClient().server().run((RunOnServer) UPConfigParserTest::validateConfiguration_attributeValidationsErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateConfiguration_attributeValidationsErrors(KeycloakSession session) throws IOException {
|
||||||
UPConfig config = loadValidConfig();
|
UPConfig config = loadValidConfig();
|
||||||
|
|
||||||
Map<String, Map<String, Object>> validationConfig = config.getAttributes().get(1).getValidations();
|
//reset all validations not to affect our test as they may be invalid
|
||||||
|
for(UPAttribute att: config.getAttributes()) {
|
||||||
|
att.setValidations(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
//add validation config for one attribute for testing purposes
|
||||||
|
Map<String, Map<String, Object>> validationConfig = new HashMap<>();
|
||||||
|
config.getAttributes().get(1).setValidations(validationConfig);
|
||||||
|
|
||||||
|
// empty validator name
|
||||||
validationConfig.put(" ",null);
|
validationConfig.put(" ",null);
|
||||||
List<String> errors = validate(config);
|
List<String> errors = validate(session, config);
|
||||||
Assert.assertEquals(1, errors.size());
|
Assert.assertEquals(1, errors.size());
|
||||||
|
|
||||||
// TODO Validation SPI integration - test validation of the validator existence and validator config
|
|
||||||
// validationConfig.setValidator("unknownValidator");
|
// wrong configuration for "length" validator
|
||||||
// errors = UPConfigUtils.validateConfiguration(config);
|
validationConfig.clear();
|
||||||
// Assert.assertEquals(1, errors.size());
|
Map<String, Object> vc = new HashMap<>();
|
||||||
|
vc.put("min", "aaa");
|
||||||
|
validationConfig.put("length", vc );
|
||||||
|
errors = validate(session, config);
|
||||||
|
Assert.assertEquals(1, errors.size());
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -218,7 +218,7 @@ error-pattern-no-match=Invalid value.
|
||||||
error-invalid-uri=Invalid URL.
|
error-invalid-uri=Invalid URL.
|
||||||
error-invalid-uri-scheme=Invalid URL scheme.
|
error-invalid-uri-scheme=Invalid URL scheme.
|
||||||
error-invalid-uri-fragment=Invalid URL fragment.
|
error-invalid-uri-fragment=Invalid URL fragment.
|
||||||
|
error-user-attribute-required=Please specify this field.
|
||||||
|
|
||||||
invalidPasswordExistingMessage=Invalid existing password.
|
invalidPasswordExistingMessage=Invalid existing password.
|
||||||
invalidPasswordBlacklistedMessage=Invalid password: password is blacklisted.
|
invalidPasswordBlacklistedMessage=Invalid password: password is blacklisted.
|
||||||
|
|
Loading…
Reference in a new issue