KEYCLOAK-7724 User Profile default validations

This commit is contained in:
Vlastimil Elias 2021-07-19 14:47:28 +02:00 committed by Stian Thorgersen
parent 4dacbb9e0b
commit 32f2f095fe
20 changed files with 545 additions and 49 deletions

View file

@ -41,6 +41,8 @@ public abstract class AbstractNumberValidator extends AbstractSimpleValidator im
public static final String MESSAGE_INVALID_NUMBER = "error-invalid-number";
public static final String MESSAGE_NUMBER_OUT_OF_RANGE = "error-number-out-of-range";
public static final String MESSAGE_NUMBER_OUT_OF_RANGE_TOO_SMALL = "error-number-out-of-range-too-small";
public static final String MESSAGE_NUMBER_OUT_OF_RANGE_TOO_BIG = "error-number-out-of-range-too-big";
public static final String KEY_MIN = "min";
public static final String KEY_MAX = "max";
@ -111,18 +113,31 @@ public abstract class AbstractNumberValidator extends AbstractSimpleValidator im
Number max = getMinMaxConfig(config, KEY_MAX);
if (min != null && isFirstGreaterThanToSecond(min, number)) {
context.addError(new ValidationError(getId(), inputHint, MESSAGE_NUMBER_OUT_OF_RANGE, min, max));
context.addError(new ValidationError(getId(), inputHint, selectRangeErrorMessage(config), min, max));
return;
}
if (max != null && isFirstGreaterThanToSecond(number, max)) {
context.addError(new ValidationError(getId(), inputHint, MESSAGE_NUMBER_OUT_OF_RANGE, min, max));
context.addError(new ValidationError(getId(), inputHint, selectRangeErrorMessage(config), min, max));
return;
}
return;
}
/**
* Select error message depending on the allowed range interval bound configuration.
*/
protected String selectRangeErrorMessage(ValidatorConfig config) {
if (!config.containsKey(KEY_MAX)) {
return MESSAGE_NUMBER_OUT_OF_RANGE_TOO_SMALL;
} else if (!config.containsKey(KEY_MIN)) {
return MESSAGE_NUMBER_OUT_OF_RANGE_TOO_BIG;
} else {
return MESSAGE_NUMBER_OUT_OF_RANGE;
}
}
@Override
public ValidationResult validateConfig(KeycloakSession session, ValidatorConfig config) {
Set<ValidationError> errors = new LinkedHashSet<>();

View file

@ -45,6 +45,8 @@ public class LengthValidator extends AbstractStringValidator implements Configur
public static final String ID = "length";
public static final String MESSAGE_INVALID_LENGTH = "error-invalid-length";
public static final String MESSAGE_INVALID_LENGTH_TOO_SHORT = "error-invalid-length-too-short";
public static final String MESSAGE_INVALID_LENGTH_TOO_LONG = "error-invalid-length-too-long";
public static final String KEY_MIN = "min";
public static final String KEY_MAX = "max";
@ -85,17 +87,30 @@ public class LengthValidator extends AbstractStringValidator implements Configur
int length = value.length();
if (config.containsKey(KEY_MIN) && length < min.intValue()) {
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_LENGTH, min, max));
context.addError(new ValidationError(ID, inputHint, selectErrorMessage(config), min, max));
return;
}
if (config.containsKey(KEY_MAX) && length > max.intValue()) {
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_LENGTH, min, max));
context.addError(new ValidationError(ID, inputHint, selectErrorMessage(config), min, max));
return;
}
}
/**
* Select error message depending on the allowed length interval bound configuration.
*/
protected String selectErrorMessage(ValidatorConfig config) {
if (!config.containsKey(KEY_MAX)) {
return MESSAGE_INVALID_LENGTH_TOO_SHORT;
} else if (!config.containsKey(KEY_MIN)) {
return MESSAGE_INVALID_LENGTH_TOO_LONG;
} else {
return MESSAGE_INVALID_LENGTH;
}
}
@Override
public ValidationResult validateConfig(KeycloakSession session, ValidatorConfig config) {

View file

@ -42,20 +42,29 @@ public class PatternValidator extends AbstractStringValidator implements Configu
public static final PatternValidator INSTANCE = new PatternValidator();
public static final String KEY_PATTERN = "pattern";
public static final String CFG_PATTERN = "pattern";
public static final String MESSAGE_NO_MATCH = "error-pattern-no-match";
public static final String CFG_ERROR_MESSAGE = "error-message";
private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
static {
ProviderConfigProperty property;
property = new ProviderConfigProperty();
property.setName(KEY_PATTERN);
property.setName(CFG_PATTERN);
property.setLabel("RegExp pattern");
property.setHelpText("RegExp pattern the value must match. Java Pattern syntax is used.");
property.setType(ProviderConfigProperty.STRING_TYPE);
configProperties.add(property);
property = new ProviderConfigProperty();
property.setName(CFG_ERROR_MESSAGE);
property.setLabel("Error message key");
property.setHelpText("Key of the error message in i18n bundle. Dafault message key is " + MESSAGE_NO_MATCH);
property.setType(ProviderConfigProperty.STRING_TYPE);
configProperties.add(property);
}
@Override
@ -65,10 +74,10 @@ public class PatternValidator extends AbstractStringValidator implements Configu
@Override
protected void doValidate(String value, String inputHint, ValidationContext context, ValidatorConfig config) {
Pattern pattern = config.getPattern(KEY_PATTERN);
Pattern pattern = config.getPattern(CFG_PATTERN);
if (!pattern.matcher(value).matches()) {
context.addError(new ValidationError(ID, inputHint, MESSAGE_NO_MATCH, config.getString(KEY_PATTERN)));
context.addError(new ValidationError(ID, inputHint, config.getStringOrDefault(CFG_ERROR_MESSAGE, MESSAGE_NO_MATCH), config.getString(CFG_PATTERN)));
}
}
@ -76,17 +85,17 @@ public class PatternValidator extends AbstractStringValidator implements Configu
public ValidationResult validateConfig(KeycloakSession session, ValidatorConfig config) {
Set<ValidationError> errors = new LinkedHashSet<>();
if (config == null || config == ValidatorConfig.EMPTY || !config.containsKey(KEY_PATTERN)) {
errors.add(new ValidationError(ID, KEY_PATTERN, ValidatorConfigValidator.MESSAGE_CONFIG_MISSING_VALUE));
if (config == null || config == ValidatorConfig.EMPTY || !config.containsKey(CFG_PATTERN)) {
errors.add(new ValidationError(ID, CFG_PATTERN, ValidatorConfigValidator.MESSAGE_CONFIG_MISSING_VALUE));
} else {
Object maybePattern = config.get(KEY_PATTERN);
Object maybePattern = config.get(CFG_PATTERN);
try {
Pattern pattern = config.getPattern(KEY_PATTERN);
Pattern pattern = config.getPattern(CFG_PATTERN);
if (pattern == null) {
errors.add(new ValidationError(ID, KEY_PATTERN, ValidatorConfigValidator.MESSAGE_CONFIG_INVALID_VALUE, maybePattern));
errors.add(new ValidationError(ID, CFG_PATTERN, ValidatorConfigValidator.MESSAGE_CONFIG_INVALID_VALUE, maybePattern));
}
} catch (PatternSyntaxException pse) {
errors.add(new ValidationError(ID, KEY_PATTERN, ValidatorConfigValidator.MESSAGE_CONFIG_INVALID_VALUE, maybePattern));
errors.add(new ValidationError(ID, CFG_PATTERN, ValidatorConfigValidator.MESSAGE_CONFIG_INVALID_VALUE, maybePattern, pse.getMessage()));
}
}
return new ValidationResult(errors);

View file

@ -60,6 +60,12 @@ public class BuiltinValidatorsTest {
// test value trimming disabled in config
Assert.assertTrue("trim disabled but performed", validator.validate("t ", "name", configFromMap(ImmutableMap.of(LengthValidator.KEY_MIN, 2, LengthValidator.KEY_TRIM_DISABLED, true))).isValid());
//test correct error message selection
Assert.assertEquals(LengthValidator.MESSAGE_INVALID_LENGTH_TOO_SHORT,validator.validate("", "name", ValidatorConfig.builder().config(LengthValidator.KEY_MIN, 1).build()).getErrors().iterator().next().getMessage());
Assert.assertEquals(LengthValidator.MESSAGE_INVALID_LENGTH,validator.validate("", "name", ValidatorConfig.builder().config(LengthValidator.KEY_MIN, 1).config(LengthValidator.KEY_MAX, 10).build()).getErrors().iterator().next().getMessage());
Assert.assertEquals(LengthValidator.MESSAGE_INVALID_LENGTH_TOO_LONG,validator.validate("aaa", "name", ValidatorConfig.builder().config(LengthValidator.KEY_MAX, 1).build()).getErrors().iterator().next().getMessage());
Assert.assertEquals(LengthValidator.MESSAGE_INVALID_LENGTH,validator.validate("aaa", "name", ValidatorConfig.builder().config(LengthValidator.KEY_MIN, 1).config(LengthValidator.KEY_MAX, 2).build()).getErrors().iterator().next().getMessage());
}
@Test
@ -243,6 +249,11 @@ public class BuiltinValidatorsTest {
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 correct error message selection
Assert.assertEquals(DoubleValidator.MESSAGE_NUMBER_OUT_OF_RANGE_TOO_SMALL,validator.validate("10", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 100).build()).getErrors().iterator().next().getMessage());
Assert.assertEquals(DoubleValidator.MESSAGE_NUMBER_OUT_OF_RANGE,validator.validate("10", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 100).config(DoubleValidator.KEY_MAX, 1000).build()).getErrors().iterator().next().getMessage());
Assert.assertEquals(DoubleValidator.MESSAGE_NUMBER_OUT_OF_RANGE,validator.validate("10000", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 100).config(DoubleValidator.KEY_MAX, 1000).build()).getErrors().iterator().next().getMessage());
Assert.assertEquals(DoubleValidator.MESSAGE_NUMBER_OUT_OF_RANGE_TOO_BIG,validator.validate("10000", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MAX, 1000).build()).getErrors().iterator().next().getMessage());
}
@ -318,28 +329,34 @@ public class BuiltinValidatorsTest {
Assert.assertFalse(validator.validate(Arrays.asList("10", new Object()), "notANumberPresent").isValid());
// min only
Assert.assertTrue(validator.validate("10", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 1).build()).isValid());
Assert.assertFalse(validator.validate("10", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 100).build()).isValid());
Assert.assertTrue(validator.validate("10", "name", ValidatorConfig.builder().config(IntegerValidator.KEY_MIN, 1).build()).isValid());
Assert.assertFalse(validator.validate("10", "name", ValidatorConfig.builder().config(IntegerValidator.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());
Assert.assertFalse(validator.validate(null, "name", ValidatorConfig.builder().config(IntegerValidator.KEY_MIN, 1).build()).isValid());
Assert.assertFalse(validator.validate("", "name", ValidatorConfig.builder().config(IntegerValidator.KEY_MIN, 1).build()).isValid());
Assert.assertFalse(validator.validate(" ", "name", ValidatorConfig.builder().config(IntegerValidator.KEY_MIN, 1).build()).isValid());
Assert.assertTrue(validator.validate(null, "name", ValidatorConfig.builder().config(IntegerValidator.KEY_MIN, 1).config(valConfigIgnoreEmptyValues).build()).isValid());
Assert.assertTrue(validator.validate("", "name", ValidatorConfig.builder().config(IntegerValidator.KEY_MIN, 1).config(valConfigIgnoreEmptyValues).build()).isValid());
Assert.assertTrue(validator.validate(" ", "name", ValidatorConfig.builder().config(IntegerValidator.KEY_MIN, 1).config(valConfigIgnoreEmptyValues).build()).isValid());
// max only
Assert.assertFalse(validator.validate("10", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MAX, 1).build()).isValid());
Assert.assertTrue(validator.validate("10", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MAX, 100).build()).isValid());
Assert.assertFalse(validator.validate("10", "name", ValidatorConfig.builder().config(IntegerValidator.KEY_MAX, 1).build()).isValid());
Assert.assertTrue(validator.validate("10", "name", ValidatorConfig.builder().config(IntegerValidator.KEY_MAX, 100).build()).isValid());
// min and max
Assert.assertFalse(validator.validate("9", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 10).config(DoubleValidator.KEY_MAX, 100).build()).isValid());
Assert.assertTrue(validator.validate("10", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 10).config(DoubleValidator.KEY_MAX, 100).build()).isValid());
Assert.assertTrue(validator.validate("100", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 10).config(DoubleValidator.KEY_MAX, 100).build()).isValid());
Assert.assertFalse(validator.validate("101", "name", ValidatorConfig.builder().config(DoubleValidator.KEY_MIN, 10).config(DoubleValidator.KEY_MAX, 100).build()).isValid());
Assert.assertFalse(validator.validate("9", "name", ValidatorConfig.builder().config(IntegerValidator.KEY_MIN, 10).config(IntegerValidator.KEY_MAX, 100).build()).isValid());
Assert.assertTrue(validator.validate("10", "name", ValidatorConfig.builder().config(IntegerValidator.KEY_MIN, 10).config(IntegerValidator.KEY_MAX, 100).build()).isValid());
Assert.assertTrue(validator.validate("100", "name", ValidatorConfig.builder().config(IntegerValidator.KEY_MIN, 10).config(IntegerValidator.KEY_MAX, 100).build()).isValid());
Assert.assertFalse(validator.validate("101", "name", ValidatorConfig.builder().config(IntegerValidator.KEY_MIN, 10).config(IntegerValidator.KEY_MAX, 100).build()).isValid());
Assert.assertTrue(validator.validate(Long.MIN_VALUE, "name").isValid());
Assert.assertTrue(validator.validate(Long.MAX_VALUE, "name").isValid());
//test correct error message selection
Assert.assertEquals(IntegerValidator.MESSAGE_NUMBER_OUT_OF_RANGE_TOO_SMALL,validator.validate("10", "name", ValidatorConfig.builder().config(IntegerValidator.KEY_MIN, 100).build()).getErrors().iterator().next().getMessage());
Assert.assertEquals(IntegerValidator.MESSAGE_NUMBER_OUT_OF_RANGE,validator.validate("10", "name", ValidatorConfig.builder().config(IntegerValidator.KEY_MIN, 100).config(IntegerValidator.KEY_MAX, 1000).build()).getErrors().iterator().next().getMessage());
Assert.assertEquals(IntegerValidator.MESSAGE_NUMBER_OUT_OF_RANGE,validator.validate("10000", "name", ValidatorConfig.builder().config(IntegerValidator.KEY_MIN, 100).config(IntegerValidator.KEY_MAX, 1000).build()).getErrors().iterator().next().getMessage());
Assert.assertEquals(IntegerValidator.MESSAGE_NUMBER_OUT_OF_RANGE_TOO_BIG,validator.validate("10000", "name", ValidatorConfig.builder().config(IntegerValidator.KEY_MAX, 1000).build()).getErrors().iterator().next().getMessage());
}
@Test
@ -384,15 +401,19 @@ public class BuiltinValidatorsTest {
Validator validator = Validators.patternValidator();
// Pattern object in the configuration
ValidatorConfig config = configFromMap(Collections.singletonMap(PatternValidator.KEY_PATTERN, Pattern.compile("^start-.*-end$")));
ValidatorConfig config = configFromMap(Collections.singletonMap(PatternValidator.CFG_PATTERN, Pattern.compile("^start-.*-end$")));
Assert.assertTrue(validator.validate("start-1234-end", "value", config).isValid());
Assert.assertFalse(validator.validate("start___end", "value", config).isValid());
// String in the configuration
config = configFromMap(Collections.singletonMap(PatternValidator.KEY_PATTERN, "^start-.*-end$"));
config = configFromMap(Collections.singletonMap(PatternValidator.CFG_PATTERN, "^start-.*-end$"));
Assert.assertTrue(validator.validate("start-1234-end", "value", config).isValid());
Assert.assertFalse(validator.validate("start___end", "value", config).isValid());
//custom error message
config = ValidatorConfig.builder().config(PatternValidator.CFG_PATTERN, "^start-.*-end$").config(PatternValidator.CFG_ERROR_MESSAGE, "customError").build();
Assert.assertEquals("customError", validator.validate("start___end", "value", config).getErrors().iterator().next().getMessage());
// null and empty values handling
Assert.assertFalse(validator.validate(null, "value", config).isValid());
Assert.assertFalse(validator.validate("", "value", config).isValid());

View file

@ -86,7 +86,7 @@ public class ValidatorTest {
Assert.assertNotNull(error);
Assert.assertEquals(LengthValidator.ID, error.getValidatorId());
Assert.assertEquals(inputHint, error.getInputHint());
Assert.assertEquals(LengthValidator.MESSAGE_INVALID_LENGTH, error.getMessage());
Assert.assertEquals(LengthValidator.MESSAGE_INVALID_LENGTH_TOO_SHORT, error.getMessage());
Assert.assertEquals(new Integer(2), error.getMessageParameters()[0]);
Assert.assertTrue(result.hasErrorsForValidatorId(LengthValidator.ID));

View file

@ -302,10 +302,11 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
new AttributeValidatorMetadata(DuplicateUsernameValidator.ID),
new AttributeValidatorMetadata(UsernameMutationValidator.ID)).setAttributeDisplayName("${username}");
metadata.addAttribute(UserModel.EMAIL, -1, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_EMAIL, false)),
new AttributeValidatorMetadata(EmailValidator.ID, ValidatorConfig.builder().config(EmailValidator.IGNORE_EMPTY_VALUE, true).build()),
metadata.addAttribute(UserModel.EMAIL, -1,
new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_EMAIL, false)),
new AttributeValidatorMetadata(DuplicateEmailValidator.ID),
new AttributeValidatorMetadata(EmailExistsAsUsernameValidator.ID)).setAttributeDisplayName("${email}");
new AttributeValidatorMetadata(EmailExistsAsUsernameValidator.ID))
.setAttributeDisplayName("${email}");
List<AttributeValidatorMetadata> readonlyValidators = new ArrayList<>();
@ -326,8 +327,9 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
metadata.addAttribute(UserModel.USERNAME, -2, AbstractUserProfileProvider::editUsernameCondition,
AbstractUserProfileProvider::readUsernameCondition, new AttributeValidatorMetadata(BrokeringFederatedUsernameHasValueValidator.ID)).setAttributeDisplayName("${username}");
metadata.addAttribute(UserModel.EMAIL, -1, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_EMAIL, true)),
new AttributeValidatorMetadata(EmailValidator.ID)).setAttributeDisplayName("${email}");
metadata.addAttribute(UserModel.EMAIL, -1,
new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_EMAIL, true)))
.setAttributeDisplayName("${email}");
List<AttributeValidatorMetadata> readonlyValidators = new ArrayList<>();

View file

@ -62,6 +62,7 @@ import org.keycloak.userprofile.validator.BlankAttributeValidator;
import org.keycloak.userprofile.validator.ImmutableAttributeValidator;
import org.keycloak.validate.AbstractSimpleValidator;
import org.keycloak.validate.ValidatorConfig;
import org.keycloak.validate.validators.EmailValidator;
/**
* {@link UserProfileProvider} loading configuration from the changeable JSON file stored in component config. Parsed
@ -142,6 +143,13 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
decoratedMetadata.addAttribute(UserModel.FIRST_NAME, 1, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(
Messages.MISSING_FIRST_NAME, metadata.getContext() == UserProfileContext.IDP_REVIEW))).setAttributeDisplayName("${firstName}");
decoratedMetadata.addAttribute(UserModel.LAST_NAME, 2, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_LAST_NAME, metadata.getContext() == UserProfileContext.IDP_REVIEW))).setAttributeDisplayName("${lastName}");
//add email format validator to legacy profile
List<AttributeMetadata> em = decoratedMetadata.getAttribute(UserModel.EMAIL);
for(AttributeMetadata e: em) {
e.addValidator(new AttributeValidatorMetadata(EmailValidator.ID, ValidatorConfig.builder().config(EmailValidator.IGNORE_EMPTY_VALUE, true).build()));
}
return decoratedMetadata;
}
return decoratedMetadata;

View file

@ -0,0 +1,83 @@
/*
* 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.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import org.keycloak.provider.ConfiguredProvider;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.validate.AbstractStringValidator;
import org.keycloak.validate.ValidationContext;
import org.keycloak.validate.ValidationError;
import org.keycloak.validate.ValidatorConfig;
/**
* This validator disallowing bunch of characters we really not to expect in names of persons (fist, middle, last names).
* <p>
* Validates against hardcoded RegEx pattern - accepts plain string and collection of strings, for basic behavior
* like null/blank values handling and collections support see {@link AbstractStringValidator}.
*/
public class PersonNameProhibitedCharactersValidator extends AbstractStringValidator implements ConfiguredProvider {
public static final String ID = "person-name-prohibited-characters";
public static final PersonNameProhibitedCharactersValidator INSTANCE = new PersonNameProhibitedCharactersValidator();
protected static final Pattern PATTERN = Pattern.compile("^[^<>&\"\\v$%!#?§;*~/\\\\|^=\\[\\]{}()\\p{Cntrl}]+$");
public static final String MESSAGE_NO_MATCH = "error-person-name-invalid-character";
public static final String CFG_ERROR_MESSAGE = "error-message";
private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
static {
ProviderConfigProperty property;
property = new ProviderConfigProperty();
property.setName(CFG_ERROR_MESSAGE);
property.setLabel("Error message key");
property.setHelpText("Key of the error message in i18n bundle. Dafault message key is " + MESSAGE_NO_MATCH);
property.setType(ProviderConfigProperty.STRING_TYPE);
configProperties.add(property);
}
@Override
public String getId() {
return ID;
}
@Override
protected void doValidate(String value, String inputHint, ValidationContext context, ValidatorConfig config) {
if (!PATTERN.matcher(value).matches()) {
context.addError(new ValidationError(ID, inputHint, config.getStringOrDefault(CFG_ERROR_MESSAGE, MESSAGE_NO_MATCH)));
}
}
@Override
public String getHelpText() {
return "Basic person name (First, Middle, Last name) validator disallowing bunch of characters we really do not expect in names.";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
}

View file

@ -0,0 +1,83 @@
/*
* 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.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import org.keycloak.provider.ConfiguredProvider;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.validate.AbstractStringValidator;
import org.keycloak.validate.ValidationContext;
import org.keycloak.validate.ValidationError;
import org.keycloak.validate.ValidatorConfig;
/**
* This validator disallowing bunch of characters we really not to expect in username.
* <p>
* Validates against hardcoded RegEx pattern - accepts plain string and collection of strings, for basic behavior
* like null/blank values handling and collections support see {@link AbstractStringValidator}.
*/
public class UsernameProhibitedCharactersValidator extends AbstractStringValidator implements ConfiguredProvider {
public static final String ID = "username-prohibited-characters";
public static final UsernameProhibitedCharactersValidator INSTANCE = new UsernameProhibitedCharactersValidator();
protected static final Pattern PATTERN = Pattern.compile("^[^<>&\"'\\s\\v\\h$%!#?§,;:*~/\\\\|^=\\[\\]{}()`\\p{Cntrl}]+$");
public static final String MESSAGE_NO_MATCH = "error-username-invalid-character";
public static final String CFG_ERROR_MESSAGE = "error-message";
private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
static {
ProviderConfigProperty property;
property = new ProviderConfigProperty();
property.setName(CFG_ERROR_MESSAGE);
property.setLabel("Error message key");
property.setHelpText("Key of the error message in i18n bundle. Dafault message key is " + MESSAGE_NO_MATCH);
property.setType(ProviderConfigProperty.STRING_TYPE);
configProperties.add(property);
}
@Override
public String getId() {
return ID;
}
@Override
protected void doValidate(String value, String inputHint, ValidationContext context, ValidatorConfig config) {
if (!PATTERN.matcher(value).matches()) {
context.addError(new ValidationError(ID, inputHint, config.getStringOrDefault(CFG_ERROR_MESSAGE, MESSAGE_NO_MATCH)));
}
}
@Override
public String getHelpText() {
return "Basic Username validator disallowing bunch of characters we really do not expect in username.";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
}

View file

@ -11,3 +11,5 @@ org.keycloak.userprofile.validator.RegistrationUsernameExistsValidator
org.keycloak.userprofile.validator.RegistrationEmailAsUsernameEmailValueValidator
org.keycloak.userprofile.validator.BrokeringFederatedUsernameHasValueValidator
org.keycloak.userprofile.validator.ImmutableAttributeValidator
org.keycloak.userprofile.validator.UsernameProhibitedCharactersValidator
org.keycloak.userprofile.validator.PersonNameProhibitedCharactersValidator

View file

@ -2,11 +2,19 @@
"attributes": [
{
"name": "username",
"displayName": "${username}"
"displayName": "${username}",
"validations": {
"length": { "min": 3, "max": 255 },
"username-prohibited-characters": {}
}
},
{
"name": "email",
"displayName": "${email}"
"displayName": "${email}",
"validations": {
"email" : {},
"length": { "max": 255 }
}
},
{
"name": "firstName",
@ -15,6 +23,10 @@
"permissions": {
"view": ["admin", "user"],
"edit": ["admin", "user"]
},
"validations": {
"length": { "max": 255 },
"person-name-prohibited-characters": {}
}
},
{
@ -24,6 +36,10 @@
"permissions": {
"view": ["admin", "user"],
"edit": ["admin", "user"]
},
"validations": {
"length": { "max": 255 },
"person-name-prohibited-characters": {}
}
}
]

View file

@ -0,0 +1,109 @@
/*
* 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 org.junit.Assert;
import org.junit.Test;
/**
* @author Vlastimil Elias <velias@redhat.com>
*/
public class PersonNameProhibitedCharactersValidatorTest {
@Test
public void allowed() {
// letters and numbers
assertValid("a");
assertValid("A");
assertValid("z");
assertValid("Z");
assertValid("0");
assertValid("9");
//other than ASCII alphabets
assertValid("\u010D");
assertValid("\u01B1");
assertValid("\u0397");
assertValid("\u98CE\u7720");
// symbols we want to be allowed
assertValid(" ");
assertValid(".");
assertValid("-");
assertValid("_");
assertValid("@");
assertValid("'");
assertValid(":");
assertValid(",");
assertValid("as tr");
//crazy but existing name ;-)
assertValid("X \u00C6 A-12");
}
@Test
public void disallowed() {
// white and control characters
assertInvalid("\t");
assertInvalid("\n");
assertInvalid("\f");
assertInvalid("\r");
assertInvalid("\u0000");
//symbols dangerous for distinct technologies or really unnecessary in names
//potential path traversals
assertInvalid("/");
assertInvalid("\\");
//html/javascript dangerous
assertInvalid("<");
assertInvalid(">");
assertInvalid("\"");
assertInvalid("&");
//other symbols not expected in names and potentially dangerous for other technologies
assertInvalid("*");
assertInvalid("$");
assertInvalid("%");
assertInvalid("#");
assertInvalid("(");
assertInvalid(")");
assertInvalid("{");
assertInvalid("}");
assertInvalid("|");
assertInvalid("~");
assertInvalid("^");
assertInvalid("!");
assertInvalid("?");
assertInvalid(";");
assertInvalid("§");
assertInvalid("=");
//unexpected character between expected
assertInvalid("as\ttr");
assertInvalid("\tastr");
assertInvalid("astr\t");
}
private void assertValid(String value) {
Assert.assertTrue(PersonNameProhibitedCharactersValidator.INSTANCE.validate(value).isValid());
}
private void assertInvalid(String value) {
Assert.assertFalse(PersonNameProhibitedCharactersValidator.INSTANCE.validate(value).isValid());
}
}

View file

@ -0,0 +1,105 @@
/*
* 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 org.junit.Assert;
import org.junit.Test;
/**
* @author Vlastimil Elias <velias@redhat.com>
*/
public class UsernameProhibitedCharactersValidatorTest {
@Test
public void allowed() {
// letters and numbers
assertValid("a");
assertValid("A");
assertValid("z");
assertValid("Z");
assertValid("0");
assertValid("9");
assertValid("\u010D");
assertValid("\u01B1");
assertValid("\u0397");
// symbols we want to be allowed
assertValid(".");
assertValid("-");
assertValid("_");
assertValid("@");
}
@Test
public void disallowed() {
// white and control characters
assertInvalid(" ");
assertInvalid("\t");
assertInvalid("\n");
assertInvalid("\f");
assertInvalid("\r");
assertInvalid("\u0000");
//symbols dangerous for distinct technologies or really unnecessary in username
//potential path traversals
assertInvalid("/");
assertInvalid("\\");
//html/javascript dangerous
assertInvalid("<");
assertInvalid(">");
assertInvalid("'");
assertInvalid("\"");
assertInvalid("&");
//other symbols not expected in username and potentially dangerous for other technologies
assertInvalid("*");
assertInvalid("$");
assertInvalid("%");
assertInvalid("#");
assertInvalid("(");
assertInvalid(")");
assertInvalid("{");
assertInvalid("}");
assertInvalid("|");
assertInvalid("`");
assertInvalid("~");
assertInvalid("^");
assertInvalid("!");
assertInvalid("?");
assertInvalid(":");
assertInvalid(",");
assertInvalid(";");
assertInvalid("§");
assertInvalid("=");
//unexpected character between expected
assertInvalid("as tr");
assertInvalid("\tastr");
assertInvalid("astr\t");
}
private void assertValid(String value) {
Assert.assertTrue(UsernameProhibitedCharactersValidator.INSTANCE.validate(value).isValid());
}
private void assertInvalid(String value) {
Assert.assertFalse(UsernameProhibitedCharactersValidator.INSTANCE.validate(value).isValid());
}
}

View file

@ -111,7 +111,6 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe
events.expectRequiredAction(EventType.UPDATE_PROFILE).detail(Details.PREVIOUS_FIRST_NAME, "Tom").detail(Details.UPDATED_FIRST_NAME, "New first")
.detail(Details.PREVIOUS_LAST_NAME, "Brady").detail(Details.UPDATED_LAST_NAME, "New last")
.detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com")
.detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com")
.assertEvent();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());

View file

@ -320,13 +320,16 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest {
loginPage.clickRegister();
registerPage.assertCurrent();
registerPage.register("firstName", "lastName", "registerUserSuccess@email", "registerUserSuccess", "password", "password");
//contains few special characters we want to be sure they are allowed in username
String username = "register.U-se@rS_uccess";
registerPage.register("firstName", "lastName", "registerUserSuccess@email", username, "password", "password");
appPage.assertCurrent();
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
String userId = events.expectRegister("registerUserSuccess", "registerUserSuccess@email").assertEvent().getUserId();
assertUserRegistered(userId, "registerusersuccess", "registerusersuccess@email");
String userId = events.expectRegister(username, "registerUserSuccess@email").assertEvent().getUserId();
assertUserRegistered(userId, username.toLowerCase(), "registerusersuccess@email");
}
private void assertUserRegistered(String userId, String username, String email) {

View file

@ -758,7 +758,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
fail("Should fail validation");
} catch (ValidationException ve) {
assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
assertTrue(ve.hasError(LengthValidator.MESSAGE_INVALID_LENGTH));
assertTrue(ve.hasError(LengthValidator.MESSAGE_INVALID_LENGTH_TOO_SHORT));
}
attributes.put(UserModel.USERNAME, "user");
@ -769,7 +769,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
provider.setConfiguration(null);
attributes.put(UserModel.USERNAME, "us");
attributes.put(UserModel.USERNAME, "user");
attributes.put(UserModel.FIRST_NAME, "Joe");
attributes.put(UserModel.LAST_NAME, "Doe");

View file

@ -380,12 +380,19 @@ error-invalid-value=Invalid value.
error-invalid-blank=Please specify value.
error-empty=Please specify value.
error-invalid-length=Attribute {0} must have a length between {1} and {2}.
error-invalid-length-too-short=Attribute {0} must have minimal length of {1}.
error-invalid-length-too-long=Attribute {0} must have maximal length of {2}.
error-invalid-email=Invalid email address.
error-invalid-number=Invalid number.
error-number-out-of-range=Attribute {0} must be a number between {1} and {2}.
error-number-out-of-range-too-small=Attribute {0} must have minimal value of {1}.
error-number-out-of-range-too-big=Attribute {0} must have maximal value of {2}.
error-pattern-no-match=Invalid value.
error-invalid-uri=Invalid URL.
error-invalid-uri-scheme=Invalid URL scheme.
error-invalid-uri-fragment=Invalid URL fragment.
error-user-attribute-required=Please specify attribute {0}.
error-invalid-date=Invalid date.
error-user-attribute-read-only=The field {0} is read only.
error-username-invalid-character=Username contains invalid character.
error-person-name-invalid-character=Name contains invalid character.

View file

@ -45,14 +45,19 @@ error-invalid-value=Invalid value.
error-invalid-blank=Please specify value.
error-empty=Please specify value.
error-invalid-length=Attribute {0} must have a length between {1} and {2}.
error-invalid-length-too-short=Attribute {0} must have minimal length of {1}.
error-invalid-length-too-long=Attribute {0} must have maximal length of {2}.
error-invalid-email=Invalid email address.
error-invalid-number=Invalid number.
error-number-out-of-range=Attribute {0} must be a number between {1} and {2}.
error-number-out-of-range-too-small=Attribute {0} must have minimal value of {1}.
error-number-out-of-range-too-big=Attribute {0} must have maximal value of {2}.
error-pattern-no-match=Invalid value.
error-invalid-uri=Invalid URL.
error-invalid-uri-scheme=Invalid URL scheme.
error-invalid-uri-fragment=Invalid URL fragment.
error-user-attribute-required=Please specify attribute {0}.
error-invalid-date=Invalid date.
error-invalid-date=Attribute {0} is invalid date.
error-user-attribute-read-only=Attribute {0} is read only.
error-username-invalid-character={0} contains invalid character.
error-person-name-invalid-character={0} contains invalid character.

View file

@ -212,15 +212,22 @@ error-invalid-value=Invalid value.
error-invalid-blank=Please specify value.
error-empty=Please specify value.
error-invalid-length=Length must be between {1} and {2}.
error-invalid-length-too-short=Minimal length is {1}.
error-invalid-length-too-long=Maximal length is {2}.
error-invalid-email=Invalid email address.
error-invalid-number=Invalid number.
error-number-out-of-range=Number must be between {1} and {2}.
error-number-out-of-range-too-small=Number must have minimal value of {1}.
error-number-out-of-range-too-big=Number must have maximal value of {2}.
error-pattern-no-match=Invalid value.
error-invalid-uri=Invalid URL.
error-invalid-uri-scheme=Invalid URL scheme.
error-invalid-uri-fragment=Invalid URL fragment.
error-user-attribute-required=Please specify this field.
error-invalid-date=Invalid date.
error-user-attribute-read-only=This field is read only.
error-username-invalid-character=Value contains invalid character.
error-person-name-invalid-character=Value contains invalid character.
invalidPasswordExistingMessage=Invalid existing password.
invalidPasswordBlacklistedMessage=Invalid password: password is blacklisted.

View file

@ -124,11 +124,18 @@ error-invalid-value=''{0}'' has invalid value.
error-invalid-blank=Please specify value of ''{0}''.
error-empty=Please specify value of ''{0}''.
error-invalid-length=''{0}'' must have a length between {1} and {2}.
error-invalid-length-too-short=''{0}'' must have minimal length of {1}.
error-invalid-length-too-long=''{0}'' must have maximal length of {2}.
error-invalid-email=Invalid email address.
error-invalid-number=''{0}'' is invalid number.
error-number-out-of-range=''{0}'' must be a number between {1} and {2}.
error-number-out-of-range-too-small=''{0}'' must have minimal value of {1}.
error-number-out-of-range-too-big=''{0}'' must have maximal value of {2}.
error-pattern-no-match=''{0}'' doesn''t match required format.
error-invalid-uri=''{0}'' is invalid URL.
error-invalid-uri-scheme=''{0}'' has invalid URL scheme.
error-invalid-uri-fragment=''{0}'' is invalid URL fragment.
error-user-attribute-required=Please specify ''{0}''.
error-invalid-date=''{0}'' is invalid date.
error-username-invalid-character=''{0}'' contains invalid character.
error-person-name-invalid-character='{0}' contains invalid character.