Fixing UriValidator
closes #26792 Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
parent
4ff4c3f897
commit
b4d289c562
4 changed files with 126 additions and 12 deletions
|
@ -6,6 +6,10 @@ import { HelpItem } from "ui-shared";
|
||||||
import { MultiLineInput } from "../multi-line-input/MultiLineInput";
|
import { MultiLineInput } from "../multi-line-input/MultiLineInput";
|
||||||
import { convertToName } from "./DynamicComponents";
|
import { convertToName } from "./DynamicComponents";
|
||||||
|
|
||||||
|
function convertDefaultValue(formValue?: any): string[] {
|
||||||
|
return formValue && Array.isArray(formValue) ? formValue : [formValue];
|
||||||
|
}
|
||||||
|
|
||||||
export const MultiValuedStringComponent = ({
|
export const MultiValuedStringComponent = ({
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
|
@ -29,7 +33,7 @@ export const MultiValuedStringComponent = ({
|
||||||
aria-label={t(label!)}
|
aria-label={t(label!)}
|
||||||
name={fieldName}
|
name={fieldName}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
defaultValue={[defaultValue]}
|
defaultValue={convertDefaultValue(defaultValue)}
|
||||||
addButtonLabel={t("addMultivaluedLabel", {
|
addButtonLabel={t("addMultivaluedLabel", {
|
||||||
fieldLabel: t(label!).toLowerCase(),
|
fieldLabel: t(label!).toLowerCase(),
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -18,7 +18,7 @@ package org.keycloak.validate.validators;
|
||||||
|
|
||||||
import org.keycloak.provider.ConfiguredProvider;
|
import org.keycloak.provider.ConfiguredProvider;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
import org.keycloak.validate.SimpleValidator;
|
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;
|
||||||
import org.keycloak.validate.ValidatorConfig;
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
@ -27,6 +27,7 @@ import java.net.MalformedURLException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
@ -37,7 +38,7 @@ import java.util.Set;
|
||||||
* URI validation - accepts {@link URI}, {@link URL} and single String. Null input is valid, use other validators (like
|
* URI validation - accepts {@link URI}, {@link URL} and single String. Null input is valid, use other validators (like
|
||||||
* {@link NotBlankValidator} or {@link NotEmptyValidator} to force field as required.
|
* {@link NotBlankValidator} or {@link NotEmptyValidator} to force field as required.
|
||||||
*/
|
*/
|
||||||
public class UriValidator implements SimpleValidator, ConfiguredProvider {
|
public class UriValidator extends AbstractSimpleValidator implements ConfiguredProvider {
|
||||||
|
|
||||||
public static final UriValidator INSTANCE = new UriValidator();
|
public static final UriValidator INSTANCE = new UriValidator();
|
||||||
|
|
||||||
|
@ -45,10 +46,10 @@ public class UriValidator implements SimpleValidator, ConfiguredProvider {
|
||||||
public static final String KEY_ALLOW_FRAGMENT = "allowFragment";
|
public static final String KEY_ALLOW_FRAGMENT = "allowFragment";
|
||||||
public static final String KEY_REQUIRE_VALID_URL = "requireValidUrl";
|
public static final String KEY_REQUIRE_VALID_URL = "requireValidUrl";
|
||||||
|
|
||||||
public static final Set<String> DEFAULT_ALLOWED_SCHEMES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
|
public static final List<String> DEFAULT_ALLOWED_SCHEMES = Collections.unmodifiableList(Arrays.asList(
|
||||||
"http",
|
"http",
|
||||||
"https"
|
"https"
|
||||||
)));
|
));
|
||||||
public static final String MESSAGE_INVALID_URI = "error-invalid-uri";
|
public static final String MESSAGE_INVALID_URI = "error-invalid-uri";
|
||||||
public static final String MESSAGE_INVALID_SCHEME = "error-invalid-uri-scheme";
|
public static final String MESSAGE_INVALID_SCHEME = "error-invalid-uri-scheme";
|
||||||
public static final String MESSAGE_INVALID_FRAGMENT = "error-invalid-uri-fragment";
|
public static final String MESSAGE_INVALID_FRAGMENT = "error-invalid-uri-fragment";
|
||||||
|
@ -59,16 +60,50 @@ public class UriValidator implements SimpleValidator, ConfiguredProvider {
|
||||||
|
|
||||||
public static final String ID = "uri";
|
public static final String ID = "uri";
|
||||||
|
|
||||||
|
private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
ProviderConfigProperty property;
|
||||||
|
property = new ProviderConfigProperty();
|
||||||
|
property.setName(KEY_ALLOWED_SCHEMES);
|
||||||
|
property.setLabel("Allowed schemes");
|
||||||
|
property.setHelpText("Allowed URL schemes. Defaults to 'http' and 'https' as only allowed schemes");
|
||||||
|
property.setType(ProviderConfigProperty.MULTIVALUED_STRING_TYPE);
|
||||||
|
property.setDefaultValue(DEFAULT_ALLOWED_SCHEMES);
|
||||||
|
configProperties.add(property);
|
||||||
|
|
||||||
|
property = new ProviderConfigProperty();
|
||||||
|
property.setName(KEY_ALLOW_FRAGMENT);
|
||||||
|
property.setLabel("Allow fragment");
|
||||||
|
property.setHelpText("Specify if allow URL with the URI fragment. It is true by default");
|
||||||
|
property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
|
||||||
|
property.setDefaultValue(DEFAULT_ALLOW_FRAGMENT);
|
||||||
|
configProperties.add(property);
|
||||||
|
|
||||||
|
property = new ProviderConfigProperty();
|
||||||
|
property.setName(KEY_REQUIRE_VALID_URL);
|
||||||
|
property.setLabel("Require Valid URL");
|
||||||
|
property.setHelpText("Checks if the specified URL is valid URL. It is true by default");
|
||||||
|
property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
|
||||||
|
property.setDefaultValue(DEFAULT_REQUIRE_VALID_URL);
|
||||||
|
configProperties.add(property);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return ID;
|
return ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
protected boolean skipValidation(Object value, ValidatorConfig config) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doValidate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||||
|
|
||||||
if(input == null || (input instanceof String && ((String) input).isEmpty())) {
|
if(input == null || (input instanceof String && ((String) input).isEmpty())) {
|
||||||
return context;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -77,7 +112,7 @@ public class UriValidator implements SimpleValidator, ConfiguredProvider {
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_URI, input));
|
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_URI, input));
|
||||||
} else {
|
} else {
|
||||||
Set<String> allowedSchemes = config.getStringSetOrDefault(KEY_ALLOWED_SCHEMES, DEFAULT_ALLOWED_SCHEMES);
|
Set<String> allowedSchemes = new HashSet<>(config.getStringListOrDefault(KEY_ALLOWED_SCHEMES, DEFAULT_ALLOWED_SCHEMES));
|
||||||
boolean allowFragment = config.getBooleanOrDefault(KEY_ALLOW_FRAGMENT, DEFAULT_ALLOW_FRAGMENT);
|
boolean allowFragment = config.getBooleanOrDefault(KEY_ALLOW_FRAGMENT, DEFAULT_ALLOW_FRAGMENT);
|
||||||
boolean requireValidUrl = config.getBooleanOrDefault(KEY_REQUIRE_VALID_URL, DEFAULT_REQUIRE_VALID_URL);
|
boolean requireValidUrl = config.getBooleanOrDefault(KEY_REQUIRE_VALID_URL, DEFAULT_REQUIRE_VALID_URL);
|
||||||
|
|
||||||
|
@ -86,8 +121,6 @@ public class UriValidator implements SimpleValidator, ConfiguredProvider {
|
||||||
} catch (MalformedURLException | IllegalArgumentException | URISyntaxException e) {
|
} catch (MalformedURLException | IllegalArgumentException | URISyntaxException e) {
|
||||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_URI, input, e.getMessage()));
|
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_URI, input, e.getMessage()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private URI toUri(Object input) throws URISyntaxException {
|
private URI toUri(Object input) throws URISyntaxException {
|
||||||
|
@ -144,6 +177,6 @@ public class UriValidator implements SimpleValidator, ConfiguredProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ProviderConfigProperty> getConfigProperties() {
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
return Collections.emptyList();
|
return configProperties;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,6 @@ import static org.junit.Assert.assertTrue;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_ADMIN;
|
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_ADMIN;
|
||||||
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_USER;
|
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_USER;
|
||||||
import static org.keycloak.userprofile.config.UPConfigUtils.parseSystemDefaultConfig;
|
|
||||||
|
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -85,6 +84,7 @@ import org.keycloak.userprofile.validator.UsernameIDNHomographValidator;
|
||||||
import org.keycloak.validate.ValidationError;
|
import org.keycloak.validate.ValidationError;
|
||||||
import org.keycloak.validate.validators.EmailValidator;
|
import org.keycloak.validate.validators.EmailValidator;
|
||||||
import org.keycloak.validate.validators.LengthValidator;
|
import org.keycloak.validate.validators.LengthValidator;
|
||||||
|
import org.keycloak.validate.validators.UriValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
@ -1111,6 +1111,74 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ModelTest(realmName = "test")
|
||||||
|
public void testUriValidator(KeycloakSession session) {
|
||||||
|
UserProfileProvider provider = getUserProfileProvider(session);
|
||||||
|
UPConfig config = UPConfigUtils.parseSystemDefaultConfig();
|
||||||
|
|
||||||
|
UPAttribute attribute = new UPAttribute("picture-url");
|
||||||
|
attribute.addValidation(UriValidator.ID, new HashMap<>());
|
||||||
|
config.addOrReplaceAttribute(attribute);
|
||||||
|
provider.setConfiguration(config);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Should fail with the default error message
|
||||||
|
Map<String, Object> attributes = new HashMap<>();
|
||||||
|
attributes.put(UserModel.USERNAME, "abc");
|
||||||
|
attributes.put("picture-url", "some-invalid-url");
|
||||||
|
|
||||||
|
UserProfile profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||||
|
try {
|
||||||
|
profile.validate();
|
||||||
|
fail("Should fail validation");
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
assertTrue(ve.hasError(UriValidator.MESSAGE_INVALID_URI));
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL with fragment should be OK by default
|
||||||
|
attributes.put("picture-url", "https://somehost/somepath?param=foo#frg=bar");
|
||||||
|
profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||||
|
profile.validate();
|
||||||
|
|
||||||
|
// Not allow fragment
|
||||||
|
attribute.addValidation(UriValidator.ID, Map.of(UriValidator.KEY_ALLOW_FRAGMENT, false));
|
||||||
|
config.addOrReplaceAttribute(attribute);
|
||||||
|
provider.setConfiguration(config);
|
||||||
|
|
||||||
|
attributes.put("picture-url", "https://somehost/somepath?param=foo#frg=bar");
|
||||||
|
profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||||
|
try {
|
||||||
|
profile.validate();
|
||||||
|
fail("Should fail validation");
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
assertTrue(ve.hasError(UriValidator.MESSAGE_INVALID_FRAGMENT));
|
||||||
|
}
|
||||||
|
|
||||||
|
// not allow file URL by default
|
||||||
|
attributes.put("picture-url", "file:///somefile.txt");
|
||||||
|
profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||||
|
try {
|
||||||
|
profile.validate();
|
||||||
|
fail("Should fail validation");
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
assertTrue(ve.hasError(UriValidator.MESSAGE_INVALID_SCHEME));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow file scheme and check it works
|
||||||
|
attribute.addValidation(UriValidator.ID, Map.of(UriValidator.KEY_ALLOWED_SCHEMES, Arrays.asList("https", "http", "file")));
|
||||||
|
config.addOrReplaceAttribute(attribute);
|
||||||
|
provider.setConfiguration(config);
|
||||||
|
|
||||||
|
attributes.put("picture-url", "file:///somefile.txt");
|
||||||
|
profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||||
|
profile.validate();
|
||||||
|
} finally {
|
||||||
|
config.removeAttribute("picture-url");
|
||||||
|
provider.setConfiguration(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCustomAttributeRequired() {
|
public void testCustomAttributeRequired() {
|
||||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testCustomAttributeRequired);
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testCustomAttributeRequired);
|
||||||
|
|
|
@ -22,6 +22,7 @@ package org.keycloak.testsuite.validation;
|
||||||
import static org.keycloak.validate.ValidatorConfig.configFromMap;
|
import static org.keycloak.validate.ValidatorConfig.configFromMap;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -500,10 +501,18 @@ public class BuiltinValidatorsTest extends AbstractKeycloakTest {
|
||||||
Assert.assertTrue(validator.validate("http://localhost:3000/", "baseUrl").isValid());
|
Assert.assertTrue(validator.validate("http://localhost:3000/", "baseUrl").isValid());
|
||||||
Assert.assertTrue(validator.validate("https://localhost:3000/", "baseUrl").isValid());
|
Assert.assertTrue(validator.validate("https://localhost:3000/", "baseUrl").isValid());
|
||||||
Assert.assertTrue(validator.validate("https://localhost:3000/#someFragment", "baseUrl").isValid());
|
Assert.assertTrue(validator.validate("https://localhost:3000/#someFragment", "baseUrl").isValid());
|
||||||
|
Assert.assertTrue(validator.validate(new URL("https://localhost:3000/#someFragment"), "baseUrl").isValid());
|
||||||
|
|
||||||
|
// Collections
|
||||||
|
Assert.assertTrue(validator.validate(Arrays.asList("https://localhost:3000/#someFragment", "https://localhost:3000"), "baseUrl").isValid());
|
||||||
|
Assert.assertTrue(validator.validate(Arrays.asList("https://localhost:3000/#someFragment"), "baseUrl").isValid());
|
||||||
|
Assert.assertTrue(validator.validate(Arrays.asList(new URL("https://localhost:3000/#someFragment")), "baseUrl").isValid());
|
||||||
|
Assert.assertTrue(validator.validate(Arrays.asList(""), "baseUrl").isValid());
|
||||||
|
|
||||||
Assert.assertFalse(validator.validate(" ", "baseUrl").isValid());
|
Assert.assertFalse(validator.validate(" ", "baseUrl").isValid());
|
||||||
Assert.assertFalse(validator.validate("file:///somefile.txt", "baseUrl").isValid());
|
Assert.assertFalse(validator.validate("file:///somefile.txt", "baseUrl").isValid());
|
||||||
Assert.assertFalse(validator.validate("invalidUrl++@23", "invalidUri").isValid());
|
Assert.assertFalse(validator.validate("invalidUrl++@23", "invalidUri").isValid());
|
||||||
|
Assert.assertFalse(validator.validate(Arrays.asList("https://localhost:3000/#someFragment", "file:///somefile.txt"), "baseUrl").isValid());
|
||||||
|
|
||||||
ValidatorConfig config = configFromMap(ImmutableMap.of(UriValidator.KEY_ALLOW_FRAGMENT, false));
|
ValidatorConfig config = configFromMap(ImmutableMap.of(UriValidator.KEY_ALLOW_FRAGMENT, false));
|
||||||
Assert.assertFalse(validator.validate("https://localhost:3000/#someFragment", "baseUrl", config).isValid());
|
Assert.assertFalse(validator.validate("https://localhost:3000/#someFragment", "baseUrl", config).isValid());
|
||||||
|
|
Loading…
Reference in a new issue