Fixing UriValidator

closes #26792

Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
mposolda 2024-02-12 18:44:22 +01:00 committed by Marek Posolda
parent 4ff4c3f897
commit b4d289c562
4 changed files with 126 additions and 12 deletions

View file

@ -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(),
})} })}

View file

@ -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;
} }
} }

View file

@ -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);

View file

@ -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());