[KEYCLOAK-17399] - Declarative User Profile and UI
Co-authored-by: Vlastimil Elias <velias@redhat.com>
This commit is contained in:
parent
d2a8a95d79
commit
ef3a0ee06c
91 changed files with 3884 additions and 998 deletions
|
@ -61,7 +61,8 @@ public class Profile {
|
|||
WEB_AUTHN(Type.DEFAULT, Type.PREVIEW),
|
||||
CLIENT_POLICIES(Type.DEFAULT),
|
||||
CIBA(Type.PREVIEW),
|
||||
MAP_STORAGE(Type.EXPERIMENTAL);
|
||||
MAP_STORAGE(Type.EXPERIMENTAL),
|
||||
DECLARATIVE_USER_PROFILE(Type.PREVIEW);
|
||||
|
||||
private final Type typeProject;
|
||||
private final Type typeProduct;
|
||||
|
|
|
@ -21,8 +21,8 @@ public class ProfileTest {
|
|||
@Test
|
||||
public void checkDefaultsKeycloak() {
|
||||
Assert.assertEquals("community", Profile.getName());
|
||||
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS, Profile.Feature.CIBA, Profile.Feature.MAP_STORAGE);
|
||||
assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.CIBA);
|
||||
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS, Profile.Feature.CIBA, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE);
|
||||
assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.CIBA, Profile.Feature.DECLARATIVE_USER_PROFILE);
|
||||
assertEquals(Profile.getDeprecatedFeatures(), Profile.Feature.UPLOAD_SCRIPTS);
|
||||
|
||||
Assert.assertTrue(Profile.Feature.WEB_AUTHN.hasDifferentProductType());
|
||||
|
@ -37,8 +37,8 @@ public class ProfileTest {
|
|||
Profile.init();
|
||||
|
||||
Assert.assertEquals("product", Profile.getName());
|
||||
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS, Profile.Feature.WEB_AUTHN, Profile.Feature.CIBA, Profile.Feature.MAP_STORAGE);
|
||||
assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.WEB_AUTHN, Profile.Feature.CIBA);
|
||||
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS, Profile.Feature.WEB_AUTHN, Profile.Feature.CIBA, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE);
|
||||
assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.WEB_AUTHN, Profile.Feature.CIBA, Profile.Feature.DECLARATIVE_USER_PROFILE);
|
||||
assertEquals(Profile.getDeprecatedFeatures(), Profile.Feature.UPLOAD_SCRIPTS);
|
||||
|
||||
Assert.assertTrue(Profile.Feature.WEB_AUTHN.hasDifferentProductType());
|
||||
|
|
|
@ -17,16 +17,35 @@
|
|||
|
||||
package org.keycloak.representations.idm;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class ErrorRepresentation {
|
||||
private String field;
|
||||
private String errorMessage;
|
||||
private Object[] params;
|
||||
private List<ErrorRepresentation> errors;
|
||||
|
||||
public ErrorRepresentation() {
|
||||
}
|
||||
|
||||
public ErrorRepresentation(String errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public ErrorRepresentation(String field, String errorMessage, Object[] params) {
|
||||
super();
|
||||
this.field = field;
|
||||
this.errorMessage = errorMessage;
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
public String getField() {
|
||||
return field;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
@ -42,4 +61,12 @@ public class ErrorRepresentation {
|
|||
public void setParams(Object[] params) {
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
public void setErrors(List<ErrorRepresentation> errors) {
|
||||
this.errors = errors;
|
||||
}
|
||||
|
||||
public List<ErrorRepresentation> getErrors() {
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.admin.client.resource;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
/**
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*/
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public interface UserProfileResource {
|
||||
|
||||
@GET
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
String getConfiguration();
|
||||
|
||||
@PUT
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
Response update(String text);
|
||||
}
|
|
@ -247,5 +247,7 @@ public interface UsersResource {
|
|||
@DELETE
|
||||
Response delete(@PathParam("id") String id);
|
||||
|
||||
@Path("profile")
|
||||
UserProfileResource userProfile();
|
||||
|
||||
}
|
||||
|
|
|
@ -63,6 +63,8 @@ public enum EventType {
|
|||
UPDATE_TOTP_ERROR(true),
|
||||
VERIFY_EMAIL(true),
|
||||
VERIFY_EMAIL_ERROR(true),
|
||||
VERIFY_PROFILE(true),
|
||||
VERIFY_PROFILE_ERROR(true),
|
||||
|
||||
REMOVE_TOTP(true),
|
||||
REMOVE_TOTP_ERROR(true),
|
||||
|
|
|
@ -26,6 +26,6 @@ public enum LoginFormsPages {
|
|||
LOGIN_IDP_LINK_CONFIRM, LOGIN_IDP_LINK_EMAIL,
|
||||
OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, LOGIN_SELECT_AUTHENTICATOR, REGISTER, INFO, ERROR, ERROR_WEBAUTHN, LOGIN_UPDATE_PROFILE,
|
||||
LOGIN_PAGE_EXPIRED, CODE, X509_CONFIRM, SAML_POST_FORM,
|
||||
LOGIN_OAUTH2_DEVICE_VERIFY_USER_CODE;
|
||||
LOGIN_OAUTH2_DEVICE_VERIFY_USER_CODE, VERIFY_PROFILE;
|
||||
|
||||
}
|
||||
|
|
|
@ -43,25 +43,26 @@ public final class AttributeMetadata {
|
|||
|
||||
private final String attributeName;
|
||||
private final Predicate<AttributeContext> selector;
|
||||
private final Predicate<AttributeContext> readOnly;
|
||||
private final Predicate<AttributeContext> writeAllowed;
|
||||
/** Predicate to decide if attribute is required, it is handled as required if predicate is null */
|
||||
private final Predicate<AttributeContext> required;
|
||||
private final Predicate<AttributeContext> readAllowed;
|
||||
private List<AttributeValidatorMetadata> validators;
|
||||
private Map<String, Object> annotations;
|
||||
|
||||
AttributeMetadata(String attributeName) {
|
||||
this(attributeName, ALWAYS_TRUE, ALWAYS_FALSE, ALWAYS_TRUE);
|
||||
this(attributeName, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE);
|
||||
}
|
||||
|
||||
AttributeMetadata(String attributeName, Predicate<AttributeContext> readOnly, Predicate<AttributeContext> required) {
|
||||
this(attributeName, ALWAYS_TRUE, readOnly, required);
|
||||
AttributeMetadata(String attributeName, Predicate<AttributeContext> writeAllowed, Predicate<AttributeContext> required) {
|
||||
this(attributeName, ALWAYS_TRUE, writeAllowed, required, ALWAYS_TRUE);
|
||||
}
|
||||
|
||||
AttributeMetadata(String attributeName, Predicate<AttributeContext> selector) {
|
||||
this(attributeName, selector, ALWAYS_FALSE, ALWAYS_TRUE);
|
||||
this(attributeName, selector, ALWAYS_FALSE, ALWAYS_TRUE, ALWAYS_TRUE);
|
||||
}
|
||||
|
||||
AttributeMetadata(String attributeName, List<String> scopes, Predicate<AttributeContext> readOnly, Predicate<AttributeContext> required) {
|
||||
AttributeMetadata(String attributeName, List<String> scopes, Predicate<AttributeContext> writeAllowed, Predicate<AttributeContext> required) {
|
||||
this(attributeName, context -> {
|
||||
KeycloakSession session = context.getSession();
|
||||
AuthenticationSessionModel authSession = session.getContext().getAuthenticationSession();
|
||||
|
@ -81,14 +82,17 @@ public final class AttributeMetadata {
|
|||
|
||||
return authSession.getClientScopes().stream()
|
||||
.map(id -> clientScopes.getClientScopeById(realm, id).getName()).anyMatch(scopes::contains);
|
||||
}, readOnly, required);
|
||||
}, writeAllowed, required, ALWAYS_TRUE);
|
||||
}
|
||||
|
||||
AttributeMetadata(String attributeName, Predicate<AttributeContext> selector, Predicate<AttributeContext> readOnly, Predicate<AttributeContext> required) {
|
||||
AttributeMetadata(String attributeName, Predicate<AttributeContext> selector, Predicate<AttributeContext> writeAllowed,
|
||||
Predicate<AttributeContext> required,
|
||||
Predicate<AttributeContext> readAllowed) {
|
||||
this.attributeName = attributeName;
|
||||
this.selector = selector;
|
||||
this.readOnly = readOnly;
|
||||
this.writeAllowed = writeAllowed;
|
||||
this.required = required;
|
||||
this.readAllowed = readAllowed;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
|
@ -100,7 +104,11 @@ public final class AttributeMetadata {
|
|||
}
|
||||
|
||||
public boolean isReadOnly(AttributeContext context) {
|
||||
return readOnly.test(context);
|
||||
return !writeAllowed.test(context);
|
||||
}
|
||||
|
||||
public boolean canView(AttributeContext context) {
|
||||
return readAllowed.test(context);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -148,7 +156,7 @@ public final class AttributeMetadata {
|
|||
|
||||
@Override
|
||||
public AttributeMetadata clone() {
|
||||
AttributeMetadata cloned = new AttributeMetadata(attributeName, selector, readOnly, required);
|
||||
AttributeMetadata cloned = new AttributeMetadata(attributeName, selector, writeAllowed, required, readAllowed);
|
||||
// we clone validators list to allow adding or removing validators. Validators
|
||||
// itself are not cloned as we do not expect them to be reconfigured.
|
||||
if (validators != null) {
|
||||
|
@ -160,4 +168,19 @@ public final class AttributeMetadata {
|
|||
}
|
||||
return cloned;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || !(o instanceof AttributeMetadata)) return false;
|
||||
|
||||
AttributeMetadata that = (AttributeMetadata) o;
|
||||
|
||||
return that.getName().equals(getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return attributeName.hashCode();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,9 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.validate.ValidationError;
|
||||
|
||||
/**
|
||||
|
@ -108,4 +110,61 @@ public interface Attributes {
|
|||
* @return the attributes
|
||||
*/
|
||||
Set<Map.Entry<String, List<String>>> attributeSet();
|
||||
|
||||
/**
|
||||
* <p>Returns the metadata associated with the attribute with the given {@code name}.
|
||||
*
|
||||
* <p>The {@link AttributeMetadata} is a copy of the original metadata. The original metadata
|
||||
* keeps immutable.
|
||||
*
|
||||
* @param name the attribute name
|
||||
* @return the metadata
|
||||
*/
|
||||
AttributeMetadata getMetadata(String name);
|
||||
|
||||
/**
|
||||
* Returns whether the attribute with the given {@code name} is required.
|
||||
*
|
||||
* @param name the attribute name
|
||||
* @return {@code true} if the attribute is required. Otherwise, {@code false}.
|
||||
*/
|
||||
boolean isRequired(String name);
|
||||
|
||||
/**
|
||||
* Similar to {{@link #getReadable(boolean)}} but with the possibility to add or remove
|
||||
* the root attributes.
|
||||
*
|
||||
* @param includeBuiltin if the root attributes should be included.
|
||||
* @return the attributes with read/write permission.
|
||||
*/
|
||||
default Map<String, List<String>> getReadable(boolean includeBuiltin) {
|
||||
return getReadable().entrySet().stream().filter(entry -> {
|
||||
if (includeBuiltin) {
|
||||
return true;
|
||||
}
|
||||
return !isRootAttribute(entry.getKey());
|
||||
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns only the attributes that have read/write permissions.
|
||||
*
|
||||
* @return the attributes with read/write permission.
|
||||
*/
|
||||
Map<String, List<String>> getReadable();
|
||||
|
||||
/**
|
||||
* Returns whether the attribute with the given {@code name} is a root attribute.
|
||||
*
|
||||
* @param name the attribute name
|
||||
* @return
|
||||
*/
|
||||
default boolean isRootAttribute(String name) {
|
||||
return UserModel.USERNAME.equals(name)
|
||||
|| UserModel.EMAIL.equals(name)
|
||||
|| UserModel.FIRST_NAME.equals(name)
|
||||
|| UserModel.LAST_NAME.equals(name);
|
||||
}
|
||||
|
||||
Map<String, List<String>> toMap();
|
||||
}
|
||||
|
|
|
@ -27,7 +27,6 @@ import java.util.Map;
|
|||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -48,7 +47,7 @@ import org.keycloak.validate.ValidationError;
|
|||
*
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public final class DefaultAttributes extends HashMap<String, List<String>> implements Attributes {
|
||||
public class DefaultAttributes extends HashMap<String, List<String>> implements Attributes {
|
||||
|
||||
/**
|
||||
* To reference dynamic attributes that can be configured as read-only when setting up the provider.
|
||||
|
@ -59,7 +58,7 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
|||
private final UserProfileContext context;
|
||||
private final KeycloakSession session;
|
||||
private final Map<String, AttributeMetadata> metadataByAttribute;
|
||||
private final UserModel user;
|
||||
protected final UserModel user;
|
||||
|
||||
public DefaultAttributes(UserProfileContext context, Map<String, ?> attributes, UserModel user,
|
||||
UserProfileMetadata profileMetadata,
|
||||
|
@ -79,12 +78,24 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
|||
private boolean isReadOnlyFromMetadata(String attributeName) {
|
||||
AttributeMetadata attributeMetadata = metadataByAttribute.get(attributeName);
|
||||
|
||||
if (attributeMetadata != null && attributeMetadata.isReadOnly(createAttributeContext(attributeName, attributeMetadata))) {
|
||||
return true;
|
||||
}
|
||||
if (attributeMetadata == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return attributeMetadata.isReadOnly(createAttributeContext(attributeMetadata));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRequired(String name) {
|
||||
AttributeMetadata attributeMetadata = metadataByAttribute.get(name);
|
||||
|
||||
if (attributeMetadata == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return attributeMetadata.isRequired(createAttributeContext(attributeMetadata));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validate(String name, Consumer<ValidationError>... listeners) {
|
||||
Entry<String, List<String>> attribute = createAttribute(name);
|
||||
|
@ -95,31 +106,33 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
|||
metadatas.addAll(Optional.ofNullable(this.metadataByAttribute.get(READ_ONLY_ATTRIBUTE_KEY))
|
||||
.map(Collections::singletonList).orElse(Collections.emptyList()));
|
||||
|
||||
List<ValidationContext> failingValidators = Collections.emptyList();
|
||||
Boolean result = null;
|
||||
|
||||
for (AttributeMetadata metadata : metadatas) {
|
||||
AttributeContext attributeContext = createAttributeContext(attribute, metadata);
|
||||
|
||||
for (AttributeValidatorMetadata validator : metadata.getValidators()) {
|
||||
ValidationContext vc = validator.validate(createAttributeContext(attribute, metadata));
|
||||
if (!vc.isValid()) {
|
||||
if (failingValidators.equals(Collections.emptyList())) {
|
||||
failingValidators = new ArrayList<>();
|
||||
}
|
||||
failingValidators.add(vc);
|
||||
}
|
||||
ValidationContext vc = validator.validate(attributeContext);
|
||||
|
||||
if (vc.isValid()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result == null) {
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (listeners != null) {
|
||||
for (ValidationContext failingValidator : failingValidators) {
|
||||
for (ValidationError error : vc.getErrors()) {
|
||||
for (Consumer<ValidationError> consumer : listeners) {
|
||||
for(ValidationError err: failingValidator.getErrors()) {
|
||||
consumer.accept(err);
|
||||
consumer.accept(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return failingValidators.isEmpty();
|
||||
return result == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -142,12 +155,43 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
|||
return entrySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttributeMetadata getMetadata(String name) {
|
||||
AttributeMetadata metadata = metadataByAttribute.get(name);
|
||||
|
||||
if (metadata == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return metadata.clone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> getReadable() {
|
||||
Map<String, List<String>> attributes = new HashMap<>(user.getAttributes());
|
||||
|
||||
if (attributes.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> toMap() {
|
||||
return this;
|
||||
}
|
||||
|
||||
private AttributeContext createAttributeContext(Entry<String, List<String>> attribute, AttributeMetadata metadata) {
|
||||
return new AttributeContext(context, session, attribute, user, metadata);
|
||||
}
|
||||
|
||||
private AttributeContext createAttributeContext(String attributeName, AttributeMetadata metadata) {
|
||||
return createAttributeContext(createAttribute(attributeName), metadata);
|
||||
return new AttributeContext(context, session, createAttribute(attributeName), user, metadata);
|
||||
}
|
||||
|
||||
protected AttributeContext createAttributeContext(AttributeMetadata metadata) {
|
||||
return createAttributeContext(createAttribute(metadata.getName()), metadata);
|
||||
}
|
||||
|
||||
private Map<String, AttributeMetadata> configureMetadata(List<AttributeMetadata> attributes) {
|
||||
|
@ -155,7 +199,7 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
|||
|
||||
for (AttributeMetadata metadata : attributes) {
|
||||
// checks whether the attribute is selected for the current profile
|
||||
if (metadata.isSelected(createAttributeContext(metadata.getName(), metadata))) {
|
||||
if (metadata.isSelected(createAttributeContext(metadata))) {
|
||||
metadatas.put(metadata.getName(), metadata);
|
||||
}
|
||||
}
|
||||
|
@ -190,9 +234,8 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
|||
Map<String, List<String>> newAttributes = new HashMap<>();
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
if (attributes != null && !attributes.isEmpty()) {
|
||||
if (attributes != null) {
|
||||
for (Map.Entry<String, ?> entry : attributes.entrySet()) {
|
||||
Object value = entry.getValue();
|
||||
String key = entry.getKey();
|
||||
|
||||
if (!isSupportedAttribute(key)) {
|
||||
|
@ -204,6 +247,7 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
|||
}
|
||||
|
||||
List<String> values;
|
||||
Object value = entry.getValue();
|
||||
|
||||
if (value instanceof String) {
|
||||
values = Collections.singletonList((String) value);
|
||||
|
@ -215,26 +259,27 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
|||
values = Collections.singletonList(values.get(0).toLowerCase());
|
||||
}
|
||||
|
||||
if (isReadOnlyFromMetadata(key)) {
|
||||
// only revert attribute values if not an internal read-only attribute
|
||||
// for backward compatibility changing these attributes should cause validation errors
|
||||
// ideally, we should just ignore and remove this check
|
||||
if (user == null) {
|
||||
values = EMPTY_VALUE;
|
||||
} else {
|
||||
values = user.getAttributeStream(key).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
newAttributes.put(key, Collections.unmodifiableList(values));
|
||||
}
|
||||
}
|
||||
|
||||
// the profile should always hold all attributes defined in the config
|
||||
for (String attributeName : metadataByAttribute.keySet()) {
|
||||
if (isSupportedAttribute(attributeName)) {
|
||||
newAttributes.computeIfAbsent(attributeName, s -> EMPTY_VALUE);
|
||||
if (!isSupportedAttribute(attributeName) || newAttributes.containsKey(attributeName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<String> values = EMPTY_VALUE;
|
||||
AttributeMetadata metadata = metadataByAttribute.get(attributeName);
|
||||
|
||||
// if the attribute is not provided and does not have view permission, use the current values
|
||||
// this check makes possible to decide whether or not validation should happen for read-only attributes
|
||||
// when the context does not have access to such attributes
|
||||
if (user != null && !metadata.canView(createAttributeContext(metadata))) {
|
||||
values = user.getAttributes().get(attributeName);
|
||||
}
|
||||
|
||||
newAttributes.put(attributeName, values);
|
||||
}
|
||||
|
||||
if (user != null) {
|
||||
|
@ -287,7 +332,7 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
|||
}
|
||||
|
||||
// checks whether the attribute is a core attribute
|
||||
return UserModel.USERNAME.equals(name) || UserModel.EMAIL.equals(name) || UserModel.LAST_NAME.equals(name) || UserModel.FIRST_NAME.equals(name);
|
||||
return isRootAttribute(name);
|
||||
}
|
||||
|
||||
private boolean isReadOnlyInternalAttribute(String attributeName) {
|
||||
|
@ -298,10 +343,10 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
|||
return false;
|
||||
}
|
||||
|
||||
SimpleImmutableEntry<String, List<String>> attribute = createAttribute(attributeName);
|
||||
AttributeContext attributeContext = createAttributeContext(attributeName, readonlyMetadata);
|
||||
|
||||
for (AttributeValidatorMetadata validator : readonlyMetadata.getValidators()) {
|
||||
ValidationContext vc = validator.validate(createAttributeContext(attribute, readonlyMetadata));
|
||||
ValidationContext vc = validator.validate(attributeContext);
|
||||
if (!vc.isValid()) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import java.util.function.BiConsumer;
|
|||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
|
@ -43,22 +44,24 @@ public final class DefaultUserProfile implements UserProfile {
|
|||
|
||||
private final Function<Attributes, UserModel> userSupplier;
|
||||
private final Attributes attributes;
|
||||
private final KeycloakSession session;
|
||||
private boolean validated;
|
||||
private UserModel user;
|
||||
|
||||
public DefaultUserProfile(Attributes attributes, Function<Attributes, UserModel> userCreator, UserModel user) {
|
||||
public DefaultUserProfile(Attributes attributes, Function<Attributes, UserModel> userCreator, UserModel user,
|
||||
KeycloakSession session) {
|
||||
this.userSupplier = userCreator;
|
||||
this.attributes = attributes;
|
||||
this.user = user;
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() {
|
||||
ValidationException validationException = new ValidationException();
|
||||
ValidationException validationException = new ValidationException(session, user);
|
||||
|
||||
for (String attributeName : attributes.nameSet()) {
|
||||
this.attributes.validate(attributeName,
|
||||
(error) -> validationException.addError(error));
|
||||
this.attributes.validate(attributeName, validationException);
|
||||
}
|
||||
|
||||
if (validationException.hasError()) {
|
||||
|
@ -121,6 +124,7 @@ public final class DefaultUserProfile implements UserProfile {
|
|||
// the attribute map was sent.
|
||||
if (removeAttributes) {
|
||||
Set<String> attrsToRemove = new HashSet<>(user.getAttributes().keySet());
|
||||
|
||||
attrsToRemove.removeAll(attributes.nameSet());
|
||||
|
||||
for (String attr : attrsToRemove) {
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
*/
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.validate.ValidationContext;
|
||||
import org.keycloak.validate.Validator;
|
||||
|
||||
|
@ -46,4 +50,12 @@ public class UserProfileAttributeValidationContext extends ValidationContext {
|
|||
return attributeContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAttributes() {
|
||||
Map<String, Object> attributes = super.getAttributes();
|
||||
|
||||
attributes.put(UserModel.class.getName(), getAttributeContext().getUser());
|
||||
|
||||
return attributes;
|
||||
}
|
||||
}
|
|
@ -19,6 +19,9 @@
|
|||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import static org.keycloak.userprofile.AttributeMetadata.ALWAYS_FALSE;
|
||||
import static org.keycloak.userprofile.AttributeMetadata.ALWAYS_TRUE;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
@ -42,10 +45,6 @@ public final class UserProfileMetadata implements Cloneable {
|
|||
return attributes;
|
||||
}
|
||||
|
||||
public void addAttributes(AttributeMetadata... metadata) {
|
||||
addAttributes(Arrays.asList(metadata));
|
||||
}
|
||||
|
||||
public void addAttributes(List<AttributeMetadata> metadata) {
|
||||
if (attributes == null) {
|
||||
attributes = new ArrayList<>();
|
||||
|
@ -62,16 +61,20 @@ public final class UserProfileMetadata implements Cloneable {
|
|||
return addAttribute(name, Arrays.asList(validator));
|
||||
}
|
||||
|
||||
public AttributeMetadata addAttribute(String name, Predicate<AttributeContext> writeAllowed, Predicate<AttributeContext> readAllowed, AttributeValidatorMetadata... validator) {
|
||||
return addAttribute(new AttributeMetadata(name, ALWAYS_TRUE, writeAllowed, ALWAYS_TRUE, readAllowed).addValidator(Arrays.asList(validator)));
|
||||
}
|
||||
|
||||
public AttributeMetadata addAttribute(String name, Predicate<AttributeContext> writeAllowed, List<AttributeValidatorMetadata> validators) {
|
||||
return addAttribute(new AttributeMetadata(name, ALWAYS_TRUE, writeAllowed, ALWAYS_TRUE, ALWAYS_TRUE).addValidator(validators));
|
||||
}
|
||||
|
||||
public AttributeMetadata addAttribute(String name, List<AttributeValidatorMetadata> validators) {
|
||||
return addAttribute(new AttributeMetadata(name).addValidator(validators));
|
||||
}
|
||||
|
||||
public AttributeMetadata addAttribute(String name, List<AttributeValidatorMetadata> validator, Predicate<AttributeContext> required) {
|
||||
return addAttribute(new AttributeMetadata(name, AttributeMetadata.ALWAYS_FALSE, required).addValidator(validator));
|
||||
}
|
||||
|
||||
public AttributeMetadata addAttribute(String name, List<AttributeValidatorMetadata> validator, Predicate<AttributeContext> readOnly, Predicate<AttributeContext> required) {
|
||||
return addAttribute(new AttributeMetadata(name, readOnly, required).addValidator(validator));
|
||||
public AttributeMetadata addAttribute(String name, List<AttributeValidatorMetadata> validator, Predicate<AttributeContext> writeAllowed, Predicate<AttributeContext> required, Predicate<AttributeContext> readAllowed) {
|
||||
return addAttribute(new AttributeMetadata(name, ALWAYS_TRUE, writeAllowed, required, readAllowed).addValidator(validator));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -97,7 +100,7 @@ public final class UserProfileMetadata implements Cloneable {
|
|||
|
||||
//deeply clone AttributeMetadata so we can modify them (add validators etc)
|
||||
if (attributes != null) {
|
||||
metadata.addAttributes(attributes.stream().map((c)-> c.clone()).collect(Collectors.toList()));
|
||||
metadata.addAttributes(attributes.stream().map(AttributeMetadata::clone).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
return metadata;
|
||||
|
|
|
@ -26,6 +26,8 @@ import org.keycloak.provider.Spi;
|
|||
*/
|
||||
public class UserProfileSpi implements Spi {
|
||||
|
||||
public static final String ID = "userProfile";
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
|
@ -33,7 +35,7 @@ public class UserProfileSpi implements Spi {
|
|||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "userProfile";
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -19,22 +19,39 @@
|
|||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.keycloak.models.KeycloakContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.theme.Theme;
|
||||
import org.keycloak.validate.ValidationError;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public final class ValidationException extends RuntimeException {
|
||||
public final class ValidationException extends RuntimeException implements Consumer<ValidationError> {
|
||||
|
||||
private final Map<String, List<Error>> errors = new HashMap<>();
|
||||
private final BiFunction<String, Object[], String> messageFormatter;
|
||||
|
||||
public ValidationException(KeycloakSession session, UserModel user) {
|
||||
this.messageFormatter = new MessageFormatter(session, user);
|
||||
}
|
||||
|
||||
public List<Error> getErrors() {
|
||||
return errors.values().stream().reduce(new ArrayList<>(), (l, r) -> {
|
||||
|
@ -72,9 +89,14 @@ public final class ValidationException extends RuntimeException {
|
|||
return errors.values().stream().flatMap(Collection::stream).anyMatch(error -> names.contains(error.getAttribute()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(ValidationError error) {
|
||||
addError(error);
|
||||
}
|
||||
|
||||
void addError(ValidationError error) {
|
||||
List<Error> errors = this.errors.computeIfAbsent(error.getMessage(), (k) -> new ArrayList<>());
|
||||
errors.add(new Error(error));
|
||||
errors.add(new Error(error, messageFormatter));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -87,12 +109,25 @@ public final class ValidationException extends RuntimeException {
|
|||
return toString();
|
||||
}
|
||||
|
||||
public Response.Status getStatusCode() {
|
||||
for (Map.Entry<String, List<Error>> entry : errors.entrySet()) {
|
||||
for (Error error : entry.getValue()) {
|
||||
if (!Response.Status.BAD_REQUEST.equals(error.getStatusCode())) {
|
||||
return error.getStatusCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
return Response.Status.BAD_REQUEST;
|
||||
}
|
||||
|
||||
public static class Error implements Serializable {
|
||||
|
||||
private final ValidationError error;
|
||||
private final BiFunction<String, Object[], String> messageFormatter;
|
||||
|
||||
public Error(ValidationError error) {
|
||||
public Error(ValidationError error, BiFunction<String, Object[], String> messageFormatter) {
|
||||
this.error = error;
|
||||
this.messageFormatter = messageFormatter;
|
||||
}
|
||||
|
||||
public String getAttribute() {
|
||||
|
@ -104,7 +139,7 @@ public final class ValidationException extends RuntimeException {
|
|||
}
|
||||
|
||||
public Object[] getMessageParameters() {
|
||||
return error.getMessageParameters();
|
||||
return error.getInputHintWithMessageParameters();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -112,5 +147,40 @@ public final class ValidationException extends RuntimeException {
|
|||
return "Error [error=" + error + "]";
|
||||
}
|
||||
|
||||
public String getFormattedMessage() {
|
||||
return messageFormatter.apply(getMessage(), getMessageParameters());
|
||||
}
|
||||
|
||||
public Response.Status getStatusCode() {
|
||||
return error.getStatusCode();
|
||||
}
|
||||
}
|
||||
|
||||
private final class MessageFormatter implements BiFunction<String, Object[], String> {
|
||||
|
||||
private final Locale locale;
|
||||
private final Properties messages;
|
||||
|
||||
public MessageFormatter(KeycloakSession session, UserModel user) {
|
||||
try {
|
||||
KeycloakContext context = session.getContext();
|
||||
locale = context.resolveLocale(user);
|
||||
messages = getTheme(session).getMessages(locale);
|
||||
RealmModel realm = context.getRealm();
|
||||
Map<String, String> localizationTexts = realm.getRealmLocalizationTextsByLocale(locale.toLanguageTag());
|
||||
messages.putAll(localizationTexts);
|
||||
} catch (IOException cause) {
|
||||
throw new RuntimeException("Failed to configure error messages", cause);
|
||||
}
|
||||
}
|
||||
|
||||
private Theme getTheme(KeycloakSession session) throws IOException {
|
||||
return session.theme().getTheme(Theme.Type.ADMIN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String apply(String s, Object[] objects) {
|
||||
return new MessageFormat(messages.getProperty(s, s), locale).format(objects);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,10 +16,12 @@
|
|||
*/
|
||||
package org.keycloak.validate;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Denotes an error found during validation.
|
||||
|
@ -60,6 +62,14 @@ public class ValidationError implements Serializable {
|
|||
*/
|
||||
private final Object[] messageParameters;
|
||||
|
||||
/**
|
||||
* The status code associated with this error. This information serves as a hint so that
|
||||
* callers can choose whether they want to respect the status defined for the error.
|
||||
*
|
||||
* TODO: Should be better to refactor {@code Messages} to bing messages to status code as well as any other metadata that might be associated with the message.
|
||||
*/
|
||||
private Response.Status statusCode = Response.Status.BAD_REQUEST;
|
||||
|
||||
public ValidationError(String validatorId, String inputHint, String message) {
|
||||
this(validatorId, inputHint, message, EMPTY_PARAMETERS);
|
||||
}
|
||||
|
@ -145,4 +155,13 @@ public class ValidationError implements Serializable {
|
|||
public String toString() {
|
||||
return "ValidationError{" + "validatorId='" + validatorId + '\'' + ", inputHint='" + inputHint + '\'' + ", message='" + message + '\'' + ", messageParameters=" + Arrays.toString(messageParameters) + '}';
|
||||
}
|
||||
|
||||
public ValidationError setStatusCode(Response.Status statusCode) {
|
||||
this.statusCode = statusCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Response.Status getStatusCode() {
|
||||
return statusCode;
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@ import java.util.stream.Collectors;
|
|||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.validate.validators.LocalDateValidator;
|
||||
import org.keycloak.validate.validators.EmailValidator;
|
||||
import org.keycloak.validate.validators.IntegerValidator;
|
||||
import org.keycloak.validate.validators.LengthValidator;
|
||||
|
@ -154,6 +155,10 @@ public class Validators {
|
|||
return IntegerValidator.INSTANCE;
|
||||
}
|
||||
|
||||
public static LocalDateValidator dateValidator() {
|
||||
return LocalDateValidator.INSTANCE;
|
||||
}
|
||||
|
||||
public static ValidatorConfigValidator validatorConfigValidator() {
|
||||
return ValidatorConfigValidator.INSTANCE;
|
||||
}
|
||||
|
|
|
@ -16,10 +16,14 @@
|
|||
*/
|
||||
package org.keycloak.validate.validators;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.provider.ConfiguredProvider;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.utils.StringUtil;
|
||||
import org.keycloak.validate.AbstractSimpleValidator;
|
||||
import org.keycloak.validate.ValidationContext;
|
||||
|
@ -33,7 +37,7 @@ import org.keycloak.validate.ValidatorConfig;
|
|||
*
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*/
|
||||
public abstract class AbstractNumberValidator extends AbstractSimpleValidator {
|
||||
public abstract class AbstractNumberValidator extends AbstractSimpleValidator implements ConfiguredProvider {
|
||||
|
||||
public static final String MESSAGE_INVALID_NUMBER = "error-invalid-number";
|
||||
public static final String MESSAGE_NUMBER_OUT_OF_RANGE = "error-number-out-of-range";
|
||||
|
@ -43,6 +47,24 @@ public abstract class AbstractNumberValidator extends AbstractSimpleValidator {
|
|||
|
||||
private final ValidatorConfig defaultConfig;
|
||||
|
||||
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(KEY_MIN);
|
||||
property.setLabel("Minimum");
|
||||
property.setHelpText("The minimal allowed value - this config is optional.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(KEY_MAX);
|
||||
property.setLabel("Maximum");
|
||||
property.setHelpText("The maximal allowed value - this config is optional.");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
public AbstractNumberValidator() {
|
||||
// for reflection
|
||||
this(ValidatorConfig.EMPTY);
|
||||
|
@ -52,6 +74,10 @@ public abstract class AbstractNumberValidator extends AbstractSimpleValidator {
|
|||
this.defaultConfig = config;
|
||||
}
|
||||
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean skipValidation(Object value, ValidatorConfig config) {
|
||||
if (isIgnoreEmptyValuesConfigured(config) && (value == null || value instanceof String)) {
|
||||
|
@ -77,7 +103,7 @@ public abstract class AbstractNumberValidator extends AbstractSimpleValidator {
|
|||
}
|
||||
|
||||
if (number == null) {
|
||||
context.addError(new ValidationError(getId(), inputHint, MESSAGE_INVALID_NUMBER, value));
|
||||
context.addError(new ValidationError(getId(), inputHint, MESSAGE_INVALID_NUMBER));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -85,12 +111,12 @@ public abstract class AbstractNumberValidator extends AbstractSimpleValidator {
|
|||
Number max = getMinMaxConfig(config, KEY_MAX);
|
||||
|
||||
if (min != null && isFirstGreaterThanToSecond(min, number)) {
|
||||
context.addError(new ValidationError(getId(), inputHint, MESSAGE_NUMBER_OUT_OF_RANGE, value, min, max));
|
||||
context.addError(new ValidationError(getId(), inputHint, MESSAGE_NUMBER_OUT_OF_RANGE, min, max));
|
||||
return;
|
||||
}
|
||||
|
||||
if (max != null && isFirstGreaterThanToSecond(number, max)) {
|
||||
context.addError(new ValidationError(getId(), inputHint, MESSAGE_NUMBER_OUT_OF_RANGE, value, min, max));
|
||||
context.addError(new ValidationError(getId(), inputHint, MESSAGE_NUMBER_OUT_OF_RANGE, min, max));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
package org.keycloak.validate.validators;
|
||||
|
||||
import org.keycloak.provider.ConfiguredProvider;
|
||||
import org.keycloak.validate.ValidatorConfig;
|
||||
|
||||
/**
|
||||
|
@ -24,7 +25,7 @@ import org.keycloak.validate.ValidatorConfig;
|
|||
*
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*/
|
||||
public class DoubleValidator extends AbstractNumberValidator {
|
||||
public class DoubleValidator extends AbstractNumberValidator implements ConfiguredProvider {
|
||||
|
||||
public static final String ID = "double";
|
||||
|
||||
|
@ -60,4 +61,10 @@ public class DoubleValidator extends AbstractNumberValidator {
|
|||
protected boolean isFirstGreaterThanToSecond(Number n1, Number n2) {
|
||||
return n1.doubleValue() > n2.doubleValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Validator to check Double number format and optionally min and max values";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,8 +16,12 @@
|
|||
*/
|
||||
package org.keycloak.validate.validators;
|
||||
|
||||
import java.util.Collections;
|
||||
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;
|
||||
|
@ -27,7 +31,7 @@ import org.keycloak.validate.ValidatorConfig;
|
|||
* Email format validation - accepts plain string and collection of strings, for basic behavior like null/blank values
|
||||
* handling and collections support see {@link AbstractStringValidator}.
|
||||
*/
|
||||
public class EmailValidator extends AbstractStringValidator {
|
||||
public class EmailValidator extends AbstractStringValidator implements ConfiguredProvider {
|
||||
|
||||
public static final String ID = "email";
|
||||
|
||||
|
@ -38,9 +42,6 @@ public class EmailValidator extends AbstractStringValidator {
|
|||
// Actually allow same emails like angular. See ValidationTest.testEmailValidation()
|
||||
private static final Pattern EMAIL_PATTERN = Pattern.compile("[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*");
|
||||
|
||||
private EmailValidator() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
|
@ -52,4 +53,14 @@ public class EmailValidator extends AbstractStringValidator {
|
|||
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_EMAIL, value));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Email format validator";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
package org.keycloak.validate.validators;
|
||||
|
||||
import org.keycloak.provider.ConfiguredProvider;
|
||||
import org.keycloak.validate.ValidatorConfig;
|
||||
|
||||
/**
|
||||
|
@ -25,7 +26,7 @@ import org.keycloak.validate.ValidatorConfig;
|
|||
*
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*/
|
||||
public class IntegerValidator extends AbstractNumberValidator {
|
||||
public class IntegerValidator extends AbstractNumberValidator implements ConfiguredProvider {
|
||||
|
||||
public static final String ID = "integer";
|
||||
public static final IntegerValidator INSTANCE = new IntegerValidator();
|
||||
|
@ -60,4 +61,10 @@ public class IntegerValidator extends AbstractNumberValidator {
|
|||
protected boolean isFirstGreaterThanToSecond(Number n1, Number n2) {
|
||||
return n1.longValue() > n2.longValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Validator to check Integer number format and optionally min and max values";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,10 +16,14 @@
|
|||
*/
|
||||
package org.keycloak.validate.validators;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
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;
|
||||
|
@ -34,7 +38,7 @@ import org.keycloak.validate.ValidatorConfig;
|
|||
* <p>
|
||||
* Configuration have to be always provided, with at least one of {@link #KEY_MIN} and {@link #KEY_MAX}.
|
||||
*/
|
||||
public class LengthValidator extends AbstractStringValidator {
|
||||
public class LengthValidator extends AbstractStringValidator implements ConfiguredProvider {
|
||||
|
||||
public static final LengthValidator INSTANCE = new LengthValidator();
|
||||
|
||||
|
@ -46,7 +50,22 @@ public class LengthValidator extends AbstractStringValidator {
|
|||
public static final String KEY_MAX = "max";
|
||||
public static final String KEY_TRIM_DISABLED = "trim-disabled";
|
||||
|
||||
private LengthValidator() {
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(KEY_MIN);
|
||||
property.setLabel("Minimum length");
|
||||
property.setHelpText("The minimum length");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(KEY_MAX);
|
||||
property.setLabel("Maximum length");
|
||||
property.setHelpText("The maximum length");
|
||||
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||
configProperties.add(property);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -66,12 +85,12 @@ public class LengthValidator extends AbstractStringValidator {
|
|||
int length = value.length();
|
||||
|
||||
if (config.containsKey(KEY_MIN) && length < min.intValue()) {
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_LENGTH, value, min, max));
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_LENGTH, min, max));
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.containsKey(KEY_MAX) && length > max.intValue()) {
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_LENGTH, value, min, max));
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_LENGTH, min, max));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -113,4 +132,14 @@ public class LengthValidator extends AbstractStringValidator {
|
|||
}
|
||||
return new ValidationResult(errors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Length validator";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.validate.validators;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.keycloak.models.KeycloakContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ConfiguredProvider;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.utils.StringUtil;
|
||||
import org.keycloak.validate.AbstractStringValidator;
|
||||
import org.keycloak.validate.ValidationContext;
|
||||
import org.keycloak.validate.ValidationError;
|
||||
import org.keycloak.validate.ValidationResult;
|
||||
import org.keycloak.validate.ValidatorConfig;
|
||||
|
||||
/**
|
||||
* A date validator that only takes into account the format associated with the current locale.
|
||||
*/
|
||||
public class LocalDateValidator extends AbstractStringValidator implements ConfiguredProvider {
|
||||
|
||||
public static final LocalDateValidator INSTANCE = new LocalDateValidator();
|
||||
|
||||
public static final String ID = "local-date";
|
||||
|
||||
public static final String MESSAGE_INVALID_DATE = "error-invalid-date";
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doValidate(String value, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||
UserModel user = (UserModel) context.getAttributes().get(UserModel.class.getName());
|
||||
KeycloakSession session = context.getSession();
|
||||
KeycloakContext keycloakContext = session.getContext();
|
||||
Locale locale = keycloakContext.resolveLocale(user);
|
||||
DateFormat formatter = DateFormat.getDateInstance(DateFormat.SHORT, locale);
|
||||
|
||||
formatter.setLenient(false);
|
||||
|
||||
try {
|
||||
formatter.parse(value);
|
||||
} catch (ParseException e) {
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_DATE));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationResult validateConfig(KeycloakSession session, ValidatorConfig config) {
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Validates date formats based on the realm or user locale.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isIgnoreEmptyValuesConfigured(ValidatorConfig config) {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -33,15 +33,12 @@ import org.keycloak.validate.ValidatorConfig;
|
|||
*/
|
||||
public class NotBlankValidator implements SimpleValidator {
|
||||
|
||||
public static final String ID = "blank";
|
||||
public static final String ID = "not-blank";
|
||||
|
||||
public static final String MESSAGE_BLANK = "error-invalid-blank";
|
||||
|
||||
public static final NotBlankValidator INSTANCE = new NotBlankValidator();
|
||||
|
||||
private NotBlankValidator() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
|
|
|
@ -38,9 +38,6 @@ public class NotEmptyValidator implements SimpleValidator {
|
|||
|
||||
public static final String MESSAGE_ERROR_EMPTY = "error-empty";
|
||||
|
||||
private NotEmptyValidator() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
|
|
|
@ -16,12 +16,16 @@
|
|||
*/
|
||||
package org.keycloak.validate.validators;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
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;
|
||||
|
@ -32,7 +36,7 @@ import org.keycloak.validate.ValidatorConfig;
|
|||
* Validate String against configured RegEx pattern - accepts plain string and collection of strings, for basic behavior
|
||||
* like null/blank values handling and collections support see {@link AbstractStringValidator}.
|
||||
*/
|
||||
public class PatternValidator extends AbstractStringValidator {
|
||||
public class PatternValidator extends AbstractStringValidator implements ConfiguredProvider {
|
||||
|
||||
public static final String ID = "pattern";
|
||||
|
||||
|
@ -42,7 +46,16 @@ public class PatternValidator extends AbstractStringValidator {
|
|||
|
||||
public static final String MESSAGE_NO_MATCH = "error-pattern-no-match";
|
||||
|
||||
private PatternValidator() {
|
||||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
|
||||
|
||||
static {
|
||||
ProviderConfigProperty property;
|
||||
property = new ProviderConfigProperty();
|
||||
property.setName(KEY_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);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -55,7 +68,7 @@ public class PatternValidator extends AbstractStringValidator {
|
|||
Pattern pattern = config.getPattern(KEY_PATTERN);
|
||||
|
||||
if (!pattern.matcher(value).matches()) {
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_NO_MATCH, value, config.getString(KEY_PATTERN)));
|
||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_NO_MATCH, config.getString(KEY_PATTERN)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,4 +92,14 @@ public class PatternValidator extends AbstractStringValidator {
|
|||
return new ValidationResult(errors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "RegExp Pattern validator";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return configProperties;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
*/
|
||||
package org.keycloak.validate.validators;
|
||||
|
||||
import org.keycloak.provider.ConfiguredProvider;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.validate.SimpleValidator;
|
||||
import org.keycloak.validate.ValidationContext;
|
||||
import org.keycloak.validate.ValidationError;
|
||||
|
@ -28,13 +30,14 @@ import java.net.URL;
|
|||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* URI validation - accepts {@link URI}, {@link URL} and single String. Null input is valid, use other validators (like
|
||||
* {@link NotBlankValidator} or {@link NotEmptyValidator} to force field as required.
|
||||
*/
|
||||
public class UriValidator implements SimpleValidator {
|
||||
public class UriValidator implements SimpleValidator, ConfiguredProvider {
|
||||
|
||||
public static final UriValidator INSTANCE = new UriValidator();
|
||||
|
||||
|
@ -56,9 +59,6 @@ public class UriValidator implements SimpleValidator {
|
|||
|
||||
public static final String ID = "uri";
|
||||
|
||||
private UriValidator() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
|
@ -136,4 +136,14 @@ public class UriValidator implements SimpleValidator {
|
|||
|
||||
return valid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Uri Validator";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
org.keycloak.validate.validators.LengthValidator
|
||||
org.keycloak.validate.validators.NotEmptyValidator
|
||||
org.keycloak.validate.validators.UriValidator
|
||||
org.keycloak.validate.validators.EmailValidator
|
||||
org.keycloak.validate.validators.NotBlankValidator
|
||||
org.keycloak.validate.validators.PatternValidator
|
||||
org.keycloak.validate.validators.DoubleValidator
|
||||
org.keycloak.validate.validators.IntegerValidator
|
||||
org.keycloak.validate.validators.LocalDateValidator
|
|
@ -87,7 +87,7 @@ public class ValidatorTest {
|
|||
Assert.assertEquals(LengthValidator.ID, error.getValidatorId());
|
||||
Assert.assertEquals(inputHint, error.getInputHint());
|
||||
Assert.assertEquals(LengthValidator.MESSAGE_INVALID_LENGTH, error.getMessage());
|
||||
Assert.assertEquals(input, error.getMessageParameters()[0]);
|
||||
Assert.assertEquals(new Integer(2), error.getMessageParameters()[0]);
|
||||
|
||||
Assert.assertTrue(result.hasErrorsForValidatorId(LengthValidator.ID));
|
||||
Assert.assertFalse(result.hasErrorsForValidatorId("unknown"));
|
||||
|
|
|
@ -299,7 +299,8 @@ public interface UserModel extends RoleMapperModel {
|
|||
void setServiceAccountClientLink(String clientInternalId);
|
||||
|
||||
enum RequiredAction {
|
||||
VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, UPDATE_PASSWORD, TERMS_AND_CONDITIONS
|
||||
VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, UPDATE_PASSWORD, TERMS_AND_CONDITIONS,
|
||||
VERIFY_PROFILE
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* 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.authentication.requiredactions;
|
||||
|
||||
import javax.ws.rs.HttpMethod;
|
||||
import javax.ws.rs.core.MultivaluedHashMap;
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.authentication.DisplayTypeRequiredActionFactory;
|
||||
import org.keycloak.authentication.InitiatedActionSupport;
|
||||
import org.keycloak.authentication.RequiredActionContext;
|
||||
import org.keycloak.authentication.RequiredActionFactory;
|
||||
import org.keycloak.authentication.RequiredActionProvider;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.forms.login.LoginFormsProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.FormMessage;
|
||||
import org.keycloak.services.validation.Validation;
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
import org.keycloak.userprofile.ValidationException;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class VerifyUserProfile implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
|
||||
|
||||
@Override
|
||||
public InitiatedActionSupport initiatedActionSupport() {
|
||||
return InitiatedActionSupport.SUPPORTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evaluateTriggers(RequiredActionContext context) {
|
||||
UserModel user = context.getUser();
|
||||
UserProfileProvider provider = context.getSession().getProvider(UserProfileProvider.class);
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, user);
|
||||
|
||||
try {
|
||||
profile.validate();
|
||||
context.getAuthenticationSession().removeRequiredAction(getId());
|
||||
user.removeRequiredAction(getId());
|
||||
} catch (ValidationException e) {
|
||||
context.getAuthenticationSession().addRequiredAction(getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requiredActionChallenge(RequiredActionContext context) {
|
||||
UserProfileProvider provider = context.getSession().getProvider(UserProfileProvider.class);
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, context.getUser());
|
||||
|
||||
try {
|
||||
profile.validate();
|
||||
context.success();
|
||||
} catch (ValidationException ve) {
|
||||
List<FormMessage> errors = Validation.getFormErrorsFromValidation(ve.getErrors());
|
||||
MultivaluedMap<String, String> parameters;
|
||||
|
||||
if (context.getHttpRequest().getHttpMethod().equalsIgnoreCase(HttpMethod.GET)) {
|
||||
parameters = new MultivaluedHashMap<>();
|
||||
} else {
|
||||
parameters = context.getHttpRequest().getDecodedFormParameters();
|
||||
}
|
||||
|
||||
context.challenge(createResponse(context, profile, parameters, errors));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processAction(RequiredActionContext context) {
|
||||
EventBuilder event = context.getEvent();
|
||||
event.event(EventType.VERIFY_PROFILE);
|
||||
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||
|
||||
if (!context.getRealm().isEditUsernameAllowed()) {
|
||||
formData.putSingle(UserModel.USERNAME, context.getUser().getUsername());
|
||||
}
|
||||
|
||||
UserProfileProvider provider = context.getSession().getProvider(UserProfileProvider.class);
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, formData, context.getUser());
|
||||
|
||||
try {
|
||||
profile.update();
|
||||
context.success();
|
||||
} catch (ValidationException ve) {
|
||||
List<FormMessage> errors = Validation.getFormErrorsFromValidation(ve.getErrors());
|
||||
context.challenge(createResponse(context, profile, formData, errors));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequiredActionProvider create(KeycloakSession session) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
|
||||
if (displayType == null) return this;
|
||||
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
|
||||
return ConsoleUpdateProfile.SINGLETON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayText() {
|
||||
return "Verify Profile";
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return UserModel.RequiredAction.VERIFY_PROFILE.name();
|
||||
}
|
||||
|
||||
private Response createResponse(RequiredActionContext context, UserProfile profile,
|
||||
MultivaluedMap<String, String> formData, List<FormMessage> errors) {
|
||||
LoginFormsProvider form = context.form();
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
form.setErrors(errors);
|
||||
}
|
||||
|
||||
return form.setFormData(formData)
|
||||
.createResponse(UserModel.RequiredAction.VERIFY_PROFILE);
|
||||
}
|
||||
}
|
|
@ -40,6 +40,7 @@ import org.keycloak.forms.login.freemarker.model.SAMLPostFormBean;
|
|||
import org.keycloak.forms.login.freemarker.model.TotpBean;
|
||||
import org.keycloak.forms.login.freemarker.model.TotpLoginBean;
|
||||
import org.keycloak.forms.login.freemarker.model.UrlBean;
|
||||
import org.keycloak.forms.login.freemarker.model.VerifyProfileBean;
|
||||
import org.keycloak.forms.login.freemarker.model.X509ConfirmBean;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
|
@ -159,6 +160,13 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
actionMessage = Messages.VERIFY_EMAIL;
|
||||
page = LoginFormsPages.LOGIN_VERIFY_EMAIL;
|
||||
break;
|
||||
case VERIFY_PROFILE:
|
||||
UpdateProfileContext verifyProfile = new UserUpdateProfileContext(realm, user);
|
||||
this.attributes.put(UPDATE_PROFILE_CONTEXT_ATTR, verifyProfile);
|
||||
|
||||
actionMessage = Messages.UPDATE_PROFILE;
|
||||
page = LoginFormsPages.VERIFY_PROFILE;
|
||||
break;
|
||||
default:
|
||||
return Response.serverError().build();
|
||||
}
|
||||
|
@ -238,6 +246,9 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
case SAML_POST_FORM:
|
||||
attributes.put("samlPost", new SAMLPostFormBean(formData));
|
||||
break;
|
||||
case VERIFY_PROFILE:
|
||||
attributes.put("profile", new VerifyProfileBean(user, formData, session));
|
||||
break;
|
||||
}
|
||||
|
||||
return processTemplate(theme, Templates.getTemplate(page), locale);
|
||||
|
|
|
@ -72,6 +72,8 @@ public class Templates {
|
|||
return "login-x509-info.ftl";
|
||||
case SAML_POST_FORM:
|
||||
return "saml-post-form.ftl";
|
||||
case VERIFY_PROFILE:
|
||||
return "verify-profile.ftl";
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
package org.keycloak.forms.login.freemarker.model;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.userprofile.AttributeMetadata;
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class VerifyProfileBean {
|
||||
|
||||
private final UserModel user;
|
||||
private final MultivaluedMap<String, String> formData;
|
||||
private final List<Attribute> attributes;
|
||||
private final UserProfile profile;
|
||||
|
||||
public VerifyProfileBean(UserModel user, MultivaluedMap<String, String> formData, KeycloakSession session) {
|
||||
this.user = user;
|
||||
this.formData = formData;
|
||||
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||
this.profile = provider.create(UserProfileContext.UPDATE_PROFILE, user);
|
||||
this.attributes = toAttributes(profile.getAttributes().getReadable());
|
||||
|
||||
}
|
||||
|
||||
public List<Attribute> getAttributes() {
|
||||
return attributes;
|
||||
}
|
||||
|
||||
public List<Attribute> getAllAttributes() {
|
||||
return toAttributes(profile.getAttributes().toMap());
|
||||
}
|
||||
|
||||
private List<Attribute> toAttributes(Map<String, List<String>> readable) {
|
||||
return readable.keySet().stream()
|
||||
.map(name -> profile.getAttributes().getMetadata(name)).map(Attribute::new)
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public class Attribute implements Comparable<Attribute> {
|
||||
|
||||
private final AttributeMetadata metadata;
|
||||
|
||||
public Attribute(AttributeMetadata metadata) {
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return metadata.getName();
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return formData.getOrDefault(getName(), singletonList(user.getFirstAttribute(getName()))).get(0);
|
||||
}
|
||||
|
||||
public boolean isRequired() {
|
||||
return profile.getAttributes().isRequired(getName());
|
||||
}
|
||||
|
||||
public boolean isReadOnly() {
|
||||
return profile.getAttributes().isReadOnly(getName());
|
||||
}
|
||||
|
||||
public Map<String, Object> getAnnotations() {
|
||||
Map<String, Object> annotations = metadata.getAnnotations();
|
||||
|
||||
if (annotations == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
return annotations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Attribute o) {
|
||||
return getName().compareTo(o.getName());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ import org.keycloak.representations.idm.ErrorRepresentation;
|
|||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
|
@ -42,4 +43,12 @@ public class ErrorResponse {
|
|||
return Response.status(status).entity(error).type(MediaType.APPLICATION_JSON).build();
|
||||
}
|
||||
|
||||
public static Response errors(List<ErrorRepresentation> s, Response.Status status) {
|
||||
if (s.size() == 1) {
|
||||
return Response.status(status).entity(s.get(0)).type(MediaType.APPLICATION_JSON).build();
|
||||
}
|
||||
ErrorRepresentation error = new ErrorRepresentation();
|
||||
error.setErrors(s);
|
||||
return Response.status(status).entity(error).type(MediaType.APPLICATION_JSON).build();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ import org.keycloak.representations.account.ClientRepresentation;
|
|||
import org.keycloak.representations.account.ConsentRepresentation;
|
||||
import org.keycloak.representations.account.ConsentScopeRepresentation;
|
||||
import org.keycloak.representations.account.UserRepresentation;
|
||||
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||
import org.keycloak.services.ErrorResponse;
|
||||
import org.keycloak.services.managers.Auth;
|
||||
import org.keycloak.services.managers.UserConsentManager;
|
||||
|
@ -47,6 +48,7 @@ import org.keycloak.storage.ReadOnlyException;
|
|||
import org.keycloak.theme.Theme;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.userprofile.ValidationException;
|
||||
import org.keycloak.userprofile.ValidationException.Error;
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
|
||||
|
@ -65,6 +67,7 @@ import javax.ws.rs.core.HttpHeaders;
|
|||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
|
@ -136,13 +139,11 @@ public class AccountRestService {
|
|||
rep.setEmail(user.getEmail());
|
||||
rep.setEmailVerified(user.isEmailVerified());
|
||||
rep.setEmailVerified(user.isEmailVerified());
|
||||
Map<String, List<String>> attributes = user.getAttributes();
|
||||
Map<String, List<String>> copiedAttributes = new HashMap<>(attributes);
|
||||
copiedAttributes.remove(UserModel.FIRST_NAME);
|
||||
copiedAttributes.remove(UserModel.LAST_NAME);
|
||||
copiedAttributes.remove(UserModel.EMAIL);
|
||||
copiedAttributes.remove(UserModel.USERNAME);
|
||||
rep.setAttributes(copiedAttributes);
|
||||
|
||||
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||
UserProfile profile = provider.create(UserProfileContext.ACCOUNT, user);
|
||||
|
||||
rep.setAttributes(profile.getAttributes().getReadable(false));
|
||||
|
||||
return rep;
|
||||
}
|
||||
|
@ -167,21 +168,36 @@ public class AccountRestService {
|
|||
|
||||
return Response.noContent().build();
|
||||
} catch (ValidationException pve) {
|
||||
if (pve.hasError(Messages.READ_ONLY_USERNAME))
|
||||
return ErrorResponse.error(Messages.READ_ONLY_USERNAME, Response.Status.BAD_REQUEST);
|
||||
if (pve.hasError(Messages.USERNAME_EXISTS))
|
||||
return ErrorResponse.exists(Messages.USERNAME_EXISTS);
|
||||
if (pve.hasError(Messages.EMAIL_EXISTS))
|
||||
return ErrorResponse.exists(Messages.EMAIL_EXISTS);
|
||||
|
||||
// Here should be possibility to somehow return all errors?
|
||||
String firstErrorMessage = pve.getErrors().get(0).getMessage();
|
||||
return ErrorResponse.error(firstErrorMessage, Response.Status.BAD_REQUEST);
|
||||
List<ErrorRepresentation> errors = new ArrayList<>();
|
||||
for(Error err: pve.getErrors()) {
|
||||
errors.add(new ErrorRepresentation(err.getAttribute(), err.getMessage(), validationErrorParamsToString(err.getMessageParameters())));
|
||||
}
|
||||
return ErrorResponse.errors(errors, pve.getStatusCode());
|
||||
} catch (ReadOnlyException e) {
|
||||
return ErrorResponse.error(Messages.READ_ONLY_USER, Response.Status.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
private String[] validationErrorParamsToString(Object[] messageParameters) {
|
||||
if(messageParameters == null)
|
||||
return null;
|
||||
String[] ret = new String[messageParameters.length];
|
||||
int i = 0;
|
||||
for(Object p: messageParameters) {
|
||||
if(p != null) {
|
||||
//first parameter is field name, we add replacer code so it is localized in React UI
|
||||
if(i==0) {
|
||||
ret[i++] = "${"+p.toString()+"}";
|
||||
} else {
|
||||
ret[i++] = p.toString();
|
||||
}
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session information.
|
||||
*
|
||||
|
|
|
@ -16,28 +16,23 @@
|
|||
*/
|
||||
package org.keycloak.services.resources.admin;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
|
||||
import org.keycloak.component.ComponentValidationException;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.services.ErrorResponse;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*
|
||||
*/
|
||||
public class UserProfileResource {
|
||||
|
||||
|
@ -52,31 +47,24 @@ public class UserProfileResource {
|
|||
this.auth = auth;
|
||||
}
|
||||
|
||||
|
||||
@GET
|
||||
@Path("configuration")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public String getConfiguration() {
|
||||
|
||||
auth.realm().requireViewRealm();
|
||||
|
||||
UserProfileProvider t = session.getProvider(UserProfileProvider.class);
|
||||
return t.getConfiguration();
|
||||
return session.getProvider(UserProfileProvider.class).getConfiguration();
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("configuration")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public Response updateConfiguration(String text) throws IOException {
|
||||
|
||||
public Response update(String text) {
|
||||
auth.realm().requireManageRealm();
|
||||
|
||||
UserProfileProvider t = session.getProvider(UserProfileProvider.class);
|
||||
|
||||
try {
|
||||
t.setConfiguration(text);
|
||||
} catch (ComponentValidationException e) {
|
||||
//show validation result containing details about error
|
||||
return Response.status(Status.BAD_REQUEST).type(MediaType.TEXT_PLAIN).entity(e.getMessage()).build();
|
||||
return ErrorResponse.error(e.getMessage(), Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
return Response.ok(t.getConfiguration()).type(MediaType.APPLICATION_JSON).build();
|
||||
|
|
|
@ -54,6 +54,7 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
|||
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
|
||||
import org.keycloak.representations.idm.GroupRepresentation;
|
||||
import org.keycloak.representations.idm.UserConsentRepresentation;
|
||||
|
@ -98,6 +99,7 @@ import javax.ws.rs.core.Response.Status;
|
|||
import javax.ws.rs.core.UriBuilder;
|
||||
import java.net.URI;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
|
@ -171,7 +173,7 @@ public class UserResource {
|
|||
|
||||
UserProfile profile = session.getProvider(UserProfileProvider.class).create(USER_API, rep.toAttributes(), user);
|
||||
|
||||
Response response = validateUserProfile(profile);
|
||||
Response response = validateUserProfile(profile, user, session);
|
||||
if (response != null) {
|
||||
return response;
|
||||
}
|
||||
|
@ -205,18 +207,17 @@ public class UserResource {
|
|||
}
|
||||
}
|
||||
|
||||
public static Response validateUserProfile(UserProfile profile) {
|
||||
public static Response validateUserProfile(UserProfile profile, UserModel user, KeycloakSession session) {
|
||||
try {
|
||||
profile.validate();
|
||||
} catch (ValidationException pve) {
|
||||
List<ErrorRepresentation> errors = new ArrayList<>();
|
||||
|
||||
for (ValidationException.Error error : pve.getErrors()) {
|
||||
StringBuilder s = new StringBuilder("Failed to update attribute " + error.getAttribute() + ": ");
|
||||
|
||||
s.append(error.getMessage()).append(", ");
|
||||
|
||||
logger.warn(s);
|
||||
errors.add(new ErrorRepresentation(error.getFormattedMessage()));
|
||||
}
|
||||
return ErrorResponse.error("Could not update user! See server log for more details", Response.Status.BAD_REQUEST);
|
||||
|
||||
return ErrorResponse.errors(errors, Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -281,6 +282,15 @@ public class UserResource {
|
|||
}
|
||||
rep.setAccess(auth.users().getAccess(user));
|
||||
|
||||
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||
UserProfile profile = provider.create(USER_API, user);
|
||||
|
||||
Map<String, List<String>> attributes = profile.getAttributes().getReadable(false);
|
||||
|
||||
if (!attributes.isEmpty()) {
|
||||
rep.setAttributes(attributes);
|
||||
}
|
||||
|
||||
return rep;
|
||||
}
|
||||
|
||||
|
|
|
@ -155,7 +155,7 @@ public class UsersResource {
|
|||
UserProfile profile = profileProvider.create(USER_API, rep.toAttributes());
|
||||
|
||||
try {
|
||||
Response response = UserResource.validateUserProfile(profile);
|
||||
Response response = UserResource.validateUserProfile(profile, null, session);
|
||||
if (response != null) {
|
||||
return response;
|
||||
}
|
||||
|
@ -385,6 +385,19 @@ public class UsersResource {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get representation of the user
|
||||
*
|
||||
* @param id User id
|
||||
* @return
|
||||
*/
|
||||
@Path("profile")
|
||||
public UserProfileResource userProfile() {
|
||||
UserProfileResource resource = new UserProfileResource(realm, auth);
|
||||
ResteasyProviderFactory.getInstance().injectProperties(resource);
|
||||
return resource;
|
||||
}
|
||||
|
||||
private Stream<UserRepresentation> searchForUser(Map<String, String> attributes, RealmModel realm, UserPermissionEvaluator usersEvaluator, Boolean briefRepresentation, Integer firstResult, Integer maxResults, Boolean includeServiceAccounts) {
|
||||
session.setAttribute(UserModel.INCLUDE_SERVICE_ACCOUNT, includeServiceAccounts);
|
||||
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
package org.keycloak.userprofile.config;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.userprofile.AttributeMetadata;
|
||||
import org.keycloak.userprofile.DefaultAttributes;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.userprofile.UserProfileMetadata;
|
||||
|
||||
/**
|
||||
* Temporary implementation of {@link org.keycloak.userprofile.Attributes}. It should be removed once
|
||||
* the {@link DeclarativeUserProfileProvider} becomes the default.
|
||||
*
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class DeclarativeAttributes extends DefaultAttributes {
|
||||
|
||||
public DeclarativeAttributes(UserProfileContext context, Map<String, ?> attributes,
|
||||
UserModel user, UserProfileMetadata profileMetadata,
|
||||
KeycloakSession session) {
|
||||
super(context, attributes, user, profileMetadata, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> getReadable() {
|
||||
Map<String, List<String>> attributes = new HashMap<>(this);
|
||||
|
||||
for (String name : nameSet()) {
|
||||
AttributeMetadata metadata = getMetadata(name);
|
||||
|
||||
if (metadata == null || !metadata.canView(createAttributeContext(metadata))) {
|
||||
attributes.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@
|
|||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.testsuite.user.profile.config;
|
||||
package org.keycloak.userprofile.config;
|
||||
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
|
@ -17,10 +17,11 @@
|
|||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.testsuite.user.profile.config;
|
||||
package org.keycloak.userprofile.config;
|
||||
|
||||
import static org.keycloak.common.util.ObjectUtil.isBlank;
|
||||
import static org.keycloak.testsuite.user.profile.config.UPConfigUtils.readConfig;
|
||||
import static org.keycloak.protocol.oidc.TokenManager.getRequestedClientScopes;
|
||||
import static org.keycloak.userprofile.config.UPConfigUtils.readConfig;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
|
@ -31,30 +32,33 @@ import java.util.Collections;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.StreamUtil;
|
||||
import org.keycloak.component.AmphibianProviderFactory;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.component.ComponentValidationException;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.userprofile.AttributeContext;
|
||||
import org.keycloak.userprofile.AttributeMetadata;
|
||||
import org.keycloak.userprofile.AttributeValidatorMetadata;
|
||||
import org.keycloak.userprofile.Attributes;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.userprofile.UserProfileMetadata;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
import org.keycloak.userprofile.legacy.AbstractUserProfileProvider;
|
||||
import org.keycloak.userprofile.validator.AttributeRequiredByMetadataValidator;
|
||||
import org.keycloak.userprofile.validator.ImmutableAttributeValidator;
|
||||
import org.keycloak.validate.AbstractSimpleValidator;
|
||||
import org.keycloak.validate.ValidatorConfig;
|
||||
|
||||
|
@ -65,13 +69,28 @@ import org.keycloak.validate.ValidatorConfig;
|
|||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*/
|
||||
public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<DeclarativeUserProfileProvider> implements AmphibianProviderFactory<DeclarativeUserProfileProvider> {
|
||||
public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<DeclarativeUserProfileProvider>
|
||||
implements AmphibianProviderFactory<DeclarativeUserProfileProvider>, EnvironmentDependentProviderFactory {
|
||||
|
||||
public static final String ID = "declarative-userprofile-provider";
|
||||
public static final String SYSTEM_DEFAULT_CONFIG_RESOURCE = "keycloak-default-user-profile.json";
|
||||
public static final String ID = "declarative-user-profile";
|
||||
public static final String UP_PIECES_COUNT_COMPONENT_CONFIG_KEY = "config-pieces-count";
|
||||
private static final String PARSED_CONFIG_COMPONENT_KEY = "kc.user.profile.metadata";
|
||||
private static final String UP_PIECE_COMPONENT_CONFIG_KEY_BASE = "config-piece-";
|
||||
private static final String SYSTEM_DEFAULT_CONFIG_RESOURCE = "keycloak-default-user-profile.json";
|
||||
|
||||
private static boolean createRequiredForScopePredicate(AttributeContext context, List<String> requiredScopes) {
|
||||
KeycloakSession session = context.getSession();
|
||||
AuthenticationSessionModel authenticationSession = session.getContext().getAuthenticationSession();
|
||||
|
||||
if (authenticationSession == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String requestedScopesString = authenticationSession.getClientNote(OIDCLoginProtocol.SCOPE_PARAM);
|
||||
ClientModel client = authenticationSession.getClient();
|
||||
|
||||
return getRequestedClientScopes(requestedScopesString, client).map((csm) -> csm.getName()).anyMatch(requiredScopes::contains);
|
||||
}
|
||||
|
||||
private String defaultRawConfig;
|
||||
|
||||
|
@ -79,8 +98,9 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
|||
// for reflection
|
||||
}
|
||||
|
||||
public DeclarativeUserProfileProvider(KeycloakSession session, Map<UserProfileContext, UserProfileMetadata> metadataRegistry) {
|
||||
public DeclarativeUserProfileProvider(KeycloakSession session, Map<UserProfileContext, UserProfileMetadata> metadataRegistry, String defaultRawConfig) {
|
||||
super(session, metadataRegistry);
|
||||
this.defaultRawConfig = defaultRawConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -90,7 +110,13 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
|||
|
||||
@Override
|
||||
protected DeclarativeUserProfileProvider create(KeycloakSession session, Map<UserProfileContext, UserProfileMetadata> metadataRegistry) {
|
||||
return new DeclarativeUserProfileProvider(session, metadataRegistry);
|
||||
return new DeclarativeUserProfileProvider(session, metadataRegistry, defaultRawConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Attributes createAttributes(UserProfileContext context, Map<String, ?> attributes,
|
||||
UserModel user, UserProfileMetadata metadata) {
|
||||
return new DeclarativeAttributes(context, attributes, user, metadata, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -122,10 +148,10 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
|||
List<String> errors = UPConfigUtils.validate(session, upc);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
throw new ComponentValidationException("UserProfile configuration is invalid: " + errors.toString());
|
||||
throw new ComponentValidationException(errors.toString());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ComponentValidationException("UserProfile configuration is invalid due to JSON parsing error: " + e.getMessage(), e);
|
||||
throw new ComponentValidationException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -202,7 +228,8 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
|||
UserProfileContext context = metadata.getContext();
|
||||
UPConfig parsedConfig = getParsedConfig(model);
|
||||
|
||||
if (parsedConfig == null) {
|
||||
// do not change config for REGISTRATION_USER_CREATION context, everything important is covered thanks to REGISTRATION_PROFILE
|
||||
if (parsedConfig == null || context == UserProfileContext.REGISTRATION_USER_CREATION) {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
|
@ -227,46 +254,58 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
|||
if (rc != null && !(UserModel.USERNAME.equals(attributeName) || UserModel.EMAIL.equals(attributeName))) {
|
||||
// do not take requirements from config for username and email as they are
|
||||
// driven by business logic from parent!
|
||||
|
||||
if (rc.isAlways() || UPConfigUtils.isRoleForContext(context, rc.getRoles())) {
|
||||
validators.add(createRequiredValidator(attrConfig));
|
||||
required = AttributeMetadata.ALWAYS_TRUE;
|
||||
} else if (UPConfigUtils.canBeAuthFlowContext(context) && rc.getScopes() != null && !rc.getScopes().isEmpty()) {
|
||||
// for contexts executed from auth flow and with configured scopes requirement
|
||||
// we have to create required validation with scopes based selector
|
||||
required = (c) -> attributePredicateAuthFlowRequestedScope(rc.getScopes());
|
||||
validators.add(createRequiredValidator(attrConfig));
|
||||
}
|
||||
required = (c) -> createRequiredForScopePredicate(c, rc.getScopes());
|
||||
}
|
||||
|
||||
Predicate<AttributeContext> readOnly = AttributeMetadata.ALWAYS_FALSE;
|
||||
validators.add(new AttributeValidatorMetadata(AttributeRequiredByMetadataValidator.ID));
|
||||
}
|
||||
|
||||
Predicate<AttributeContext> writeAllowed = AttributeMetadata.ALWAYS_FALSE;
|
||||
Predicate<AttributeContext> readAllowed = AttributeMetadata.ALWAYS_FALSE;
|
||||
UPAttributePermissions permissions = attrConfig.getPermissions();
|
||||
|
||||
if (permissions != null) {
|
||||
List<String> editRoles = permissions.getEdit();
|
||||
|
||||
if (editRoles != null && !editRoles.isEmpty()) {
|
||||
readOnly = ac -> !UPConfigUtils.isRoleForContext(ac.getContext(), editRoles);
|
||||
if (!editRoles.isEmpty()) {
|
||||
writeAllowed = ac -> UPConfigUtils.isRoleForContext(ac.getContext(), editRoles);
|
||||
}
|
||||
|
||||
List<String> viewRoles = permissions.getView();
|
||||
|
||||
if (viewRoles.isEmpty()) {
|
||||
readAllowed = writeAllowed;
|
||||
} else {
|
||||
readAllowed = createViewAllowedPredicate(writeAllowed, viewRoles);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> annotations = attrConfig.getAnnotations();
|
||||
|
||||
if (UserModel.USERNAME.equals(attributeName) || UserModel.EMAIL.equals(attributeName)) {
|
||||
// add format validators for special attributes which may exist from parent
|
||||
if (!validators.isEmpty()) {
|
||||
if (permissions == null) {
|
||||
writeAllowed = AttributeMetadata.ALWAYS_TRUE;
|
||||
}
|
||||
|
||||
List<AttributeMetadata> atts = decoratedMetadata.getAttribute(attributeName);
|
||||
|
||||
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.
|
||||
decoratedMetadata.addAttribute(attributeName, validators, readOnly).addAnnotations(annotations);
|
||||
decoratedMetadata.addAttribute(attributeName, writeAllowed, validators).addAnnotations(annotations);
|
||||
} else {
|
||||
// only add configured validators and annotations if attribute metadata exist
|
||||
atts.stream().forEach(c -> c.addValidator(validators).addAnnotations(annotations));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
decoratedMetadata.addAttribute(attributeName, validators, readOnly, required).addAnnotations(annotations);
|
||||
// always add validation for imuttable/read-only attributes
|
||||
validators.add(new AttributeValidatorMetadata(ImmutableAttributeValidator.ID));
|
||||
decoratedMetadata.addAttribute(attributeName, validators, writeAllowed, required, readAllowed).addAnnotations(annotations);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -274,6 +313,11 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
|||
|
||||
}
|
||||
|
||||
private Predicate<AttributeContext> createViewAllowedPredicate(Predicate<AttributeContext> canEdit,
|
||||
List<String> viewRoles) {
|
||||
return ac -> UPConfigUtils.isRoleForContext(ac.getContext(), viewRoles) || canEdit.test(ac);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parsed config file configured in model. Default one used if not configured.
|
||||
*
|
||||
|
@ -302,30 +346,6 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Predicate to select attributes for Authentication flow cases where requested scopes (including configured Default
|
||||
* client scopes) are compared to set of scopes from user profile configuration.
|
||||
* <p>
|
||||
* This patches problem with some auth flows (eg. register) where authSession.getClientScopes() doesn't work
|
||||
* correctly!
|
||||
*
|
||||
* @param scopesConfigured to match
|
||||
* @return true if at least one requested scope matches at least one configured scope
|
||||
*/
|
||||
private boolean attributePredicateAuthFlowRequestedScope(List<String> scopesConfigured) {
|
||||
// never match out of auth flow
|
||||
if (session.getContext().getAuthenticationSession() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getAuthFlowRequestedScopeNames().stream().anyMatch(scopesConfigured::contains);
|
||||
}
|
||||
|
||||
private Set<String> getAuthFlowRequestedScopeNames() {
|
||||
String requestedScopesString = session.getContext().getAuthenticationSession().getClientNote(OIDCLoginProtocol.SCOPE_PARAM);
|
||||
return TokenManager.getRequestedClientScopes(requestedScopesString, session.getContext().getAuthenticationSession().getClient()).map((csm) -> csm.getName()).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get componenet to store our "per realm" configuration into.
|
||||
*
|
||||
|
@ -337,15 +357,6 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
|||
return realm.getComponentsStream(realm.getId(), UserProfileProvider.class.getName()).findAny().orElseGet(() -> realm.addComponentModel(new DeclarativeUserProfileModel()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create validator for 'required' validation.
|
||||
*
|
||||
* @return validator metadata to run given validation
|
||||
*/
|
||||
protected AttributeValidatorMetadata createRequiredValidator(UPAttribute attrConfig) {
|
||||
return new AttributeValidatorMetadata(AttributeRequiredByMetadataValidator.ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create validator for validation configured in the user profile config.
|
||||
*
|
||||
|
@ -363,7 +374,7 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
|||
|
||||
int count = model.get(UP_PIECES_COUNT_COMPONENT_CONFIG_KEY, 0);
|
||||
if (count < 1) {
|
||||
return null;
|
||||
return defaultRawConfig;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
@ -390,4 +401,9 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
|||
}
|
||||
model.getConfig().remove(UP_PIECES_COUNT_COMPONENT_CONFIG_KEY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return Profile.isFeatureEnabled(Profile.Feature.DECLARATIVE_USER_PROFILE);
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.testsuite.user.profile.config;
|
||||
package org.keycloak.userprofile.config;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
|
@ -14,8 +14,9 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.testsuite.user.profile.config;
|
||||
package org.keycloak.userprofile.config;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
|
@ -26,8 +27,8 @@ import java.util.List;
|
|||
*/
|
||||
public class UPAttributePermissions {
|
||||
|
||||
private List<String> view;
|
||||
private List<String> edit;
|
||||
private List<String> view = Collections.emptyList();
|
||||
private List<String> edit = Collections.emptyList();
|
||||
|
||||
public List<String> getView() {
|
||||
return view;
|
|
@ -14,7 +14,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.testsuite.user.profile.config;
|
||||
package org.keycloak.userprofile.config;
|
||||
|
||||
import java.util.List;
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.testsuite.user.profile.config;
|
||||
package org.keycloak.userprofile.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
|
@ -14,7 +14,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.keycloak.testsuite.user.profile.config;
|
||||
package org.keycloak.userprofile.config;
|
||||
|
||||
import static org.keycloak.common.util.ObjectUtil.isBlank;
|
||||
|
||||
|
@ -106,7 +106,7 @@ public class UPConfigUtils {
|
|||
errors.add("Attribute configuration without 'name' is not allowed");
|
||||
} else {
|
||||
if (attNamesCache.contains(attributeName)) {
|
||||
errors.add("Duplicit attribute configuration with 'name':'" + attributeName + "'");
|
||||
errors.add("Attribute configuration already exists with 'name':'" + attributeName + "'");
|
||||
} else {
|
||||
attNamesCache.add(attributeName);
|
||||
if(!isValidAttributeName(attributeName)) {
|
||||
|
@ -134,7 +134,7 @@ public class UPConfigUtils {
|
|||
* @param attributeName to validate
|
||||
* @return
|
||||
*/
|
||||
static boolean isValidAttributeName(String attributeName) {
|
||||
public static boolean isValidAttributeName(String attributeName) {
|
||||
return Pattern.matches("[a-zA-Z0-9\\._\\-]+", attributeName);
|
||||
}
|
||||
|
|
@ -38,10 +38,14 @@ import java.util.regex.Pattern;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.userprofile.AttributeContext;
|
||||
import org.keycloak.userprofile.AttributeMetadata;
|
||||
import org.keycloak.userprofile.AttributeValidatorMetadata;
|
||||
import org.keycloak.userprofile.Attributes;
|
||||
import org.keycloak.userprofile.DefaultAttributes;
|
||||
|
@ -72,6 +76,13 @@ import org.keycloak.validate.validators.EmailValidator;
|
|||
*/
|
||||
public abstract class AbstractUserProfileProvider<U extends UserProfileProvider> implements UserProfileProvider, UserProfileProviderFactory<U> {
|
||||
|
||||
private static boolean editUsernameCondition(AttributeContext c) {
|
||||
KeycloakSession session = c.getSession();
|
||||
KeycloakContext context = session.getContext();
|
||||
RealmModel realm = context.getRealm();
|
||||
return realm.isEditUsernameAllowed();
|
||||
}
|
||||
|
||||
public static Pattern getRegexPatternString(String[] builtinReadOnlyAttributes) {
|
||||
if (builtinReadOnlyAttributes != null) {
|
||||
List<String> readOnlyAttributes = new ArrayList<>(Arrays.asList(builtinReadOnlyAttributes));
|
||||
|
@ -133,6 +144,8 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
|||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
// make sure registry is clear in case of re-deploy
|
||||
contextualMetadataRegistry.clear();
|
||||
Pattern pattern = getRegexPatternString(config.getArray("read-only-attributes"));
|
||||
AttributeValidatorMetadata readOnlyValidator = null;
|
||||
|
||||
|
@ -234,8 +247,13 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
|||
|
||||
private UserProfile createUserProfile(UserProfileContext context, Map<String, ?> attributes, UserModel user) {
|
||||
UserProfileMetadata metadata = configureUserProfile(contextualMetadataRegistry.get(context), session);
|
||||
Attributes profileAttributes = new DefaultAttributes(context, attributes, user, metadata, session);
|
||||
return new DefaultUserProfile(profileAttributes, createUserFactory(), user);
|
||||
Attributes profileAttributes = createAttributes(context, attributes, user, metadata);
|
||||
return new DefaultUserProfile(profileAttributes, createUserFactory(), user, session);
|
||||
}
|
||||
|
||||
protected Attributes createAttributes(UserProfileContext context, Map<String, ?> attributes, UserModel user,
|
||||
UserProfileMetadata metadata) {
|
||||
return new DefaultAttributes(context, attributes, user, metadata, session);
|
||||
}
|
||||
|
||||
private void addContextualProfileMetadata(UserProfileMetadata metadata) {
|
||||
|
@ -259,7 +277,9 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
|||
private UserProfileMetadata createDefaultProfile(UserProfileContext context, AttributeValidatorMetadata readOnlyValidator) {
|
||||
UserProfileMetadata metadata = new UserProfileMetadata(context);
|
||||
|
||||
metadata.addAttribute(UserModel.USERNAME, new AttributeValidatorMetadata(UsernameHasValueValidator.ID),
|
||||
metadata.addAttribute(UserModel.USERNAME, AbstractUserProfileProvider::editUsernameCondition,
|
||||
AbstractUserProfileProvider::editUsernameCondition,
|
||||
new AttributeValidatorMetadata(UsernameHasValueValidator.ID),
|
||||
new AttributeValidatorMetadata(DuplicateUsernameValidator.ID),
|
||||
new AttributeValidatorMetadata(UsernameMutationValidator.ID));
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import java.util.List;
|
|||
|
||||
import org.keycloak.services.validation.Validation;
|
||||
import org.keycloak.userprofile.AttributeContext;
|
||||
import org.keycloak.userprofile.AttributeMetadata;
|
||||
import org.keycloak.userprofile.UserProfileAttributeValidationContext;
|
||||
import org.keycloak.validate.SimpleValidator;
|
||||
import org.keycloak.validate.ValidationContext;
|
||||
|
@ -46,10 +47,14 @@ public class AttributeRequiredByMetadataValidator implements SimpleValidator {
|
|||
|
||||
@Override
|
||||
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||
|
||||
AttributeContext attContext = UserProfileAttributeValidationContext.from(context).getAttributeContext();
|
||||
AttributeMetadata metadata = attContext.getMetadata();
|
||||
|
||||
if (!attContext.getMetadata().isRequired(attContext)) {
|
||||
if (!metadata.isRequired(attContext)) {
|
||||
return context;
|
||||
}
|
||||
|
||||
if (metadata.isReadOnly(attContext)) {
|
||||
return context;
|
||||
}
|
||||
|
||||
|
@ -60,7 +65,7 @@ public class AttributeRequiredByMetadataValidator implements SimpleValidator {
|
|||
context.addError(new ValidationError(ID, inputHint, ERROR_USER_ATTRIBUTE_REQUIRED));
|
||||
} else {
|
||||
for (String value : values) {
|
||||
if (value == null || Validation.isBlank(value)) {
|
||||
if (Validation.isBlank(value)) {
|
||||
context.addError(new ValidationError(ID, inputHint, ERROR_USER_ATTRIBUTE_REQUIRED));
|
||||
return context;
|
||||
}
|
||||
|
@ -68,5 +73,4 @@ public class AttributeRequiredByMetadataValidator implements SimpleValidator {
|
|||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
package org.keycloak.userprofile.validator;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -67,7 +68,8 @@ public class DuplicateEmailValidator implements SimpleValidator {
|
|||
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));
|
||||
context.addError(new ValidationError(ID, inputHint, Messages.EMAIL_EXISTS)
|
||||
.setStatusCode(Response.Status.CONFLICT));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
package org.keycloak.userprofile.validator;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -63,7 +64,8 @@ public class DuplicateUsernameValidator implements SimpleValidator {
|
|||
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));
|
||||
context.addError(new ValidationError(ID, inputHint, Messages.USERNAME_EXISTS)
|
||||
.setStatusCode(Response.Status.CONFLICT));
|
||||
}
|
||||
|
||||
return context;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
package org.keycloak.userprofile.validator;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -66,7 +67,8 @@ public class EmailExistsAsUsernameValidator implements SimpleValidator {
|
|||
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));
|
||||
context.addError(new ValidationError(ID, inputHint, Messages.USERNAME_EXISTS)
|
||||
.setStatusCode(Response.Status.CONFLICT));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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.stream.Collectors;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* A validator that fails when the attribute is marked as read only and its value has changed.
|
||||
*
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class ImmutableAttributeValidator implements SimpleValidator {
|
||||
|
||||
public static final String ID = "up-immutable-attribute";
|
||||
|
||||
private static final String DEFAULT_ERROR_MESSAGE = "error-user-attribute-read-only";
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||
UserProfileAttributeValidationContext ac = (UserProfileAttributeValidationContext) context;
|
||||
AttributeContext attributeContext = ac.getAttributeContext();
|
||||
|
||||
if (!isReadOnly(attributeContext)) {
|
||||
return context;
|
||||
}
|
||||
|
||||
UserModel user = attributeContext.getUser();
|
||||
|
||||
if (user == null) {
|
||||
return context;
|
||||
}
|
||||
|
||||
List<String> currentValue = user.getAttributeStream(inputHint).collect(Collectors.toList());
|
||||
List<String> values = (List<String>) input;
|
||||
|
||||
if (!(currentValue.containsAll(values) && currentValue.size() == values.size())) {
|
||||
context.addError(new ValidationError(ID, inputHint, DEFAULT_ERROR_MESSAGE));
|
||||
return context;
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private boolean isReadOnly(AttributeContext attributeContext) {
|
||||
return attributeContext.getMetadata().isReadOnly(attributeContext);
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
package org.keycloak.userprofile.validator;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -64,7 +65,8 @@ public class RegistrationUsernameExistsValidator implements SimpleValidator {
|
|||
|
||||
UserModel existing = session.users().getUserByUsername(realm, value);
|
||||
if (existing != null) {
|
||||
context.addError(new ValidationError(ID, inputHint, Messages.USERNAME_EXISTS));
|
||||
context.addError(new ValidationError(ID, inputHint, Messages.USERNAME_EXISTS)
|
||||
.setStatusCode(Response.Status.CONFLICT));
|
||||
}
|
||||
|
||||
return context;
|
||||
|
|
|
@ -24,3 +24,4 @@ org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory
|
|||
org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory
|
||||
org.keycloak.authentication.requiredactions.UpdateUserLocaleAction
|
||||
org.keycloak.authentication.requiredactions.DeleteAccount
|
||||
org.keycloak.authentication.requiredactions.VerifyUserProfile
|
|
@ -1,18 +1,20 @@
|
|||
#
|
||||
# Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
# and other contributors as indicated by the @author tags.
|
||||
# /*
|
||||
# * 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.
|
||||
# */
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
org.keycloak.userprofile.legacy.DefaultUserProfileProvider
|
||||
org.keycloak.userprofile.config.DeclarativeUserProfileProvider
|
|
@ -10,3 +10,4 @@ org.keycloak.userprofile.validator.RegistrationEmailAsUsernameUsernameValueValid
|
|||
org.keycloak.userprofile.validator.RegistrationUsernameExistsValidator
|
||||
org.keycloak.userprofile.validator.RegistrationEmailAsUsernameEmailValueValidator
|
||||
org.keycloak.userprofile.validator.BrokeringFederatedUsernameHasValueValidator
|
||||
org.keycloak.userprofile.validator.ImmutableAttributeValidator
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"name": "username"
|
||||
},
|
||||
{
|
||||
"name": "email"
|
||||
},
|
||||
{
|
||||
"name": "firstName",
|
||||
"required": {"roles" : ["user"]},
|
||||
"permissions": {
|
||||
"view": ["admin", "user"],
|
||||
"edit": ["admin", "user"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "lastName",
|
||||
"required": {"roles" : ["user"]},
|
||||
"permissions": {
|
||||
"view": ["admin", "user"],
|
||||
"edit": ["admin", "user"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -22,6 +22,9 @@ echo ** Adding spi=userProfile with legacy-user-profile configuration of read-on
|
|||
/subsystem=keycloak-server/spi=userProfile/provider=legacy-user-profile/:add(properties={},enabled=true)
|
||||
/subsystem=keycloak-server/spi=userProfile/provider=legacy-user-profile/:map-put(name=properties,key=read-only-attributes,value=[deniedFoo,deniedBar*,deniedSome/thing,deniedsome*thing])
|
||||
/subsystem=keycloak-server/spi=userProfile/provider=legacy-user-profile/:map-put(name=properties,key=admin-read-only-attributes,value=[deniedSomeAdmin])
|
||||
/subsystem=keycloak-server/spi=userProfile/provider=declarative-user-profile/:add(properties={},enabled=true)
|
||||
/subsystem=keycloak-server/spi=userProfile/provider=declarative-user-profile/:map-put(name=properties,key=read-only-attributes,value=[deniedFoo,deniedBar*,deniedSome/thing,deniedsome*thing])
|
||||
/subsystem=keycloak-server/spi=userProfile/provider=declarative-user-profile/:map-put(name=properties,key=admin-read-only-attributes,value=[deniedSomeAdmin])
|
||||
|
||||
echo ** Do not reuse connections for HttpClientProvider within testsuite **
|
||||
/subsystem=keycloak-server/spi=connectionsHttpClient/provider=default/:map-put(name=properties,key=reuse-connections,value=false)
|
||||
|
|
|
@ -25,3 +25,6 @@ spi.truststore.file.password=secret
|
|||
|
||||
# http client connection reuse settings
|
||||
spi.connections-http-client.default.reuse-connections=false
|
||||
|
||||
# user profile provider settings
|
||||
spi.user-profile.provider=${keycloak.userProfile.provider:legacy-user-profile}
|
||||
|
|
|
@ -1,20 +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.
|
||||
# */
|
||||
#
|
||||
|
||||
org.keycloak.testsuite.user.profile.config.DeclarativeUserProfileProvider
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"name": "username"
|
||||
},
|
||||
{
|
||||
"name": "email"
|
||||
},
|
||||
{
|
||||
"name": "firstName",
|
||||
"required": {}
|
||||
},
|
||||
{
|
||||
"name": "lastName",
|
||||
"required": {}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -564,9 +564,13 @@ public class AuthServerTestEnricher {
|
|||
wasUpdated = true;
|
||||
}
|
||||
if (event.getTestClass().isAnnotationPresent(SetDefaultProvider.class)) {
|
||||
SpiProvidersSwitchingUtils.addProviderDefaultValue(suiteContext, event.getTestClass().getAnnotation(SetDefaultProvider.class));
|
||||
SetDefaultProvider defaultProvider = event.getTestClass().getAnnotation(SetDefaultProvider.class);
|
||||
|
||||
if (defaultProvider.beforeEnableFeature()) {
|
||||
SpiProvidersSwitchingUtils.addProviderDefaultValue(suiteContext, defaultProvider);
|
||||
wasUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (wasUpdated) {
|
||||
restartAuthServer();
|
||||
|
|
|
@ -10,4 +10,27 @@ import java.lang.annotation.Target;
|
|||
public @interface SetDefaultProvider {
|
||||
String spi();
|
||||
String providerId();
|
||||
|
||||
/**
|
||||
* <p>Defines whether the default provider should be set by updating an existing Spi configuration.
|
||||
*
|
||||
* <p>This flag is useful when running the Wildfly distribution and when the server is already configured
|
||||
* with a Spi that should only be updated with the default provider.
|
||||
*
|
||||
* @return {@code true} if the default provider should update an existing Spi configuration. Otherwise, the Spi
|
||||
* configuration will be added with the default provider set.
|
||||
*/
|
||||
boolean onlyUpdateDefault() default false;
|
||||
|
||||
/**
|
||||
* <p>Defines whether the default provider should be set prior to enabling a feature.
|
||||
*
|
||||
* <p>This flag should be used together with {@link EnableFeature} so that the default provider
|
||||
* is set after enabling a feature. It is useful in case the default provider is not enabled by default,
|
||||
* thus requiring the feature to be enabled first.
|
||||
*
|
||||
* @return {@code true} if the default should be set prior to enabling a feature. Otherwise,
|
||||
* the default provider is only set after enabling a feature.
|
||||
*/
|
||||
boolean beforeEnableFeature() default true;
|
||||
}
|
||||
|
|
|
@ -18,10 +18,13 @@ import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
|
|||
import org.keycloak.testsuite.arquillian.annotation.DisableFeatures;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeatures;
|
||||
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
|
||||
import org.keycloak.testsuite.client.KeycloakTestingClient;
|
||||
import org.keycloak.testsuite.util.SpiProvidersSwitchingUtils;
|
||||
import org.wildfly.extras.creaper.core.online.OnlineManagementClient;
|
||||
import org.wildfly.extras.creaper.core.online.operations.admin.Administration;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
|
@ -74,12 +77,15 @@ public class KeycloakContainerFeaturesController {
|
|||
private boolean skipRestart;
|
||||
private FeatureAction action;
|
||||
private boolean onlyForProduct;
|
||||
private final AnnotatedElement annotatedElement;
|
||||
|
||||
public UpdateFeature(Profile.Feature feature, boolean skipRestart, FeatureAction action, boolean onlyForProduct) {
|
||||
public UpdateFeature(Profile.Feature feature, boolean skipRestart, FeatureAction action, boolean onlyForProduct
|
||||
, AnnotatedElement annotatedElement) {
|
||||
this.feature = feature;
|
||||
this.skipRestart = skipRestart;
|
||||
this.action = action;
|
||||
this.onlyForProduct = onlyForProduct;
|
||||
this.annotatedElement = annotatedElement;
|
||||
}
|
||||
|
||||
private void assertPerformed() {
|
||||
|
@ -94,6 +100,18 @@ public class KeycloakContainerFeaturesController {
|
|||
if ((action == FeatureAction.ENABLE && !ProfileAssume.isFeatureEnabled(feature))
|
||||
|| (action == FeatureAction.DISABLE && ProfileAssume.isFeatureEnabled(feature))) {
|
||||
action.accept(testContextInstance.get().getTestingClient(), feature);
|
||||
SetDefaultProvider setDefaultProvider = annotatedElement.getAnnotation(SetDefaultProvider.class);
|
||||
if (setDefaultProvider != null) {
|
||||
try {
|
||||
if (action == FeatureAction.ENABLE) {
|
||||
SpiProvidersSwitchingUtils.addProviderDefaultValue(suiteContextInstance.get(), setDefaultProvider);
|
||||
} else {
|
||||
SpiProvidersSwitchingUtils.removeProvider(suiteContextInstance.get(), setDefaultProvider);
|
||||
}
|
||||
} catch (Exception cause) {
|
||||
throw new RuntimeException("Failed to (un)set default provider", cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,12 +204,13 @@ public class KeycloakContainerFeaturesController {
|
|||
|
||||
ret.addAll(Arrays.stream(annotatedElement.getAnnotationsByType(EnableFeature.class))
|
||||
.map(annotation -> new UpdateFeature(annotation.value(), annotation.skipRestart(),
|
||||
state == State.BEFORE ? FeatureAction.ENABLE : FeatureAction.DISABLE, annotation.onlyForProduct()))
|
||||
state == State.BEFORE ? FeatureAction.ENABLE : FeatureAction.DISABLE, annotation.onlyForProduct(), annotatedElement))
|
||||
.collect(Collectors.toSet()));
|
||||
|
||||
ret.addAll(Arrays.stream(annotatedElement.getAnnotationsByType(DisableFeature.class))
|
||||
.map(annotation -> new UpdateFeature(annotation.value(), annotation.skipRestart(),
|
||||
state == State.BEFORE ? FeatureAction.DISABLE : FeatureAction.ENABLE, annotation.onlyForProduct()))
|
||||
state == State.BEFORE ? FeatureAction.DISABLE : FeatureAction.ENABLE, annotation.onlyForProduct(),
|
||||
annotatedElement))
|
||||
.collect(Collectors.toSet()));
|
||||
|
||||
return ret;
|
||||
|
|
|
@ -16,6 +16,8 @@ import java.security.KeyManagementException;
|
|||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
@ -47,6 +49,9 @@ public class KeycloakQuarkusServerDeployableContainer implements DeployableConta
|
|||
@Inject
|
||||
private Instance<SuiteContext> suiteContext;
|
||||
|
||||
private boolean forceReaugmentation;
|
||||
private List<String> additionalArgs = Collections.emptyList();
|
||||
|
||||
@Override
|
||||
public Class<KeycloakQuarkusConfiguration> getConfigurationClass() {
|
||||
return KeycloakQuarkusConfiguration.class;
|
||||
|
@ -120,8 +125,12 @@ public class KeycloakQuarkusServerDeployableContainer implements DeployableConta
|
|||
FileUtils.deleteDirectory(configuration.getProvidersPath().resolve("data").toFile());
|
||||
}
|
||||
|
||||
if (configuration.isReaugmentBeforeStart()) {
|
||||
ProcessBuilder reaugment = new ProcessBuilder("./kc.sh", "config");
|
||||
if (isReaugmentBeforeStart()) {
|
||||
List<String> commands = new ArrayList<>(Arrays.asList("./kc.sh", "config", "-Dquarkus.http.root-path=/auth"));
|
||||
|
||||
addAdditionalCommands(commands);
|
||||
|
||||
ProcessBuilder reaugment = new ProcessBuilder(commands);
|
||||
|
||||
reaugment.directory(wrkDir).inheritIO();
|
||||
|
||||
|
@ -136,6 +145,10 @@ public class KeycloakQuarkusServerDeployableContainer implements DeployableConta
|
|||
return builder.start();
|
||||
}
|
||||
|
||||
private boolean isReaugmentBeforeStart() {
|
||||
return configuration.isReaugmentBeforeStart() || forceReaugmentation;
|
||||
}
|
||||
|
||||
private String[] getProcessCommands() {
|
||||
List<String> commands = new ArrayList<>();
|
||||
|
||||
|
@ -158,9 +171,15 @@ public class KeycloakQuarkusServerDeployableContainer implements DeployableConta
|
|||
|
||||
commands.add("--cluster=" + System.getProperty("auth.server.quarkus.cluster.config", "local"));
|
||||
|
||||
addAdditionalCommands(commands);
|
||||
|
||||
return commands.toArray(new String[commands.size()]);
|
||||
}
|
||||
|
||||
private void addAdditionalCommands(List<String> commands) {
|
||||
commands.addAll(additionalArgs);
|
||||
}
|
||||
|
||||
private void waitForReadiness() throws MalformedURLException, LifecycleException {
|
||||
SuiteContext suiteContext = this.suiteContext.get();
|
||||
//TODO: not sure if the best endpoint but it makes sure that everything is properly initialized. Once we have
|
||||
|
@ -252,4 +271,14 @@ public class KeycloakQuarkusServerDeployableContainer implements DeployableConta
|
|||
private long getStartTimeout() {
|
||||
return TimeUnit.SECONDS.toMillis(configuration.getStartupTimeoutInSeconds());
|
||||
}
|
||||
|
||||
public void forceReAugmentation(String... args) {
|
||||
forceReaugmentation = true;
|
||||
additionalArgs = Arrays.asList(args);
|
||||
}
|
||||
|
||||
public void resetConfiguration() {
|
||||
additionalArgs = Collections.emptyList();
|
||||
forceReAugmentation();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* 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.testsuite.pages;
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.keycloak.testsuite.auth.page.AccountFields;
|
||||
import org.keycloak.testsuite.util.UIUtils;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.NoSuchElementException;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
/**
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*/
|
||||
public class VerifyProfilePage extends AbstractPage {
|
||||
|
||||
@Page
|
||||
private AccountFields.AccountErrors accountErrors;
|
||||
|
||||
@FindBy(id = "firstName")
|
||||
private WebElement firstNameInput;
|
||||
|
||||
@FindBy(id = "lastName")
|
||||
private WebElement lastNameInput;
|
||||
|
||||
@FindBy(id = "email")
|
||||
private WebElement emailInput;
|
||||
|
||||
@FindBy(id = "department")
|
||||
private WebElement departmentInput;
|
||||
|
||||
|
||||
@FindBy(css = "input[type=\"submit\"]")
|
||||
private WebElement submitButton;
|
||||
|
||||
@FindBy(className = "alert-error")
|
||||
private WebElement loginAlertErrorMessage;
|
||||
|
||||
|
||||
public void update(String firstName, String lastName) {
|
||||
firstNameInput.clear();
|
||||
if (firstName != null) {
|
||||
firstNameInput.sendKeys(firstName);
|
||||
}
|
||||
|
||||
lastNameInput.clear();
|
||||
if (lastName != null) {
|
||||
lastNameInput.sendKeys(lastName);
|
||||
}
|
||||
|
||||
submitButton.click();
|
||||
}
|
||||
|
||||
public void update(String firstName, String lastName, String department) {
|
||||
departmentInput.clear();
|
||||
if (department != null) {
|
||||
departmentInput.sendKeys(department);
|
||||
}
|
||||
|
||||
update(firstName, lastName);
|
||||
}
|
||||
|
||||
public String getAlertError() {
|
||||
try {
|
||||
return UIUtils.getTextFromElement(loginAlertErrorMessage);
|
||||
} catch (NoSuchElementException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstNameInput.getAttribute("value");
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastNameInput.getAttribute("value");
|
||||
}
|
||||
|
||||
public String getDepartment() {
|
||||
return departmentInput.getAttribute("value");
|
||||
}
|
||||
|
||||
public boolean isDepartmentEnabled() {
|
||||
return departmentInput.isEnabled();
|
||||
}
|
||||
|
||||
public boolean isUsernamePresent() {
|
||||
try {
|
||||
return driver.findElement(By.id("username")).isDisplayed();
|
||||
} catch (NoSuchElementException nse) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isDepartmentPresent() {
|
||||
try {
|
||||
isDepartmentEnabled();
|
||||
return true;
|
||||
} catch (NoSuchElementException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return emailInput.getAttribute("value");
|
||||
}
|
||||
|
||||
public boolean isCurrent() {
|
||||
return PageUtils.getPageTitle(driver).equals("Update Account Information");
|
||||
}
|
||||
|
||||
public AccountFields.AccountErrors getInputAccountErrors(){
|
||||
return accountErrors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void open() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,31 +1,52 @@
|
|||
package org.keycloak.testsuite.util;
|
||||
|
||||
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
|
||||
import org.keycloak.testsuite.arquillian.ContainerInfo;
|
||||
import org.keycloak.testsuite.arquillian.SuiteContext;
|
||||
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
|
||||
import org.keycloak.testsuite.arquillian.containers.KeycloakQuarkusServerDeployableContainer;
|
||||
import org.wildfly.extras.creaper.core.online.CliException;
|
||||
import org.wildfly.extras.creaper.core.online.OnlineManagementClient;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public class SpiProvidersSwitchingUtils {
|
||||
public static void addProviderDefaultValue(SuiteContext suiteContext, SetDefaultProvider annotation) throws IOException, CliException {
|
||||
if (suiteContext.getAuthServerInfo().isUndertow()) {
|
||||
ContainerInfo authServerInfo = suiteContext.getAuthServerInfo();
|
||||
|
||||
if (authServerInfo.isUndertow()) {
|
||||
System.setProperty("keycloak." + annotation.spi() + ".provider", annotation.providerId());
|
||||
} else if (authServerInfo.isQuarkus()) {
|
||||
KeycloakQuarkusServerDeployableContainer container = (KeycloakQuarkusServerDeployableContainer) authServerInfo.getArquillianContainer().getDeployableContainer();
|
||||
container.forceReAugmentation("-Dkeycloak." + annotation.spi() + ".provider=" + annotation.providerId());
|
||||
} else {
|
||||
OnlineManagementClient client = AuthServerTestEnricher.getManagementClient();
|
||||
|
||||
if (annotation.onlyUpdateDefault()) {
|
||||
client.execute("/subsystem=keycloak-server/spi=" + annotation.spi() + ":write-attribute(name=default-provider, value=" + annotation.providerId() + ")");
|
||||
} else {
|
||||
client.execute("/subsystem=keycloak-server/spi=" + annotation.spi() + "/:add(default-provider=\"" + annotation.providerId() + "\")");
|
||||
}
|
||||
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeProvider(SuiteContext suiteContext, SetDefaultProvider annotation) throws IOException, CliException {
|
||||
if (suiteContext.getAuthServerInfo().isUndertow()) {
|
||||
ContainerInfo authServerInfo = suiteContext.getAuthServerInfo();
|
||||
|
||||
if (authServerInfo.isUndertow()) {
|
||||
System.clearProperty("keycloak." + annotation.spi() + ".provider");
|
||||
} else if (authServerInfo.isQuarkus()) {
|
||||
KeycloakQuarkusServerDeployableContainer container = (KeycloakQuarkusServerDeployableContainer) authServerInfo.getArquillianContainer().getDeployableContainer();
|
||||
container.resetConfiguration();
|
||||
} else {
|
||||
OnlineManagementClient client = AuthServerTestEnricher.getManagementClient();
|
||||
if (annotation.onlyUpdateDefault()) {
|
||||
client.execute("/subsystem=keycloak-server/spi=" + annotation.spi() + "/:undefine-attribute(name=default-provider)");
|
||||
} else {
|
||||
client.execute("/subsystem=keycloak-server/spi=" + annotation.spi() + "/:remove");
|
||||
}
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ public class RequiredActionsTest extends AbstractAuthenticationTest {
|
|||
|
||||
// Dummy RequiredAction is not registered in the realm and WebAuthn actions
|
||||
List<RequiredActionProviderSimpleRepresentation> result = authMgmtResource.getUnregisteredRequiredActions();
|
||||
Assert.assertEquals(3, result.size());
|
||||
Assert.assertEquals(4, result.size());
|
||||
RequiredActionProviderSimpleRepresentation action = result.get(0);
|
||||
Assert.assertEquals(DummyRequiredActionFactory.PROVIDER_ID, action.getProviderId());
|
||||
Assert.assertEquals("Dummy Action", action.getName());
|
||||
|
|
|
@ -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.testsuite.admin.userprofile;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.resource.UserProfileResource;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.util.StreamUtil;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.admin.AbstractAdminTest;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
|
||||
import org.keycloak.userprofile.UserProfileSpi;
|
||||
import org.keycloak.userprofile.config.DeclarativeUserProfileProvider;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
@EnableFeature(value = Profile.Feature.DECLARATIVE_USER_PROFILE, skipRestart = false)
|
||||
@SetDefaultProvider(spi = UserProfileSpi.ID, providerId = DeclarativeUserProfileProvider.ID,
|
||||
beforeEnableFeature = false,
|
||||
onlyUpdateDefault = true
|
||||
)
|
||||
public class UserProfileAdminTest extends AbstractAdminTest {
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultConfigIfNoneSet() {
|
||||
String defaultRawConfig;
|
||||
|
||||
try (InputStream is = DeclarativeUserProfileProvider.class.getResourceAsStream(DeclarativeUserProfileProvider.SYSTEM_DEFAULT_CONFIG_RESOURCE)) {
|
||||
defaultRawConfig = StreamUtil.readString(is, Charset.defaultCharset());
|
||||
} catch (IOException cause) {
|
||||
throw new RuntimeException("Failed to load default user profile config file", cause);
|
||||
}
|
||||
|
||||
assertEquals(defaultRawConfig, testRealm().users().userProfile().getConfiguration());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetDefaultConfig() throws IOException {
|
||||
String rawConfig = "{\"attributes\": [{\"name\": \"test\"}]}";
|
||||
UserProfileResource userProfile = testRealm().users().userProfile();
|
||||
|
||||
userProfile.update(rawConfig);
|
||||
|
||||
assertEquals(rawConfig, userProfile.getConfiguration());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,260 @@
|
|||
/*
|
||||
* Copyright 2016 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.testsuite.forms;
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
|
||||
import org.keycloak.testsuite.pages.*;
|
||||
import org.keycloak.testsuite.pages.AppPage.RequestType;
|
||||
|
||||
import org.keycloak.testsuite.util.*;
|
||||
import org.keycloak.userprofile.UserProfileSpi;
|
||||
import org.keycloak.userprofile.config.DeclarativeUserProfileProvider;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*/
|
||||
@EnableFeature(value = Profile.Feature.DECLARATIVE_USER_PROFILE, skipRestart = false)
|
||||
@SetDefaultProvider(spi = UserProfileSpi.ID, providerId = DeclarativeUserProfileProvider.ID,
|
||||
beforeEnableFeature = false,
|
||||
onlyUpdateDefault = true
|
||||
)
|
||||
public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
@Rule
|
||||
public AssertEvents events = new AssertEvents(this);
|
||||
|
||||
@Page
|
||||
protected AppPage appPage;
|
||||
|
||||
@Page
|
||||
protected LoginPage loginPage;
|
||||
|
||||
@Page
|
||||
protected RegisterPage registerPage;
|
||||
|
||||
@Page
|
||||
protected VerifyEmailPage verifyEmailPage;
|
||||
|
||||
@Page
|
||||
protected AccountUpdateProfilePage accountPage;
|
||||
|
||||
@Rule
|
||||
public GreenMailRule greenMail = new GreenMailRule();
|
||||
|
||||
|
||||
private static final String SCOPE_LAST_NAME = "lastName";
|
||||
|
||||
private static ClientRepresentation client_scope_default;
|
||||
private static ClientRepresentation client_scope_optional;
|
||||
|
||||
public static String UP_CONFIG_BASIC_ATTRIBUTES = "{\"name\": \"username\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"email\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},";
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
|
||||
testRealm.setClientScopes(Collections.singletonList(ClientScopeBuilder.create().name(SCOPE_LAST_NAME).protocol("openid-connect").build()));
|
||||
client_scope_default = KeycloakModelUtils.createClient(testRealm, "client-a");
|
||||
client_scope_default.setDefaultClientScopes(Collections.singletonList(SCOPE_LAST_NAME));
|
||||
client_scope_default.setRedirectUris(Collections.singletonList("https://*"));
|
||||
client_scope_optional = KeycloakModelUtils.createClient(testRealm, "client-b");
|
||||
client_scope_optional.setOptionalClientScopes(Collections.singletonList(SCOPE_LAST_NAME));
|
||||
client_scope_optional.setRedirectUris(Collections.singletonList("https://*"));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerUserSuccess_lastNameOptional() {
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ UP_CONFIG_BASIC_ATTRIBUTES
|
||||
+ "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + "}"
|
||||
+ "]}");
|
||||
|
||||
loginPage.open();
|
||||
loginPage.clickRegister();
|
||||
registerPage.assertCurrent();
|
||||
|
||||
registerPage.register("firstName", "", "registerUserSuccessLastNameOptional@email", "registerUserSuccessLastNameOptional", "password", "password");
|
||||
|
||||
appPage.assertCurrent();
|
||||
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
|
||||
String userId = events.expectRegister("registerUserSuccessLastNameOptional", "registerUserSuccessLastNameOptional@email").assertEvent().getUserId();
|
||||
assertUserRegistered(userId, "registerUserSuccessLastNameOptional", "registerusersuccesslastnameoptional@email", "firstName", "");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerUserSuccess_lastNameRequiredForScope_notRequested() {
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ UP_CONFIG_BASIC_ATTRIBUTES
|
||||
+ "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\""+SCOPE_LAST_NAME+"\"]}}"
|
||||
+ "]}");
|
||||
|
||||
loginPage.open();
|
||||
loginPage.clickRegister();
|
||||
registerPage.assertCurrent();
|
||||
|
||||
registerPage.register("firstName", "", "registerUserSuccessLastNameRequiredForScope_notRequested@email", "registerUserSuccessLastNameRequiredForScope_notRequested", "password", "password");
|
||||
|
||||
appPage.assertCurrent();
|
||||
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
|
||||
String userId = events.expectRegister("registerUserSuccessLastNameRequiredForScope_notRequested", "registerUserSuccessLastNameRequiredForScope_notRequested@email").assertEvent().getUserId();
|
||||
assertUserRegistered(userId, "registerUserSuccessLastNameRequiredForScope_notRequested", "registerusersuccesslastnamerequiredforscope_notrequested@email", "firstName", "");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerUserSuccess_lastNameRequiredForScope_requested() {
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ UP_CONFIG_BASIC_ATTRIBUTES
|
||||
+ "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\""+SCOPE_LAST_NAME+"\"]}}"
|
||||
+ "]}");
|
||||
|
||||
oauth.scope(SCOPE_LAST_NAME).clientId(client_scope_optional.getClientId()).openLoginForm();
|
||||
|
||||
loginPage.clickRegister();
|
||||
registerPage.assertCurrent();
|
||||
|
||||
registerPage.register("firstName", "", "registerUserSuccessLastNameRequiredForScope_requested@email", "registerUserSuccessLastNameRequiredForScope_requested", "password", "password");
|
||||
|
||||
//error reported
|
||||
registerPage.assertCurrent();
|
||||
assertEquals("Please specify this field.", registerPage.getInputAccountErrors().getLastNameError());
|
||||
|
||||
//submit correct form
|
||||
registerPage.register("firstName", "lastName", "registerUserSuccessLastNameRequiredForScope_requested@email", "registerUserSuccessLastNameRequiredForScope_requested", "password", "password");
|
||||
|
||||
appPage.assertCurrent();
|
||||
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerUserSuccess_lastNameRequiredForScope_clientDefault() {
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ UP_CONFIG_BASIC_ATTRIBUTES
|
||||
+ "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\""+SCOPE_LAST_NAME+"\"]}}"
|
||||
+ "]}");
|
||||
|
||||
oauth.clientId(client_scope_default.getClientId()).openLoginForm();
|
||||
|
||||
loginPage.clickRegister();
|
||||
registerPage.assertCurrent();
|
||||
|
||||
registerPage.register("firstName", "", "registerUserSuccessLastNameRequiredForScope_clientDefault@email", "registerUserSuccessLastNameRequiredForScope_clientDefault", "password", "password");
|
||||
|
||||
//error reported
|
||||
registerPage.assertCurrent();
|
||||
assertEquals("Please specify this field.", registerPage.getInputAccountErrors().getLastNameError());
|
||||
|
||||
//submit correct form
|
||||
registerPage.register("firstName", "lastName", "registerUserSuccessLastNameRequiredForScope_clientDefault@email", "registerUserSuccessLastNameRequiredForScope_clientDefault", "password", "password");
|
||||
|
||||
appPage.assertCurrent();
|
||||
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerUserSuccess_lastNameLengthValidation() {
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ UP_CONFIG_BASIC_ATTRIBUTES
|
||||
+ "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", " + VerifyProfileTest.VALIDATIONS_LENGTH + "}"
|
||||
+ "]}");
|
||||
|
||||
loginPage.open();
|
||||
loginPage.clickRegister();
|
||||
registerPage.assertCurrent();
|
||||
|
||||
registerPage.register("firstName", "last", "registerUserSuccessLastNameLengthValidation@email", "registerUserSuccessLastNameLengthValidation", "password", "password");
|
||||
|
||||
appPage.assertCurrent();
|
||||
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
|
||||
String userId = events.expectRegister("registerUserSuccessLastNameLengthValidation", "registerUserSuccessLastNameLengthValidation@email").assertEvent().getUserId();
|
||||
assertUserRegistered(userId, "registerUserSuccessLastNameLengthValidation", "registerusersuccesslastnamelengthvalidation@email", "firstName", "last");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerUserInvalidLastNameLength() {
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ UP_CONFIG_BASIC_ATTRIBUTES
|
||||
+ "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", " + VerifyProfileTest.VALIDATIONS_LENGTH + "}"
|
||||
+ "]}");
|
||||
|
||||
loginPage.open();
|
||||
loginPage.clickRegister();
|
||||
registerPage.assertCurrent();
|
||||
|
||||
registerPage.register("firstName", "L", "registerUserInvalidLastNameLength@email", "registerUserInvalidLastNameLength", "password", "password");
|
||||
|
||||
registerPage.assertCurrent();
|
||||
assertEquals("Length must be between 3 and 255.", registerPage.getInputAccountErrors().getLastNameError());
|
||||
|
||||
events.expectRegister("registeruserinvalidlastnamelength", "registerUserInvalidLastNameLength@email")
|
||||
.error("invalid_registration").assertEvent();
|
||||
}
|
||||
|
||||
private void assertUserRegistered(String userId, String username, String email, String firstName, String lastName) {
|
||||
events.expectLogin().detail("username", username.toLowerCase()).user(userId).assertEvent();
|
||||
|
||||
UserRepresentation user = getUser(userId);
|
||||
Assert.assertNotNull(user);
|
||||
Assert.assertNotNull(user.getCreatedTimestamp());
|
||||
// test that timestamp is current with 10s tollerance
|
||||
Assert.assertTrue((System.currentTimeMillis() - user.getCreatedTimestamp()) < 10000);
|
||||
// test user info is set from form
|
||||
assertEquals(username.toLowerCase(), user.getUsername());
|
||||
assertEquals(email.toLowerCase(), user.getEmail());
|
||||
assertEquals(firstName, user.getFirstName());
|
||||
assertEquals(lastName, user.getLastName());
|
||||
}
|
||||
|
||||
protected UserRepresentation getUser(String userId) {
|
||||
return testRealm().users().get(userId).toRepresentation();
|
||||
}
|
||||
|
||||
private void setUserProfileConfiguration(String configuration) {
|
||||
Response r = testRealm().users().userProfile().update(configuration);
|
||||
if (r.getStatus() != 200) {
|
||||
Assert.fail("Configuration not set due to error: " + r.readEntity(String.class));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,600 @@
|
|||
/*
|
||||
* 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.testsuite.forms;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.jboss.arquillian.test.api.ArquillianResource;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
|
||||
import org.keycloak.testsuite.pages.AppPage;
|
||||
import org.keycloak.testsuite.pages.AppPage.RequestType;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.pages.VerifyProfilePage;
|
||||
import org.keycloak.testsuite.util.ClientScopeBuilder;
|
||||
import org.keycloak.testsuite.util.KeycloakModelUtils;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.RealmBuilder;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
import org.keycloak.userprofile.UserProfileSpi;
|
||||
import org.keycloak.userprofile.config.DeclarativeUserProfileProvider;
|
||||
|
||||
/**
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*/
|
||||
@EnableFeature(value = Profile.Feature.DECLARATIVE_USER_PROFILE, skipRestart = false)
|
||||
@SetDefaultProvider(spi = UserProfileSpi.ID, providerId = DeclarativeUserProfileProvider.ID,
|
||||
beforeEnableFeature = false,
|
||||
onlyUpdateDefault = true)
|
||||
public class VerifyProfileTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
private static final String SCOPE_DEPARTMENT = "department";
|
||||
private static final String ATTRIBUTE_DEPARTMENT = "department";
|
||||
|
||||
public static String PERMISSIONS_ALL = "\"permissions\": {\"view\": [\"admin\", \"user\"], \"edit\": [\"admin\", \"user\"]}";
|
||||
public static String PERMISSIONS_ADMIN_ONLY = "\"permissions\": {\"view\": [\"admin\"], \"edit\": [\"admin\"]}";
|
||||
public static String PERMISSIONS_ADMIN_EDITABLE = "\"permissions\": {\"view\": [\"admin\", \"user\"], \"edit\": [\"admin\"]}";
|
||||
|
||||
public static String VALIDATIONS_LENGTH = "\"validations\": {\"length\": { \"min\": 3, \"max\": 255 }}";
|
||||
|
||||
private static final String CONFIGURATION_FOR_USER_EDIT = "{\"attributes\": ["
|
||||
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + "},"
|
||||
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + "}"
|
||||
+ "]}";
|
||||
|
||||
|
||||
private static String userId;
|
||||
|
||||
private static String user2Id;
|
||||
|
||||
private static String user3Id;
|
||||
|
||||
private static String user4Id;
|
||||
|
||||
private static String user5Id;
|
||||
|
||||
private static ClientRepresentation client_scope_default;
|
||||
private static ClientRepresentation client_scope_optional;
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
UserRepresentation user = UserBuilder.create().id(UUID.randomUUID().toString()).username("login-test").email("login@test.com").enabled(true).password("password").build();
|
||||
userId = user.getId();
|
||||
|
||||
UserRepresentation user2 = UserBuilder.create().id(UUID.randomUUID().toString()).username("login-test2").email("login2@test.com").enabled(true).password("password").build();
|
||||
user2Id = user2.getId();
|
||||
|
||||
UserRepresentation user3 = UserBuilder.create().id(UUID.randomUUID().toString()).username("login-test3").email("login3@test.com").enabled(true).password("password").lastName("ExistingLast").build();
|
||||
user3Id = user3.getId();
|
||||
|
||||
UserRepresentation user4 = UserBuilder.create().id(UUID.randomUUID().toString()).username("login-test4").email("login4@test.com").enabled(true).password("password").lastName("ExistingLast").build();
|
||||
user4Id = user4.getId();
|
||||
|
||||
UserRepresentation user5 = UserBuilder.create().id(UUID.randomUUID().toString()).username("login-test5").email("login5@test.com").enabled(true).password("password").firstName("ExistingFirst").lastName("ExistingLast").build();
|
||||
user5Id = user5.getId();
|
||||
|
||||
RealmBuilder.edit(testRealm).user(user).user(user2).user(user3).user(user4).user(user5);
|
||||
|
||||
RequiredActionProviderRepresentation action = new RequiredActionProviderRepresentation();
|
||||
action.setAlias(UserModel.RequiredAction.VERIFY_PROFILE.name());
|
||||
action.setProviderId(UserModel.RequiredAction.VERIFY_PROFILE.name());
|
||||
action.setEnabled(true);
|
||||
action.setDefaultAction(false);
|
||||
action.setPriority(10);
|
||||
|
||||
List<RequiredActionProviderRepresentation> actions = new ArrayList<>();
|
||||
actions.add(action);
|
||||
testRealm.setRequiredActions(actions);
|
||||
|
||||
testRealm.setClientScopes(Collections.singletonList(ClientScopeBuilder.create().name(SCOPE_DEPARTMENT).protocol("openid-connect").build()));
|
||||
client_scope_default = KeycloakModelUtils.createClient(testRealm, "client-a");
|
||||
client_scope_default.setDefaultClientScopes(Collections.singletonList(SCOPE_DEPARTMENT));
|
||||
client_scope_default.setRedirectUris(Collections.singletonList("https://*"));
|
||||
client_scope_optional = KeycloakModelUtils.createClient(testRealm, "client-b");
|
||||
client_scope_optional.setOptionalClientScopes(Collections.singletonList(SCOPE_DEPARTMENT));
|
||||
client_scope_optional.setRedirectUris(Collections.singletonList("https://*"));
|
||||
}
|
||||
|
||||
@Rule
|
||||
public AssertEvents events = new AssertEvents(this);
|
||||
|
||||
@Page
|
||||
protected AppPage appPage;
|
||||
|
||||
@Page
|
||||
protected LoginPage loginPage;
|
||||
|
||||
@Page
|
||||
protected VerifyProfilePage verifyProfilePage;
|
||||
|
||||
@ArquillianResource
|
||||
protected OAuthClient oauth;
|
||||
|
||||
@Test
|
||||
public void testDefaultProfile() {
|
||||
setUserProfileConfiguration(null);
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login("login-test", "password");
|
||||
|
||||
//submit with error
|
||||
verifyProfilePage.assertCurrent();
|
||||
Assert.assertFalse(verifyProfilePage.isDepartmentPresent());
|
||||
verifyProfilePage.update("First", " ");
|
||||
|
||||
//submit OK
|
||||
verifyProfilePage.assertCurrent();
|
||||
Assert.assertFalse(verifyProfilePage.isDepartmentPresent());
|
||||
verifyProfilePage.update("First", "Last");
|
||||
|
||||
|
||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
|
||||
events.expectRequiredAction(EventType.VERIFY_PROFILE).user(userId).assertEvent();
|
||||
|
||||
UserRepresentation user = getUser(userId);
|
||||
assertEquals("First", user.getFirstName());
|
||||
assertEquals("Last", user.getLastName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUsernameOnlyIfEditAllowed() {
|
||||
RealmRepresentation realm = testRealm().toRepresentation();
|
||||
|
||||
try {
|
||||
setUserProfileConfiguration(null);
|
||||
|
||||
realm.setEditUsernameAllowed(false);
|
||||
testRealm().update(realm);
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login("login-test", "password");
|
||||
|
||||
assertFalse(verifyProfilePage.isUsernamePresent());
|
||||
|
||||
realm.setEditUsernameAllowed(true);
|
||||
testRealm().update(realm);
|
||||
|
||||
driver.navigate().refresh();
|
||||
assertTrue(verifyProfilePage.isUsernamePresent());
|
||||
} finally {
|
||||
realm.setEditUsernameAllowed(false);
|
||||
testRealm().update(realm);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOptionalAttribute() {
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "}"
|
||||
+ "]}");
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login("login-test2", "password");
|
||||
|
||||
verifyProfilePage.assertCurrent();
|
||||
verifyProfilePage.update("First", "");
|
||||
|
||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
|
||||
events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user2Id).assertEvent();
|
||||
|
||||
UserRepresentation user = getUser(user2Id);
|
||||
assertEquals("First", user.getFirstName());
|
||||
assertEquals("", user.getLastName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomValidationLastName() {
|
||||
|
||||
setUserProfileConfiguration(CONFIGURATION_FOR_USER_EDIT);
|
||||
updateUser(user5Id, "ExistingFirst", "La", "Department");
|
||||
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL +","+VALIDATIONS_LENGTH + "},"
|
||||
+ "{\"name\": \"department\"," + PERMISSIONS_ADMIN_ONLY + "}"
|
||||
+ "]}");
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login("login-test5", "password");
|
||||
|
||||
verifyProfilePage.assertCurrent();
|
||||
//submit with error
|
||||
verifyProfilePage.update("First", "L");
|
||||
|
||||
verifyProfilePage.assertCurrent();
|
||||
//submit OK
|
||||
verifyProfilePage.update("First", "Last");
|
||||
|
||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
|
||||
events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user5Id).assertEvent();
|
||||
|
||||
UserRepresentation user = getUser(user5Id);
|
||||
assertEquals("First", user.getFirstName());
|
||||
assertEquals("Last", user.getLastName());
|
||||
//check that not configured attribute is unchanged
|
||||
assertEquals("Department", user.firstAttribute(ATTRIBUTE_DEPARTMENT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoActionIfNoValidationError() {
|
||||
|
||||
setUserProfileConfiguration(CONFIGURATION_FOR_USER_EDIT);
|
||||
updateUser(user5Id, "ExistingFirst", "ExistingLast", "Department");
|
||||
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL +","+VALIDATIONS_LENGTH + "}"
|
||||
+ "]}");
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login("login-test5", "password");
|
||||
|
||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequiredReadOnlyAttribute() {
|
||||
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||
+ "{\"name\": \"department\"," + PERMISSIONS_ADMIN_EDITABLE + ", \"required\":{}}"
|
||||
+ "]}");
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login("login-test3", "password");
|
||||
|
||||
verifyProfilePage.assertCurrent();
|
||||
Assert.assertEquals("ExistingLast", verifyProfilePage.getLastName());
|
||||
Assert.assertFalse(verifyProfilePage.isDepartmentEnabled());
|
||||
|
||||
//update of the other attributes must be successful in this case
|
||||
verifyProfilePage.update("First", "Last");
|
||||
|
||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
|
||||
events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user3Id).assertEvent();
|
||||
|
||||
UserRepresentation user = getUser(user3Id);
|
||||
assertEquals("First", user.getFirstName());
|
||||
assertEquals("Last", user.getLastName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAttributeNotVisible() {
|
||||
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||
+ "{\"name\": \"department\"," + PERMISSIONS_ADMIN_ONLY + ", \"required\":{}}"
|
||||
+ "]}");
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login("login-test4", "password");
|
||||
|
||||
verifyProfilePage.assertCurrent();
|
||||
Assert.assertEquals("ExistingLast", verifyProfilePage.getLastName());
|
||||
Assert.assertFalse("'department' field is visible" , verifyProfilePage.isDepartmentPresent());
|
||||
|
||||
//update of the other attributes must be successful in this case
|
||||
verifyProfilePage.update("First", "Last");
|
||||
|
||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
|
||||
events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user4Id).assertEvent();
|
||||
|
||||
UserRepresentation user = getUser(user4Id);
|
||||
assertEquals("First", user.getFirstName());
|
||||
assertEquals("Last", user.getLastName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequiredAttribute() {
|
||||
|
||||
setUserProfileConfiguration(CONFIGURATION_FOR_USER_EDIT);
|
||||
updateUser(user5Id, "ExistingFirst", "ExistingLast", null);
|
||||
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"required\":{}}"
|
||||
+ "]}");
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login("login-test5", "password");
|
||||
|
||||
verifyProfilePage.assertCurrent();
|
||||
|
||||
//submit with error
|
||||
verifyProfilePage.update("FirstCC", "LastCC", " ");
|
||||
verifyProfilePage.assertCurrent();
|
||||
|
||||
//submit OK
|
||||
verifyProfilePage.update("FirstCC", "LastCC", "DepartmentCC");
|
||||
|
||||
|
||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
|
||||
events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user5Id).assertEvent();
|
||||
|
||||
UserRepresentation user = getUser(user5Id);
|
||||
assertEquals("FirstCC", user.getFirstName());
|
||||
assertEquals("LastCC", user.getLastName());
|
||||
assertEquals("DepartmentCC", user.firstAttribute(ATTRIBUTE_DEPARTMENT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequiredOnlyIfUser() {
|
||||
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"required\":{\"roles\":[\"user\"]}}"
|
||||
+ "]}");
|
||||
|
||||
updateUser(user5Id, "ExistingFirst", "ExistingLast", null);
|
||||
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login("login-test5", "password");
|
||||
|
||||
verifyProfilePage.assertCurrent();
|
||||
|
||||
//submit with error
|
||||
verifyProfilePage.update("FirstCC", "LastCC", " ");
|
||||
verifyProfilePage.assertCurrent();
|
||||
|
||||
//submit OK
|
||||
verifyProfilePage.update("FirstCC", "LastCC", "DepartmentCC");
|
||||
|
||||
|
||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
|
||||
events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user5Id).assertEvent();
|
||||
|
||||
UserRepresentation user = getUser(user5Id);
|
||||
assertEquals("FirstCC", user.getFirstName());
|
||||
assertEquals("LastCC", user.getLastName());
|
||||
assertEquals("DepartmentCC", user.firstAttribute(ATTRIBUTE_DEPARTMENT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAttributeNotRequiredWhenMissingScope() {
|
||||
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\"profile\"]}}"
|
||||
+ "]}");
|
||||
|
||||
updateUser(user5Id, "ExistingFirst", "ExistingLast", null);
|
||||
|
||||
oauth.clientId(client_scope_optional.getClientId()).openLoginForm();
|
||||
|
||||
loginPage.login("login-test5", "password");
|
||||
|
||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
|
||||
UserRepresentation user = getUser(user5Id);
|
||||
assertEquals("ExistingFirst", user.getFirstName());
|
||||
assertEquals("ExistingLast", user.getLastName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAttributeRequiredForScope() {
|
||||
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\""+SCOPE_DEPARTMENT+"\"]}}"
|
||||
+ "]}");
|
||||
|
||||
updateUser(user5Id, "ExistingFirst", "ExistingLast", null);
|
||||
|
||||
oauth.scope(SCOPE_DEPARTMENT).clientId(client_scope_optional.getClientId()).openLoginForm();
|
||||
|
||||
loginPage.assertCurrent();
|
||||
loginPage.login("login-test5", "password");
|
||||
|
||||
verifyProfilePage.assertCurrent();
|
||||
|
||||
verifyProfilePage.update("FirstAA", "LastAA", "DepartmentAA");
|
||||
|
||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
|
||||
events.expectRequiredAction(EventType.VERIFY_PROFILE).client(client_scope_optional).user(user5Id).assertEvent();
|
||||
|
||||
UserRepresentation user = getUser(user5Id);
|
||||
assertEquals("FirstAA", user.getFirstName());
|
||||
assertEquals("LastAA", user.getLastName());
|
||||
assertEquals("DepartmentAA", user.firstAttribute(ATTRIBUTE_DEPARTMENT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAttributeRequiredForDefaultScope() {
|
||||
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\""+SCOPE_DEPARTMENT+"\"]}}"
|
||||
+ "]}");
|
||||
|
||||
updateUser(user5Id, "ExistingFirst", "ExistingLast", null);
|
||||
|
||||
oauth.clientId(client_scope_default.getClientId()).openLoginForm();
|
||||
|
||||
loginPage.assertCurrent();
|
||||
loginPage.login("login-test5", "password");
|
||||
|
||||
verifyProfilePage.assertCurrent();
|
||||
|
||||
//submit with error
|
||||
verifyProfilePage.update("FirstBB", "LastBB", " ");
|
||||
verifyProfilePage.assertCurrent();
|
||||
|
||||
//submit OK
|
||||
verifyProfilePage.update("FirstBB", "LastBB", "DepartmentBB");
|
||||
|
||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
|
||||
events.expectRequiredAction(EventType.VERIFY_PROFILE).client(client_scope_default).user(user5Id).assertEvent();
|
||||
|
||||
UserRepresentation user = getUser(user5Id);
|
||||
assertEquals("FirstBB", user.getFirstName());
|
||||
assertEquals("LastBB", user.getLastName());
|
||||
assertEquals("DepartmentBB", user.firstAttribute(ATTRIBUTE_DEPARTMENT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoActionIfValidForScope() {
|
||||
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\""+SCOPE_DEPARTMENT+"\"]}}"
|
||||
+ "]}");
|
||||
|
||||
updateUser(user5Id, "ExistingFirst", "ExistingLast", "ExistingDepartment");
|
||||
|
||||
oauth.clientId(client_scope_default.getClientId()).openLoginForm();
|
||||
|
||||
loginPage.assertCurrent();
|
||||
loginPage.login("login-test5", "password");
|
||||
|
||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
|
||||
UserRepresentation user = getUser(user5Id);
|
||||
assertEquals("ExistingFirst", user.getFirstName());
|
||||
assertEquals("ExistingLast", user.getLastName());
|
||||
assertEquals("ExistingDepartment", user.firstAttribute(ATTRIBUTE_DEPARTMENT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomValidationInCustomAttribute() {
|
||||
|
||||
setUserProfileConfiguration(CONFIGURATION_FOR_USER_EDIT);
|
||||
updateUser(user5Id, "ExistingFirst", "ExistingLast", "D");
|
||||
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + ", "+VALIDATIONS_LENGTH+"}"
|
||||
+ "]}");
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login("login-test5", "password");
|
||||
|
||||
verifyProfilePage.assertCurrent();
|
||||
|
||||
//submit with error
|
||||
verifyProfilePage.update("FirstCC", "LastCC", "De");
|
||||
verifyProfilePage.assertCurrent();
|
||||
|
||||
//submit OK
|
||||
verifyProfilePage.update("FirstCC", "LastCC", "DepartmentCC");
|
||||
|
||||
|
||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
|
||||
events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user5Id).assertEvent();
|
||||
|
||||
UserRepresentation user = getUser(user5Id);
|
||||
assertEquals("FirstCC", user.getFirstName());
|
||||
assertEquals("LastCC", user.getLastName());
|
||||
assertEquals("DepartmentCC", user.firstAttribute(ATTRIBUTE_DEPARTMENT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoActionIfSuccessfulValidationForCustomAttribute() {
|
||||
|
||||
setUserProfileConfiguration(CONFIGURATION_FOR_USER_EDIT);
|
||||
updateUser(user5Id, "ExistingFirst", "ExistingLast", "Department");
|
||||
|
||||
setUserProfileConfiguration("{\"attributes\": ["
|
||||
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + ", "+VALIDATIONS_LENGTH+"}"
|
||||
+ "]}");
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login("login-test5", "password");
|
||||
|
||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||
}
|
||||
|
||||
protected UserRepresentation getUser(String userId) {
|
||||
return testRealm().users().get(userId).toRepresentation();
|
||||
}
|
||||
|
||||
protected void updateUser(String userId, String firstName, String lastName, String department) {
|
||||
UserRepresentation ur = testRealm().users().get(userId).toRepresentation();
|
||||
ur.setFirstName(firstName);
|
||||
ur.setLastName(lastName);
|
||||
ur.singleAttribute(ATTRIBUTE_DEPARTMENT, department);
|
||||
testRealm().users().get(userId).update(ur);
|
||||
}
|
||||
|
||||
protected void setUserProfileConfiguration(String configuration) {
|
||||
Response r = testRealm().users().userProfile().update(configuration);
|
||||
if (r.getStatus() != 200) {
|
||||
Assert.fail("Configuration not set due to error: " + r.readEntity(String.class));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -30,7 +30,7 @@ import org.keycloak.models.UserModel;
|
|||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.user.profile.config.DeclarativeUserProfileProvider;
|
||||
import org.keycloak.userprofile.config.DeclarativeUserProfileProvider;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
|
||||
/**
|
||||
|
@ -39,7 +39,6 @@ import org.keycloak.userprofile.UserProfileProvider;
|
|||
public abstract class AbstractUserProfileTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
protected static void configureAuthenticationSession(KeycloakSession session) {
|
||||
configureSessionRealm(session);
|
||||
Set<String> scopes = new HashSet<>();
|
||||
|
||||
scopes.add("customer");
|
||||
|
@ -53,16 +52,12 @@ public abstract class AbstractUserProfileTest extends AbstractTestRealmKeycloakT
|
|||
session.getContext().setAuthenticationSession(createAuthenticationSession(realm.getClientByClientId(clientId), requestedScopes));
|
||||
}
|
||||
|
||||
protected static RealmModel configureSessionRealm(KeycloakSession session) {
|
||||
RealmModel realm = session.realms().getRealm(TEST_REALM_NAME);
|
||||
|
||||
session.getContext().setRealm(realm);
|
||||
|
||||
return realm;
|
||||
}
|
||||
|
||||
protected static DeclarativeUserProfileProvider getDynamicUserProfileProvider(KeycloakSession session) {
|
||||
return (DeclarativeUserProfileProvider) session.getProvider(UserProfileProvider.class, DeclarativeUserProfileProvider.ID);
|
||||
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||
|
||||
provider.setConfiguration(null);
|
||||
|
||||
return (DeclarativeUserProfileProvider) provider;
|
||||
}
|
||||
|
||||
protected static AuthenticationSessionModel createAuthenticationSession(ClientModel client, Set<String> scopes) {
|
||||
|
|
|
@ -1,633 +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.testsuite.user.profile;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.keycloak.testsuite.user.profile.config.UPConfigUtils.ROLE_USER;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.component.ComponentValidationException;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.runonserver.RunOnServer;
|
||||
import org.keycloak.testsuite.user.profile.config.DeclarativeUserProfileProvider;
|
||||
import org.keycloak.testsuite.user.profile.config.UPAttribute;
|
||||
import org.keycloak.testsuite.user.profile.config.UPAttributeRequired;
|
||||
import org.keycloak.testsuite.user.profile.config.UPConfig;
|
||||
import org.keycloak.testsuite.user.profile.config.UPConfigUtils;
|
||||
import org.keycloak.testsuite.util.KeycloakModelUtils;
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.userprofile.ValidationException;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.validate.validators.LengthValidator;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class UserProfileConfigTest extends AbstractUserProfileTest {
|
||||
|
||||
protected static final String ATT_ADDRESS = "address";
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
KeycloakModelUtils.createClient(testRealm, "client-a");
|
||||
KeycloakModelUtils.createClient(testRealm, "client-b");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConfigurationSetInvalid() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testConfigurationSetInvalid);
|
||||
}
|
||||
|
||||
private static void testConfigurationSetInvalid(KeycloakSession session) {
|
||||
configureSessionRealm(session);
|
||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
|
||||
try {
|
||||
provider.setConfiguration("{\"validateConfigAttribute\": true}");
|
||||
fail("Should fail validation");
|
||||
} catch (ComponentValidationException ve) {
|
||||
// OK
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConfigurationGetSet() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testConfigurationGetSet);
|
||||
}
|
||||
|
||||
private static void testConfigurationGetSet(KeycloakSession session) throws IOException {
|
||||
configureSessionRealm(session);
|
||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
ComponentModel component = provider.getComponentModel();
|
||||
|
||||
assertNotNull(component);
|
||||
|
||||
// generate big configuration to test slicing in the persistence/component config
|
||||
UPConfig config = new UPConfig();
|
||||
for (int i = 0; i < 80; i++) {
|
||||
UPAttribute attribute = new UPAttribute();
|
||||
attribute.setName(UserModel.USERNAME+i);
|
||||
Map<String, Object> validatorConfig = new HashMap<>();
|
||||
validatorConfig.put("min", 3);
|
||||
attribute.addValidation("length", validatorConfig);
|
||||
config.addAttribute(attribute);
|
||||
}
|
||||
String newConfig = JsonSerialization.writeValueAsString(config);
|
||||
|
||||
provider.setConfiguration(newConfig);
|
||||
// assert config is persisted in 2 pieces
|
||||
Assert.assertEquals("2", component.get(DeclarativeUserProfileProvider.UP_PIECES_COUNT_COMPONENT_CONFIG_KEY));
|
||||
// assert config is returned correctly
|
||||
Assert.assertEquals(newConfig, provider.getConfiguration());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConfigurationGetSetDefault() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testConfigurationGetSetDefault);
|
||||
}
|
||||
|
||||
private static void testConfigurationGetSetDefault(KeycloakSession session) throws IOException {
|
||||
configureSessionRealm(session);
|
||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
|
||||
provider.setConfiguration(null);
|
||||
|
||||
Assert.assertNull(provider.getComponentModel().get(DeclarativeUserProfileProvider.UP_PIECES_COUNT_COMPONENT_CONFIG_KEY));
|
||||
|
||||
ComponentModel component = provider.getComponentModel();
|
||||
|
||||
assertNotNull(component);
|
||||
|
||||
Assert.assertTrue(component.getConfig().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultConfigForUpdateProfile() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testDefaultConfigForUpdateProfile);
|
||||
}
|
||||
|
||||
private static void testDefaultConfigForUpdateProfile(KeycloakSession session) throws IOException {
|
||||
configureSessionRealm(session);
|
||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
|
||||
// reset configuration to default
|
||||
provider.setConfiguration(null);
|
||||
|
||||
// failed required validations
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, Collections.emptyMap());
|
||||
|
||||
try {
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
|
||||
}
|
||||
|
||||
// failed for blank values also
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
attributes.put(UserModel.FIRST_NAME, "");
|
||||
attributes.put(UserModel.LAST_NAME, " ");
|
||||
attributes.put(UserModel.EMAIL, "");
|
||||
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
|
||||
try {
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
|
||||
assertTrue(ve.isAttributeOnError(UserModel.FIRST_NAME));
|
||||
assertTrue(ve.isAttributeOnError(UserModel.LAST_NAME));
|
||||
assertTrue(ve.isAttributeOnError(UserModel.EMAIL));
|
||||
}
|
||||
|
||||
// all OK
|
||||
attributes.put(UserModel.USERNAME, "jdoeusername");
|
||||
attributes.put(UserModel.FIRST_NAME, "John");
|
||||
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||
attributes.put(UserModel.EMAIL, "jdoe@acme.org");
|
||||
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAdditionalValidationForUsername() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testAdditionalValidationForUsername);
|
||||
}
|
||||
|
||||
private static void testAdditionalValidationForUsername(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(UserModel.USERNAME);
|
||||
|
||||
Map<String, Object> validatorConfig = new HashMap<>();
|
||||
|
||||
validatorConfig.put("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, "us");
|
||||
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
|
||||
try {
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
|
||||
assertTrue(ve.hasError(LengthValidator.MESSAGE_INVALID_LENGTH));
|
||||
}
|
||||
|
||||
attributes.put(UserModel.USERNAME, "user");
|
||||
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
|
||||
profile.validate();
|
||||
|
||||
provider.setConfiguration(null);
|
||||
|
||||
attributes.put(UserModel.USERNAME, "us");
|
||||
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
|
||||
profile.validate();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstLastNameCanBeOptional() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testFirstLastNameCanBeOptional);
|
||||
}
|
||||
|
||||
private static void testFirstLastNameCanBeOptional(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(UserModel.FIRST_NAME);
|
||||
Map<String, Object> validatorConfig = new HashMap<>();
|
||||
validatorConfig.put(LengthValidator.KEY_MAX, 4);
|
||||
attribute.addValidation(LengthValidator.ID, validatorConfig);
|
||||
config.addAttribute(attribute);
|
||||
|
||||
attribute = new UPAttribute();
|
||||
attribute.setName(UserModel.LAST_NAME);
|
||||
attribute.addValidation(LengthValidator.ID, validatorConfig);
|
||||
config.addAttribute(attribute);
|
||||
|
||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
attributes.put(UserModel.USERNAME, "user");
|
||||
|
||||
// not present attributes are OK
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
|
||||
//empty attributes are OK
|
||||
attributes.put(UserModel.FIRST_NAME, "");
|
||||
attributes.put(UserModel.LAST_NAME, "");
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
|
||||
//filled attributes are OK
|
||||
attributes.put(UserModel.FIRST_NAME, "John");
|
||||
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
|
||||
// fails due to additional length validation so it is executed correctly
|
||||
attributes.put(UserModel.FIRST_NAME, "JohnTooLong");
|
||||
attributes.put(UserModel.LAST_NAME, "DoeTooLong");
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
try {
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(UserModel.FIRST_NAME));
|
||||
assertTrue(ve.isAttributeOnError(UserModel.LAST_NAME));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomAttribute_Required() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testCustomAttribute_Required);
|
||||
}
|
||||
|
||||
private static void testCustomAttribute_Required(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);
|
||||
|
||||
// make it ALWAYS required
|
||||
UPAttributeRequired requirements = new UPAttributeRequired();
|
||||
attribute.setRequired(requirements);
|
||||
|
||||
config.addAttribute(attribute);
|
||||
|
||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
attributes.put(UserModel.USERNAME, "user");
|
||||
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
|
||||
// fails on required validation
|
||||
try {
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
attributes.put(ATT_ADDRESS, "adress ok");
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequiredByUserRole_USER() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testRequiredByUserRole_USER);
|
||||
}
|
||||
|
||||
private static void testRequiredByUserRole_USER(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);
|
||||
|
||||
UPAttributeRequired requirements = new UPAttributeRequired();
|
||||
|
||||
List<String> roles = new ArrayList<>();
|
||||
roles.add(ROLE_USER);
|
||||
requirements.setRoles(roles);
|
||||
|
||||
attribute.setRequired(requirements);
|
||||
|
||||
config.addAttribute(attribute);
|
||||
|
||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
attributes.put(UserModel.USERNAME, "user");
|
||||
|
||||
// fail on common contexts
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
try {
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||
try {
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
|
||||
profile = provider.create(UserProfileContext.REGISTRATION_PROFILE, attributes);
|
||||
try {
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
|
||||
// no fail on User API
|
||||
profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||
profile.validate();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequiredByUserRole_ADMIN() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testRequiredByUserRole_ADMIN);
|
||||
}
|
||||
|
||||
private static void testRequiredByUserRole_ADMIN(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);
|
||||
|
||||
UPAttributeRequired requirements = new UPAttributeRequired();
|
||||
|
||||
List<String> roles = new ArrayList<>();
|
||||
roles.add(UPConfigUtils.ROLE_ADMIN);
|
||||
requirements.setRoles(roles);
|
||||
|
||||
attribute.setRequired(requirements);
|
||||
|
||||
config.addAttribute(attribute);
|
||||
|
||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
attributes.put(UserModel.USERNAME, "user");
|
||||
|
||||
// NO fail on common contexts
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||
profile.validate();
|
||||
|
||||
profile = provider.create(UserProfileContext.REGISTRATION_PROFILE, attributes);
|
||||
profile.validate();
|
||||
|
||||
// fail on User API
|
||||
try {
|
||||
profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequiredByScope_clientDefaultScope() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testRequiredByScope_clientDefaultScope);
|
||||
}
|
||||
|
||||
private static void testRequiredByScope_clientDefaultScope(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);
|
||||
|
||||
UPAttributeRequired requirements = new UPAttributeRequired();
|
||||
|
||||
List<String> scopes = new ArrayList<>();
|
||||
scopes.add("client-a");
|
||||
requirements.setScopes(scopes);
|
||||
|
||||
attribute.setRequired(requirements);
|
||||
|
||||
config.addAttribute(attribute);
|
||||
|
||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
attributes.put(UserModel.USERNAME, "user");
|
||||
|
||||
// client with default scopes for which is attribute NOT configured as required
|
||||
configureAuthenticationSession(session, "client-b", null);
|
||||
|
||||
// no fail on User API nor Account console as they do not have scopes
|
||||
UserProfile profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.ACCOUNT_OLD, attributes);
|
||||
profile.validate();
|
||||
|
||||
// no fail on auth flow scopes when scope is not required
|
||||
profile = provider.create(UserProfileContext.REGISTRATION_PROFILE, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.REGISTRATION_USER_CREATION, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.IDP_REVIEW, attributes);
|
||||
profile.validate();
|
||||
|
||||
// client with default scope for which is attribute configured as required
|
||||
configureAuthenticationSession(session, "client-a", null);
|
||||
|
||||
// no fail on User API nor Account console as they do not have scopes
|
||||
profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.ACCOUNT_OLD, attributes);
|
||||
profile.validate();
|
||||
|
||||
// fail on auth flow scopes when scope is required
|
||||
try {
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
try {
|
||||
profile = provider.create(UserProfileContext.REGISTRATION_PROFILE, attributes);
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
try {
|
||||
profile = provider.create(UserProfileContext.REGISTRATION_USER_CREATION, attributes);
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
try {
|
||||
profile = provider.create(UserProfileContext.IDP_REVIEW, attributes);
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -26,6 +26,9 @@ import static org.junit.Assert.assertFalse;
|
|||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_ADMIN;
|
||||
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_USER;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
@ -38,19 +41,26 @@ import java.util.Map;
|
|||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.component.ComponentValidationException;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
|
||||
import org.keycloak.testsuite.runonserver.RunOnServer;
|
||||
import org.keycloak.testsuite.user.profile.config.UPAttribute;
|
||||
import org.keycloak.testsuite.user.profile.config.UPAttributeRequired;
|
||||
import org.keycloak.testsuite.user.profile.config.UPConfig;
|
||||
import org.keycloak.userprofile.UserProfileSpi;
|
||||
import org.keycloak.userprofile.config.DeclarativeUserProfileProvider;
|
||||
import org.keycloak.userprofile.config.UPAttribute;
|
||||
import org.keycloak.userprofile.config.UPAttributePermissions;
|
||||
import org.keycloak.userprofile.config.UPAttributeRequired;
|
||||
import org.keycloak.userprofile.config.UPConfig;
|
||||
import org.keycloak.testsuite.util.ClientScopeBuilder;
|
||||
import org.keycloak.testsuite.util.KeycloakModelUtils;
|
||||
import org.keycloak.userprofile.Attributes;
|
||||
|
@ -58,13 +68,19 @@ import org.keycloak.userprofile.UserProfile;
|
|||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
import org.keycloak.userprofile.ValidationException;
|
||||
import org.keycloak.userprofile.config.UPConfigUtils;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.validate.ValidationError;
|
||||
import org.keycloak.validate.validators.EmailValidator;
|
||||
import org.keycloak.validate.validators.LengthValidator;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
@EnableFeature(Profile.Feature.DECLARATIVE_USER_PROFILE)
|
||||
@SetDefaultProvider(spi = UserProfileSpi.ID, providerId = DeclarativeUserProfileProvider.ID,
|
||||
beforeEnableFeature = false,
|
||||
onlyUpdateDefault = true)
|
||||
public class UserProfileTest extends AbstractUserProfileTest {
|
||||
|
||||
@Override
|
||||
|
@ -72,21 +88,12 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
testRealm.setClientScopes(Collections.singletonList(ClientScopeBuilder.create().name("customer").protocol("openid-connect").build()));
|
||||
ClientRepresentation client = KeycloakModelUtils.createClient(testRealm, "client-a");
|
||||
client.setDefaultClientScopes(Collections.singletonList("customer"));
|
||||
}
|
||||
|
||||
@After
|
||||
public void onAfter() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileTest::resetConfiguration);
|
||||
}
|
||||
|
||||
private static void resetConfiguration(KeycloakSession session) {
|
||||
configureSessionRealm(session);
|
||||
getDynamicUserProfileProvider(session).setConfiguration(null);
|
||||
KeycloakModelUtils.createClient(testRealm, "client-b");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIdempotentProfile() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testIdempotentProfile);
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testIdempotentProfile);
|
||||
}
|
||||
|
||||
private static void testIdempotentProfile(KeycloakSession session) {
|
||||
|
@ -103,7 +110,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
|
||||
@Test
|
||||
public void testCustomAttributeInAnyContext() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testCustomAttributeInAnyContext);
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testCustomAttributeInAnyContext);
|
||||
}
|
||||
|
||||
private static void testCustomAttributeInAnyContext(KeycloakSession session) {
|
||||
|
@ -113,7 +120,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
|
||||
UserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
|
||||
provider.setConfiguration("{\"attributes\": [{\"name\": \"address\", \"required\": {}}]}");
|
||||
provider.setConfiguration("{\"attributes\": [{\"name\": \"address\", \"required\": {}, \"permissions\": {\"edit\": [\"user\"]}}]}");
|
||||
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
|
||||
|
@ -137,7 +144,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
|
||||
@Test
|
||||
public void testResolveProfile() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testResolveProfile);
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testResolveProfile);
|
||||
}
|
||||
|
||||
private static void testResolveProfile(KeycloakSession session) {
|
||||
|
@ -149,7 +156,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
|
||||
UserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
|
||||
provider.setConfiguration("{\"attributes\": [{\"name\": \"business.address\", \"required\": {\"scopes\": [\"customer\"]}}]}");
|
||||
provider.setConfiguration("{\"attributes\": [{\"name\": \"business.address\", \"required\": {\"scopes\": [\"customer\"]}, \"permissions\": {\"edit\": [\"user\"]}}]}");
|
||||
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
|
||||
|
@ -173,13 +180,14 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
|
||||
@Test
|
||||
public void testValidation() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileTest::failValidationWhenEmptyAttributes);
|
||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testAttributeValidation);
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::failValidationWhenEmptyAttributes);
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testAttributeValidation);
|
||||
}
|
||||
|
||||
private static void failValidationWhenEmptyAttributes(KeycloakSession session) {
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||
provider.setConfiguration(null);
|
||||
UserProfile profile;
|
||||
|
||||
try {
|
||||
|
@ -207,6 +215,8 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
try {
|
||||
realm.setRegistrationEmailAsUsername(true);
|
||||
attributes.clear();
|
||||
attributes.put(UserModel.FIRST_NAME, "Joe");
|
||||
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||
attributes.put(UserModel.EMAIL, "profile-user@keycloak.org");
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
|
@ -219,6 +229,8 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
|
||||
attributes.clear();
|
||||
attributes.put(UserModel.USERNAME, "profile-user");
|
||||
attributes.put(UserModel.FIRST_NAME, "Joe");
|
||||
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||
provider.create(UserProfileContext.UPDATE_PROFILE, attributes).validate();
|
||||
}
|
||||
|
||||
|
@ -252,11 +264,11 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
|
||||
@Test
|
||||
public void testValidateComplianceWithUserProfile() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testValidateComplianceWithUserProfile);
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testValidateComplianceWithUserProfile);
|
||||
}
|
||||
|
||||
private static void testValidateComplianceWithUserProfile(KeycloakSession session) throws IOException {
|
||||
RealmModel realm = configureSessionRealm(session);
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
UserModel user = session.users().addUser(realm, "profiled-user");
|
||||
UserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
|
||||
|
@ -269,6 +281,10 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
|
||||
attribute.setRequired(requirements);
|
||||
|
||||
UPAttributePermissions permissions = new UPAttributePermissions();
|
||||
permissions.setEdit(Collections.singletonList(ROLE_USER));
|
||||
attribute.setPermissions(permissions);
|
||||
|
||||
config.addAttribute(attribute);
|
||||
|
||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||
|
@ -292,15 +308,15 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
|
||||
@Test
|
||||
public void testGetProfileAttributes() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testGetProfileAttributes);
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testGetProfileAttributes);
|
||||
}
|
||||
|
||||
private static void testGetProfileAttributes(KeycloakSession session) {
|
||||
RealmModel realm = configureSessionRealm(session);
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
UserModel user = session.users().addUser(realm, org.keycloak.models.utils.KeycloakModelUtils.generateId());
|
||||
UserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
|
||||
provider.setConfiguration("{\"attributes\": [{\"name\": \"address\", \"required\": {}}]}");
|
||||
provider.setConfiguration("{\"attributes\": [{\"name\": \"address\", \"required\": {}, \"permissions\": {\"edit\": [\"user\"]}}]}");
|
||||
|
||||
UserProfile profile = provider.create(UserProfileContext.ACCOUNT, user);
|
||||
Attributes attributes = profile.getAttributes();
|
||||
|
@ -334,7 +350,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
|
||||
@Test
|
||||
public void testCreateAndUpdateUser() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testCreateAndUpdateUser);
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testCreateAndUpdateUser);
|
||||
}
|
||||
|
||||
private static void testCreateAndUpdateUser(KeycloakSession session) {
|
||||
|
@ -343,6 +359,8 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
String userName = org.keycloak.models.utils.KeycloakModelUtils.generateId();
|
||||
|
||||
attributes.put(UserModel.USERNAME, userName);
|
||||
attributes.put(UserModel.FIRST_NAME, "Joe");
|
||||
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||
attributes.put("address", "fixed-address");
|
||||
|
||||
UserProfile profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||
|
@ -377,12 +395,10 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
|
||||
@Test
|
||||
public void testReadonlyUpdates() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testReadonlyUpdates);
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testReadonlyUpdates);
|
||||
}
|
||||
|
||||
private static void testReadonlyUpdates(KeycloakSession session) {
|
||||
configureSessionRealm(session);
|
||||
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
attributes.put(UserModel.USERNAME, org.keycloak.models.utils.KeycloakModelUtils.generateId());
|
||||
|
@ -415,10 +431,684 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes, user);
|
||||
|
||||
try {
|
||||
profile.update();
|
||||
fail("Should fail due to read only attribute");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError("department"));
|
||||
}
|
||||
|
||||
assertEquals("sales", user.getFirstAttribute("department"));
|
||||
|
||||
assertTrue(profile.getAttributes().isReadOnly("department"));
|
||||
}
|
||||
|
||||
protected static final String ATT_ADDRESS = "address";
|
||||
|
||||
@Test
|
||||
public void testInvalidConfiguration() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testInvalidConfiguration);
|
||||
}
|
||||
|
||||
private static void testInvalidConfiguration(KeycloakSession session) {
|
||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
|
||||
try {
|
||||
provider.setConfiguration("{\"validateConfigAttribute\": true}");
|
||||
fail("Should fail validation");
|
||||
} catch (ComponentValidationException ve) {
|
||||
// OK
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConfigurationChunks() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testConfigurationChunks);
|
||||
}
|
||||
|
||||
private static void testConfigurationChunks(KeycloakSession session) throws IOException {
|
||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
ComponentModel component = provider.getComponentModel();
|
||||
|
||||
assertNotNull(component);
|
||||
|
||||
// generate big configuration to test slicing in the persistence/component config
|
||||
UPConfig config = new UPConfig();
|
||||
for (int i = 0; i < 80; i++) {
|
||||
UPAttribute attribute = new UPAttribute();
|
||||
attribute.setName(UserModel.USERNAME+i);
|
||||
Map<String, Object> validatorConfig = new HashMap<>();
|
||||
validatorConfig.put("min", 3);
|
||||
attribute.addValidation("length", validatorConfig);
|
||||
config.addAttribute(attribute);
|
||||
}
|
||||
String newConfig = JsonSerialization.writeValueAsString(config);
|
||||
|
||||
provider.setConfiguration(newConfig);
|
||||
|
||||
component = provider.getComponentModel();
|
||||
|
||||
// assert config is persisted in 2 pieces
|
||||
Assert.assertEquals("2", component.get(DeclarativeUserProfileProvider.UP_PIECES_COUNT_COMPONENT_CONFIG_KEY));
|
||||
// assert config is returned correctly
|
||||
Assert.assertEquals(newConfig, provider.getConfiguration());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResetConfiguration() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testResetConfiguration);
|
||||
}
|
||||
|
||||
private static void testResetConfiguration(KeycloakSession session) throws IOException {
|
||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
|
||||
provider.setConfiguration(null);
|
||||
|
||||
Assert.assertNull(provider.getComponentModel().get(DeclarativeUserProfileProvider.UP_PIECES_COUNT_COMPONENT_CONFIG_KEY));
|
||||
|
||||
ComponentModel component = provider.getComponentModel();
|
||||
|
||||
assertNotNull(component);
|
||||
|
||||
Assert.assertTrue(component.getConfig().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultConfig() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testDefaultConfig);
|
||||
}
|
||||
|
||||
private static void testDefaultConfig(KeycloakSession session) {
|
||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
|
||||
// reset configuration to default
|
||||
provider.setConfiguration(null);
|
||||
|
||||
// failed required validations
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, Collections.emptyMap());
|
||||
|
||||
try {
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
|
||||
}
|
||||
|
||||
// failed for blank values also
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
attributes.put(UserModel.FIRST_NAME, "");
|
||||
attributes.put(UserModel.LAST_NAME, " ");
|
||||
attributes.put(UserModel.EMAIL, "");
|
||||
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
|
||||
try {
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
|
||||
assertTrue(ve.isAttributeOnError(UserModel.FIRST_NAME));
|
||||
assertTrue(ve.isAttributeOnError(UserModel.LAST_NAME));
|
||||
assertTrue(ve.isAttributeOnError(UserModel.EMAIL));
|
||||
}
|
||||
|
||||
// all OK
|
||||
attributes.put(UserModel.USERNAME, "jdoeusername");
|
||||
attributes.put(UserModel.FIRST_NAME, "John");
|
||||
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||
attributes.put(UserModel.EMAIL, "jdoe@acme.org");
|
||||
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomValidationForUsername() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testCustomValidationForUsername);
|
||||
}
|
||||
|
||||
private static void testCustomValidationForUsername(KeycloakSession session) throws IOException {
|
||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
ComponentModel component = provider.getComponentModel();
|
||||
|
||||
assertNotNull(component);
|
||||
|
||||
UPConfig config = new UPConfig();
|
||||
UPAttribute attribute = new UPAttribute();
|
||||
|
||||
attribute.setName(UserModel.USERNAME);
|
||||
|
||||
Map<String, Object> validatorConfig = new HashMap<>();
|
||||
|
||||
validatorConfig.put("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, "us");
|
||||
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
|
||||
try {
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
|
||||
assertTrue(ve.hasError(LengthValidator.MESSAGE_INVALID_LENGTH));
|
||||
}
|
||||
|
||||
attributes.put(UserModel.USERNAME, "user");
|
||||
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
|
||||
profile.validate();
|
||||
|
||||
provider.setConfiguration(null);
|
||||
|
||||
attributes.put(UserModel.USERNAME, "us");
|
||||
attributes.put(UserModel.FIRST_NAME, "Joe");
|
||||
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
|
||||
profile.validate();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOptionalAttributes() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testOptionalAttributes);
|
||||
}
|
||||
|
||||
private static void testOptionalAttributes(KeycloakSession session) throws IOException {
|
||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
ComponentModel component = provider.getComponentModel();
|
||||
|
||||
assertNotNull(component);
|
||||
|
||||
UPConfig config = new UPConfig();
|
||||
UPAttribute attribute = new UPAttribute();
|
||||
attribute.setName(UserModel.FIRST_NAME);
|
||||
Map<String, Object> validatorConfig = new HashMap<>();
|
||||
validatorConfig.put(LengthValidator.KEY_MAX, 4);
|
||||
attribute.addValidation(LengthValidator.ID, validatorConfig);
|
||||
config.addAttribute(attribute);
|
||||
|
||||
attribute = new UPAttribute();
|
||||
attribute.setName(UserModel.LAST_NAME);
|
||||
attribute.addValidation(LengthValidator.ID, validatorConfig);
|
||||
config.addAttribute(attribute);
|
||||
|
||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
attributes.put(UserModel.USERNAME, "user");
|
||||
|
||||
// not present attributes are OK
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
|
||||
//empty attributes are OK
|
||||
attributes.put(UserModel.FIRST_NAME, "");
|
||||
attributes.put(UserModel.LAST_NAME, "");
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
|
||||
//filled attributes are OK
|
||||
attributes.put(UserModel.FIRST_NAME, "John");
|
||||
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
|
||||
// fails due to additional length validation so it is executed correctly
|
||||
attributes.put(UserModel.FIRST_NAME, "JohnTooLong");
|
||||
attributes.put(UserModel.LAST_NAME, "DoeTooLong");
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
try {
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(UserModel.FIRST_NAME));
|
||||
assertTrue(ve.isAttributeOnError(UserModel.LAST_NAME));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomAttributeRequired() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testCustomAttributeRequired);
|
||||
}
|
||||
|
||||
private static void testCustomAttributeRequired(KeycloakSession session) throws IOException {
|
||||
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);
|
||||
|
||||
// make it ALWAYS required
|
||||
UPAttributeRequired requirements = new UPAttributeRequired();
|
||||
attribute.setRequired(requirements);
|
||||
|
||||
UPAttributePermissions permissions = new UPAttributePermissions();
|
||||
permissions.setEdit(Collections.singletonList(ROLE_USER));
|
||||
attribute.setPermissions(permissions);
|
||||
|
||||
config.addAttribute(attribute);
|
||||
|
||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
attributes.put(UserModel.USERNAME, "user");
|
||||
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
|
||||
// fails on required validation
|
||||
try {
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
|
||||
// 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
|
||||
attributes.put(ATT_ADDRESS, "adress ok");
|
||||
attributes.put(UserModel.FIRST_NAME, "Joe");
|
||||
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomAttributeOptional() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testCustomAttributeOptional);
|
||||
}
|
||||
|
||||
private static void testCustomAttributeOptional(KeycloakSession session) throws IOException {
|
||||
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
|
||||
attributes.put(ATT_ADDRESS, "adress ok");
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequiredIfUser() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testRequiredIfUser);
|
||||
}
|
||||
|
||||
private static void testRequiredIfUser(KeycloakSession session) throws IOException {
|
||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
ComponentModel component = provider.getComponentModel();
|
||||
|
||||
assertNotNull(component);
|
||||
|
||||
UPConfig config = new UPConfig();
|
||||
UPAttribute attribute = new UPAttribute();
|
||||
|
||||
attribute.setName(ATT_ADDRESS);
|
||||
|
||||
UPAttributeRequired requirements = new UPAttributeRequired();
|
||||
|
||||
List<String> roles = new ArrayList<>();
|
||||
roles.add(ROLE_USER);
|
||||
requirements.setRoles(roles);
|
||||
|
||||
attribute.setRequired(requirements);
|
||||
|
||||
UPAttributePermissions permissions = new UPAttributePermissions();
|
||||
permissions.setEdit(Collections.singletonList(ROLE_USER));
|
||||
attribute.setPermissions(permissions);
|
||||
|
||||
config.addAttribute(attribute);
|
||||
|
||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
attributes.put(UserModel.USERNAME, "user");
|
||||
|
||||
// fail on common contexts
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
try {
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||
try {
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
|
||||
profile = provider.create(UserProfileContext.REGISTRATION_PROFILE, attributes);
|
||||
try {
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
|
||||
attributes.put(UserModel.FIRST_NAME, "Joe");
|
||||
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||
|
||||
// no fail on User API
|
||||
profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||
profile.validate();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequiredIfAdmin() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testRequiredIfAdmin);
|
||||
}
|
||||
|
||||
private static void testRequiredIfAdmin(KeycloakSession session) throws IOException {
|
||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
ComponentModel component = provider.getComponentModel();
|
||||
|
||||
assertNotNull(component);
|
||||
|
||||
UPConfig config = new UPConfig();
|
||||
UPAttribute attribute = new UPAttribute();
|
||||
|
||||
attribute.setName(ATT_ADDRESS);
|
||||
|
||||
UPAttributeRequired requirements = new UPAttributeRequired();
|
||||
|
||||
requirements.setRoles(Collections.singletonList(ROLE_ADMIN));
|
||||
|
||||
attribute.setRequired(requirements);
|
||||
|
||||
UPAttributePermissions permissions = new UPAttributePermissions();
|
||||
permissions.setEdit(Collections.singletonList(UPConfigUtils.ROLE_ADMIN));
|
||||
attribute.setPermissions(permissions);
|
||||
|
||||
config.addAttribute(attribute);
|
||||
|
||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
attributes.put(UserModel.USERNAME, "user");
|
||||
|
||||
// NO fail on common contexts
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||
profile.validate();
|
||||
|
||||
profile = provider.create(UserProfileContext.REGISTRATION_PROFILE, attributes);
|
||||
profile.validate();
|
||||
|
||||
// fail on User API
|
||||
try {
|
||||
profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoValidationsIfUserReadOnly() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testNoValidationsIfUserReadOnly);
|
||||
}
|
||||
|
||||
private static void testNoValidationsIfUserReadOnly(KeycloakSession session) throws IOException {
|
||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
ComponentModel component = provider.getComponentModel();
|
||||
|
||||
assertNotNull(component);
|
||||
|
||||
UPConfig config = new UPConfig();
|
||||
UPAttribute attribute = new UPAttribute();
|
||||
|
||||
attribute.setName(ATT_ADDRESS);
|
||||
|
||||
UPAttributeRequired requirements = new UPAttributeRequired();
|
||||
attribute.setRequired(requirements);
|
||||
|
||||
UPAttributePermissions permissions = new UPAttributePermissions();
|
||||
permissions.setEdit(Collections.singletonList(UPConfigUtils.ROLE_ADMIN));
|
||||
attribute.setPermissions(permissions);
|
||||
|
||||
config.addAttribute(attribute);
|
||||
|
||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
attributes.put(UserModel.USERNAME, "user");
|
||||
attributes.put(UserModel.FIRST_NAME, "user");
|
||||
attributes.put(UserModel.LAST_NAME, "user");
|
||||
|
||||
// NO fail on USER contexts
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
|
||||
// Fails on ADMIN context - User REST API
|
||||
try {
|
||||
profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoValidationsIfAdminReadOnly() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testNoValidationsIfAdminReadOnly);
|
||||
}
|
||||
|
||||
private static void testNoValidationsIfAdminReadOnly(KeycloakSession session) throws IOException {
|
||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
ComponentModel component = provider.getComponentModel();
|
||||
|
||||
assertNotNull(component);
|
||||
|
||||
UPConfig config = new UPConfig();
|
||||
UPAttribute attribute = new UPAttribute();
|
||||
|
||||
attribute.setName(ATT_ADDRESS);
|
||||
|
||||
UPAttributeRequired requirements = new UPAttributeRequired();
|
||||
attribute.setRequired(requirements);
|
||||
|
||||
UPAttributePermissions permissions = new UPAttributePermissions();
|
||||
List<String> roles = new ArrayList<>();
|
||||
roles.add(UPConfigUtils.ROLE_USER);
|
||||
permissions.setEdit(roles);
|
||||
attribute.setPermissions(permissions);
|
||||
|
||||
config.addAttribute(attribute);
|
||||
|
||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
attributes.put(UserModel.USERNAME, "user");
|
||||
|
||||
// Fails on USER context
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
try {
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
|
||||
// NO fail on ADMIN context - User REST API
|
||||
profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||
profile.validate();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequiredByClientScope() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testRequiredByClientScope);
|
||||
}
|
||||
|
||||
private static void testRequiredByClientScope(KeycloakSession session) throws IOException {
|
||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
ComponentModel component = provider.getComponentModel();
|
||||
|
||||
assertNotNull(component);
|
||||
|
||||
UPConfig config = new UPConfig();
|
||||
UPAttribute attribute = new UPAttribute();
|
||||
|
||||
attribute.setName(ATT_ADDRESS);
|
||||
|
||||
UPAttributeRequired requirements = new UPAttributeRequired();
|
||||
|
||||
List<String> scopes = new ArrayList<>();
|
||||
scopes.add("client-a");
|
||||
requirements.setScopes(scopes);
|
||||
|
||||
attribute.setRequired(requirements);
|
||||
|
||||
UPAttributePermissions permissions = new UPAttributePermissions();
|
||||
permissions.setEdit(Collections.singletonList("user"));
|
||||
attribute.setPermissions(permissions);
|
||||
|
||||
config.addAttribute(attribute);
|
||||
|
||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
attributes.put(UserModel.USERNAME, "user");
|
||||
|
||||
// client with default scopes for which is attribute NOT configured as required
|
||||
configureAuthenticationSession(session, "client-b", null);
|
||||
|
||||
// no fail on User API nor Account console as they do not have scopes
|
||||
UserProfile profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.ACCOUNT_OLD, attributes);
|
||||
profile.validate();
|
||||
|
||||
// no fail on auth flow scopes when scope is not required
|
||||
profile = provider.create(UserProfileContext.REGISTRATION_PROFILE, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.REGISTRATION_USER_CREATION, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.IDP_REVIEW, attributes);
|
||||
profile.validate();
|
||||
|
||||
// client with default scope for which is attribute configured as required
|
||||
configureAuthenticationSession(session, "client-a", null);
|
||||
|
||||
// no fail on User API nor Account console as they do not have scopes
|
||||
profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||
profile.validate();
|
||||
profile = provider.create(UserProfileContext.ACCOUNT_OLD, attributes);
|
||||
profile.validate();
|
||||
|
||||
// fail on auth flow scopes when scope is required
|
||||
try {
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
try {
|
||||
profile = provider.create(UserProfileContext.REGISTRATION_PROFILE, attributes);
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
try {
|
||||
profile = provider.create(UserProfileContext.IDP_REVIEW, attributes);
|
||||
profile.validate();
|
||||
fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@
|
|||
*/
|
||||
package org.keycloak.testsuite.user.profile.config;
|
||||
|
||||
import static org.keycloak.testsuite.user.profile.config.UPConfigUtils.readConfig;
|
||||
import static org.keycloak.testsuite.user.profile.config.UPConfigUtils.validate;
|
||||
import static org.keycloak.userprofile.config.UPConfigUtils.readConfig;
|
||||
import static org.keycloak.userprofile.config.UPConfigUtils.validate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
@ -35,6 +35,11 @@ import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
|||
import org.keycloak.testsuite.runonserver.RunOnServer;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import org.keycloak.userprofile.config.UPAttribute;
|
||||
import org.keycloak.userprofile.config.UPAttributePermissions;
|
||||
import org.keycloak.userprofile.config.UPAttributeRequired;
|
||||
import org.keycloak.userprofile.config.UPConfig;
|
||||
import org.keycloak.userprofile.config.UPConfigUtils;
|
||||
|
||||
/**
|
||||
* Unit test for {@link UPConfigParser} functionality
|
||||
|
|
|
@ -16,8 +16,8 @@
|
|||
*/
|
||||
package org.keycloak.testsuite.user.profile.config;
|
||||
|
||||
import static org.keycloak.testsuite.user.profile.config.UPConfigUtils.ROLE_ADMIN;
|
||||
import static org.keycloak.testsuite.user.profile.config.UPConfigUtils.ROLE_USER;
|
||||
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_ADMIN;
|
||||
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_USER;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -25,6 +25,7 @@ import java.util.List;
|
|||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.userprofile.config.UPConfigUtils;
|
||||
|
||||
/**
|
||||
* Unit test for {@link UPConfigUtils}
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
*
|
||||
* * 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.testsuite.validation;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.runonserver.RunOnServer;
|
||||
import org.keycloak.validate.ValidationContext;
|
||||
import org.keycloak.validate.Validators;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class ValidatorTest extends AbstractTestRealmKeycloakTest {
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
testRealm.user("alice");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDateValidator() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) ValidatorTest::testDateValidator);
|
||||
}
|
||||
|
||||
private static void testDateValidator(KeycloakSession session) {
|
||||
assertTrue(Validators.dateValidator().validate(null, new ValidationContext(session)).isValid());
|
||||
assertTrue(Validators.dateValidator().validate("", new ValidationContext(session)).isValid());
|
||||
|
||||
// defaults to Locale.ENGLISH as per default locale selector
|
||||
assertFalse(Validators.dateValidator().validate("13/12/2021", new ValidationContext(session)).isValid());
|
||||
assertFalse(Validators.dateValidator().validate("13/12/21", new ValidationContext(session)).isValid());
|
||||
assertTrue(Validators.dateValidator().validate("12/13/2021", new ValidationContext(session)).isValid());
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
realm.setInternationalizationEnabled(true);
|
||||
realm.setDefaultLocale(Locale.FRANCE.getLanguage());
|
||||
|
||||
assertTrue(Validators.dateValidator().validate("13/12/21", new ValidationContext(session)).isValid());
|
||||
assertTrue(Validators.dateValidator().validate("13/12/2021", new ValidationContext(session)).isValid());
|
||||
assertFalse(Validators.dateValidator().validate("12/13/2021", new ValidationContext(session)).isValid());
|
||||
|
||||
UserModel alice = session.users().getUserByUsername(realm, "alice");
|
||||
|
||||
alice.setAttribute(UserModel.LOCALE, Collections.singletonList(Locale.ENGLISH.getLanguage()));
|
||||
|
||||
ValidationContext context = new ValidationContext(session);
|
||||
|
||||
context.getAttributes().put(UserModel.class.getName(), alice);
|
||||
|
||||
assertFalse(Validators.dateValidator().validate("13/12/2021", context).isValid());
|
||||
}
|
||||
}
|
|
@ -219,9 +219,14 @@
|
|||
},
|
||||
|
||||
"userProfile": {
|
||||
"provider": "${keycloak.userProfile.provider:}",
|
||||
"legacy-user-profile": {
|
||||
"read-only-attributes": [ "deniedFoo", "deniedBar*", "deniedSome/thing", "deniedsome*thing" ],
|
||||
"admin-read-only-attributes": [ "deniedSomeAdmin" ]
|
||||
},
|
||||
"declarative-user-profile": {
|
||||
"read-only-attributes": [ "deniedFoo", "deniedBar*", "deniedSome/thing", "deniedsome*thing" ],
|
||||
"admin-read-only-attributes": [ "deniedSomeAdmin" ]
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -139,9 +139,14 @@
|
|||
},
|
||||
|
||||
"userProfile": {
|
||||
"provider": "${keycloak.userProfile.provider:}",
|
||||
"legacy-user-profile": {
|
||||
"read-only-attributes": [ "deniedFoo", "deniedBar*", "deniedSome/thing", "deniedsome*thing" ],
|
||||
"admin-read-only-attributes": [ "deniedSomeAdmin" ]
|
||||
},
|
||||
"declarative-user-profile": {
|
||||
"read-only-attributes": [ "deniedFoo", "deniedBar*", "deniedSome/thing", "deniedsome*thing" ],
|
||||
"admin-read-only-attributes": [ "deniedSomeAdmin" ]
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -374,3 +374,16 @@ openshift.scope.user_info=User information
|
|||
openshift.scope.user_check-access=User access information
|
||||
openshift.scope.user_full=Full Access
|
||||
openshift.scope.list-projects=List projects
|
||||
|
||||
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-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-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}.
|
|
@ -221,6 +221,7 @@ realm-tab-cache=Cache
|
|||
realm-tab-tokens=Tokens
|
||||
realm-tab-client-registration=Client Registration
|
||||
realm-tab-security-defenses=Security Defenses
|
||||
realm-tab-user-profile=User Profile
|
||||
realm-tab-general=General
|
||||
add-realm=Add realm
|
||||
|
||||
|
@ -1882,3 +1883,23 @@ dialogs.delete.message=Are you sure you want to permanently delete the {{type}}
|
|||
dialogs.delete.confirm=Delete
|
||||
dialogs.cancel=Cancel
|
||||
dialogs.ok=Ok
|
||||
|
||||
user.profile.attribute=Attribute
|
||||
user.profile.attribute.name=Name
|
||||
user.profile.attribute.name.tooltip=The name of the attribute.
|
||||
user.profile.attribute.required=Required
|
||||
user.profile.attribute.required.tooltip=Set the attribute as required. If enabled, the attribute must be set by users and administrators. Otherwise, the attribute is optional.
|
||||
user.profile.attribute.permission=Permission
|
||||
user.profile.attribute.canUserView=Can user view?
|
||||
user.profile.attribute.canUserView.tooltip=If enabled, users can view the attribute. Otherwise, users don't have access to the attribute.
|
||||
user.profile.attribute.canUserEdit=Can user edit?
|
||||
user.profile.attribute.canUserEdit.tooltip=If enabled, users can view and edit the attribute. Otherwise, users don't have access to write to the attribute.
|
||||
user.profile.attribute.canAdminView=Can admin view?
|
||||
user.profile.attribute.canAdminView.tooltip=If enabled, administrators can view the attribute. Otherwise, administrators don't have access to the attribute.
|
||||
user.profile.attribute.canAdminEdit=Can admin edit?
|
||||
user.profile.attribute.canAdminEdit.tooltip=If enabled, administrators can view and edit the attribute. Otherwise, administrators don't have access to write to the attribute.
|
||||
user.profile.attribute.validation=Validation
|
||||
user.profile.attribute.validation.add.validator=Add Validator
|
||||
user.profile.attribute.validation.add.validator.tooltip=Select a validator to enforce specific constraints to the attribute value.
|
||||
user.profile.attribute.validation.no.validators=No validators.
|
||||
user.profile.attribute.annotation=Annotation
|
|
@ -39,3 +39,19 @@ pairwiseClientRedirectURIsMultipleHosts=Without a configured Sector Identifier U
|
|||
pairwiseMalformedSectorIdentifierURI=Malformed Sector Identifier URI.
|
||||
pairwiseFailedToGetRedirectURIs=Failed to get redirect URIs from the Sector Identifier URI.
|
||||
pairwiseRedirectURIsMismatch=Client redirect URIs does not match redirect URIs fetched from the Sector Identifier URI.
|
||||
|
||||
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-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-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=Attribute {0} is read only.
|
|
@ -260,6 +260,18 @@ module.config([ '$routeProvider', function($routeProvider) {
|
|||
},
|
||||
controller : 'RealmTokenDetailCtrl'
|
||||
})
|
||||
.when('/realms/:realm/user-profile', {
|
||||
templateUrl : resourceUrl + '/partials/realm-user-profile.html',
|
||||
resolve : {
|
||||
serverInfo : function(ServerInfoLoader) {
|
||||
return ServerInfoLoader();
|
||||
},
|
||||
realm : function(RealmLoader) {
|
||||
return RealmLoader();
|
||||
}
|
||||
},
|
||||
controller : 'RealmUserProfileCtrl'
|
||||
})
|
||||
.when('/realms/:realm/client-registration/client-initial-access', {
|
||||
templateUrl : resourceUrl + '/partials/client-initial-access.html',
|
||||
resolve : {
|
||||
|
@ -2433,6 +2445,14 @@ module.factory('errorInterceptor', function($q, $window, $rootScope, $location,
|
|||
} else if (response.status) {
|
||||
if (response.data && response.data.errorMessage) {
|
||||
Notifications.error(response.data.errorMessage);
|
||||
} else if (response.data && response.data.errors) {
|
||||
var messages = "Multiple errors found: ";
|
||||
|
||||
for (var i = 0; i < response.data.errors.length; i++) {
|
||||
messages+=response.data.errors[i].errorMessage + " ";
|
||||
}
|
||||
|
||||
Notifications.error(messages);
|
||||
} else if (response.data && response.data.error_description) {
|
||||
Notifications.error(response.data.error_description);
|
||||
} else {
|
||||
|
|
|
@ -1401,6 +1401,232 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
|
|||
};
|
||||
});
|
||||
|
||||
module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, $http, $location, $route, UserProfile, Dialog, Notifications, serverInfo) {
|
||||
$scope.realm = realm;
|
||||
$scope.validatorProviders = serverInfo.componentTypes['org.keycloak.validate.Validator'];
|
||||
|
||||
$scope.isShowAttributes = true;
|
||||
|
||||
UserProfile.get({realm: realm.realm}, function(config) {
|
||||
$scope.config = config;
|
||||
$scope.rawConfig = angular.toJson(config, true);
|
||||
});
|
||||
|
||||
$scope.isShowAttributes = true;
|
||||
|
||||
$scope.showAttributes = function() {
|
||||
$route.reload();
|
||||
}
|
||||
|
||||
$scope.showJsonEditor = function() {
|
||||
$scope.isShowAttributes = false;
|
||||
}
|
||||
|
||||
$scope.canViewPermission = {
|
||||
minimumInputLength: 0,
|
||||
delay: 500,
|
||||
allowClear: true,
|
||||
query: function (query) {
|
||||
query.callback({results: ['user', 'admin']});
|
||||
},
|
||||
formatResult: function(object, container, query) {
|
||||
return object;
|
||||
},
|
||||
formatSelection: function(object, container, query) {
|
||||
return object;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.canEditPermission = {
|
||||
minimumInputLength: 0,
|
||||
delay: 500,
|
||||
allowClear: true,
|
||||
query: function (query) {
|
||||
query.callback({results: ['user', 'admin']});
|
||||
},
|
||||
formatResult: function(object, container, query) {
|
||||
return object;
|
||||
},
|
||||
formatSelection: function(object, container, query) {
|
||||
return object;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.attributeSelected = false;
|
||||
|
||||
$scope.showListing = function() {
|
||||
return !$scope.attributeSelected && $scope.currentAttribute == null && $scope.isShowAttributes;
|
||||
}
|
||||
|
||||
$scope.create = function() {
|
||||
$scope.isCreate = true;
|
||||
$scope.currentAttribute = {
|
||||
permissions: {
|
||||
view: [],
|
||||
edit: []
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
$scope.removeAttribute = function(attribute) {
|
||||
Dialog.confirmDelete(attribute.name, 'attribute', function() {
|
||||
let newAttributes = [];
|
||||
|
||||
for (var v of $scope.config.attributes) {
|
||||
if (v != attribute) {
|
||||
newAttributes.push(v);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.config.attributes = newAttributes;
|
||||
$scope.save();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.addAnnotation = function() {
|
||||
if (!$scope.currentAttribute.annotations) {
|
||||
$scope.currentAttribute.annotations = {};
|
||||
}
|
||||
$scope.currentAttribute.annotations[$scope.newAnnotation.key] = $scope.newAnnotation.value;
|
||||
delete $scope.newAnnotation;
|
||||
}
|
||||
|
||||
$scope.removeAnnotation = function(key) {
|
||||
delete $scope.currentAttribute.annotations[key];
|
||||
}
|
||||
|
||||
$scope.edit = function(attribute) {
|
||||
if (attribute.permissions == null) {
|
||||
attribute.permissions = {
|
||||
view: [],
|
||||
edit: []
|
||||
};
|
||||
}
|
||||
|
||||
$scope.isRequired = attribute.required != null;
|
||||
$scope.canUserView = attribute.permissions.view.includes('user');
|
||||
$scope.canAdminView = attribute.permissions.view.includes('admin');
|
||||
$scope.canUserEdit = attribute.permissions.edit.includes('user');
|
||||
$scope.canAdminEdit = attribute.permissions.edit.includes('admin');
|
||||
$scope.currentAttribute = attribute;
|
||||
$scope.attributeSelected = true;
|
||||
};
|
||||
|
||||
$scope.$watch('isRequired', function() {
|
||||
if ($scope.isRequired) {
|
||||
$scope.currentAttribute.required = {};
|
||||
} else {
|
||||
delete $scope.currentAttribute.required;
|
||||
}
|
||||
}, true);
|
||||
|
||||
handlePermission = function(permission, role, allowed) {
|
||||
let attribute = $scope.currentAttribute;
|
||||
let roles = [];
|
||||
|
||||
for (let r of attribute.permissions[permission]) {
|
||||
if (r != role) {
|
||||
roles.push(r);
|
||||
}
|
||||
}
|
||||
|
||||
if (allowed) {
|
||||
roles.push(role);
|
||||
}
|
||||
|
||||
attribute.permissions[permission] = roles;
|
||||
}
|
||||
|
||||
$scope.$watch('canUserView', function() {
|
||||
handlePermission('view', 'user', $scope.canUserView);
|
||||
}, true);
|
||||
|
||||
$scope.$watch('canAdminView', function() {
|
||||
handlePermission('view', 'admin', $scope.canAdminView);
|
||||
}, true);
|
||||
|
||||
$scope.$watch('canUserEdit', function() {
|
||||
handlePermission('edit', 'user', $scope.canUserEdit);
|
||||
}, true);
|
||||
|
||||
$scope.$watch('canAdminEdit', function() {
|
||||
handlePermission('edit', 'admin', $scope.canAdminEdit);
|
||||
}, true);
|
||||
|
||||
$scope.addValidator = function(validator) {
|
||||
if ($scope.currentAttribute.validations == null) {
|
||||
$scope.currentAttribute.validations = {};
|
||||
}
|
||||
|
||||
let config = {};
|
||||
|
||||
for (let key in validator.config) {
|
||||
let values = validator.config[key];
|
||||
|
||||
for (let k in values) {
|
||||
config[key] = values[k];
|
||||
}
|
||||
}
|
||||
|
||||
$scope.currentAttribute.validations[validator.id] = config;
|
||||
|
||||
delete $scope.newValidator;
|
||||
};
|
||||
|
||||
$scope.selectValidator = function(validator) {
|
||||
validator.config = {};
|
||||
};
|
||||
|
||||
$scope.cancelAddValidator = function() {
|
||||
delete $scope.newValidator;
|
||||
};
|
||||
|
||||
$scope.removeValidator = function(id) {
|
||||
let newValidators = {};
|
||||
|
||||
for (let v in $scope.currentAttribute.validations) {
|
||||
if (v != id) {
|
||||
newValidators[v] = $scope.currentAttribute.validations[v];
|
||||
}
|
||||
}
|
||||
|
||||
if (newValidators.length == 0) {
|
||||
delete $scope.currentAttribute.validations;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.currentAttribute.validations = newValidators;
|
||||
};
|
||||
|
||||
$scope.save = function() {
|
||||
if (!$scope.isShowAttributes) {
|
||||
$scope.config = JSON.parse($scope.rawConfig);
|
||||
}
|
||||
|
||||
if ($scope.isCreate && $scope.currentAttribute) {
|
||||
$scope.config['attributes'].push($scope.currentAttribute);
|
||||
}
|
||||
|
||||
UserProfile.update({realm: realm.realm},
|
||||
$scope.config, function () {
|
||||
$scope.attributeSelected = false;
|
||||
delete $scope.currentAttribute;
|
||||
delete $scope.isCreate;
|
||||
delete $scope.isRequired;
|
||||
delete $scope.canUserView;
|
||||
delete $scope.canAdminView;
|
||||
delete $scope.canUserEdit;
|
||||
delete $scope.canAdminEdit;
|
||||
$route.reload();
|
||||
Notifications.success("The attribute has been added.");
|
||||
});
|
||||
};
|
||||
|
||||
$scope.reset = function() {
|
||||
$route.reload();
|
||||
};
|
||||
});
|
||||
|
||||
module.controller('ViewKeyCtrl', function($scope, key) {
|
||||
$scope.key = key;
|
||||
});
|
||||
|
|
|
@ -2112,6 +2112,16 @@ module.factory('UserGroupMapping', function($resource) {
|
|||
});
|
||||
});
|
||||
|
||||
module.factory('UserProfile', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/users/profile', {
|
||||
realm : '@realm'
|
||||
}, {
|
||||
update : {
|
||||
method : 'PUT'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.factory('DefaultGroups', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/default-groups/:groupId', {
|
||||
realm : '@realm',
|
||||
|
|
|
@ -0,0 +1,209 @@
|
|||
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||
<kc-tabs-realm></kc-tabs-realm>
|
||||
|
||||
<ul class="nav nav-tabs nav-tabs-pf">
|
||||
<li ng-class="{active: isShowAttributes}"><a href="" data-ng-click="showAttributes()">{{:: 'attributes' | translate}}</a></li>
|
||||
<li ng-class="{active: !isShowAttributes}"><a href=""
|
||||
data-ng-click="showJsonEditor()">{{:: 'client-profiles-json-editor' | translate}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div data-ng-show="showListing()">
|
||||
<table class="datatable table table-striped table-bordered dataTable no-footer">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="kc-table-actions" colspan="3">
|
||||
<div class="form-inline">
|
||||
<div class="pull-right" data-ng-show="access.manageClients">
|
||||
<button class="btn btn-default" data-ng-click="create()">
|
||||
{{:: 'create' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th width="90%">{{:: 'name' | translate}}</th>
|
||||
<th colspan="2">{{:: 'actions' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="attribute in config.attributes">
|
||||
<td><a href="" data-ng-click="edit(attribute)">{{attribute.name}}</a></td>
|
||||
<td class="kc-action-cell" data-ng-click="edit(attribute)">{{:: 'edit' | translate}}</td>
|
||||
<td class="kc-action-cell" data-ng-click="removeAttribute(attribute)">{{:: 'delete' | translate}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm"
|
||||
data-ng-show="!isShowAttributes">
|
||||
<filedset>
|
||||
<div class="form-group">
|
||||
<div class="col-md-10">
|
||||
<div>
|
||||
<textarea id="userProfileConfig" name="userProfileConfig" data-ng-model="rawConfig"
|
||||
class="form-control ng-binding" rows="20"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-10">
|
||||
<button class="btn btn-primary" data-ng-click="save()">{{:: 'save' | translate}}</button>
|
||||
<button class="btn btn-default" data-ng-click="reset()">{{:: 'cancel' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</filedset>
|
||||
|
||||
</form>
|
||||
|
||||
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm"
|
||||
data-ng-show="currentAttribute != null">
|
||||
<p/>
|
||||
<legend expanded><span class="text">{{:: 'user.profile.attribute' | translate}} <b
|
||||
data-ng-show="!isCreate">{{currentAttribute.name}}</b> {{:: 'configuration' | translate}}</span>
|
||||
</legend>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="currentAttribute.name">{{:: 'user.profile.attribute.name' | translate}}</label>
|
||||
<kc-tooltip>{{:: 'user.profile.attribute.name.tooltip' | translate}}</kc-tooltip>
|
||||
<div class="col-md-4">
|
||||
<input name="currentAttribute.name" data-ng-model="currentAttribute.name" id="currentAttribute.name"
|
||||
type="text" class="form-control"
|
||||
data-ng-readonly="!isCreate"
|
||||
required/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="isRequired">{{:: 'user.profile.attribute.required' | translate}}</label>
|
||||
<kc-tooltip>{{:: 'user.profile.attribute.required.tooltip' | translate}}</kc-tooltip>
|
||||
<div class="col-md-6">
|
||||
<input name="isRequired" data-ng-model="isRequired" id="isRequired" onoffswitch
|
||||
on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset class="border-top">
|
||||
<legend collapsed><span class="text">{{:: 'user.profile.attribute.permission' | translate}}</span></legend>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="canUserView">{{:: 'user.profile.attribute.canUserView' | translate}}</label>
|
||||
<kc-tooltip>{{:: 'user.profile.attribute.canUserView.tooltip' | translate}}</kc-tooltip>
|
||||
<div class="col-md-2">
|
||||
<input name="canUserView" data-ng-model="canUserView" id="canUserView" onoffswitch
|
||||
on-text="{{:: 'onText' | translate}}"
|
||||
off-text="{{:: 'offText' | translate}}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="canAdminView">{{:: 'user.profile.attribute.canAdminView' | translate}}</label>
|
||||
<kc-tooltip>{{:: 'user.profile.attribute.canAdminView.tooltip' | translate}}</kc-tooltip>
|
||||
<div class="col-md-6">
|
||||
<input name="canAdminView" data-ng-model="canAdminView" id="canAdminView" onoffswitch
|
||||
on-text="{{:: 'onText' | translate}}"
|
||||
off-text="{{:: 'offText' | translate}}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="canUserEdit">{{:: 'user.profile.attribute.canUserEdit' | translate}}</label>
|
||||
<kc-tooltip>{{:: 'user.profile.attribute.canUserEdit.tooltip' | translate}}</kc-tooltip>
|
||||
<div class="col-md-6">
|
||||
<input name="canUserEdit" data-ng-model="canUserEdit" id="canUserEdit" onoffswitch
|
||||
on-text="{{:: 'onText' | translate}}"
|
||||
off-text="{{:: 'offText' | translate}}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="canAdminEdit">{{:: 'user.profile.attribute.canAdminEdit' | translate}}</label>
|
||||
<kc-tooltip>{{:: 'user.profile.attribute.canAdminEdit.tooltip' | translate}}</kc-tooltip>
|
||||
<div class="col-md-6">
|
||||
<input name="canAdminEdit" data-ng-model="canAdminEdit" id="canAdminEdit" onoffswitch
|
||||
on-text="{{:: 'onText' | translate}}"
|
||||
off-text="{{:: 'offText' | translate}}"/>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend collapsed><span class="text">{{:: 'user.profile.attribute.validation' | translate}}</span></legend>
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="create-validator">{{:: 'user.profile.attribute.validation.add.validator' | translate}}</label>
|
||||
<kc-tooltip>{{:: 'user.profile.attribute.validation.add.validator.tooltip' | translate}}</kc-tooltip>
|
||||
<div class="col-sm-4">
|
||||
<select id="create-validator" class="form-control" ng-model="newValidator"
|
||||
ng-options="p.id for p in validatorProviders"
|
||||
data-ng-change="selectValidator(newValidator)">
|
||||
<option value="" disabled selected>{{:: 'user.profile.attribute.validation.add.validator' | translate}}...</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<kc-component-config realm="realm" config="newValidator.config"
|
||||
properties="newValidator.properties"></kc-component-config>
|
||||
<p/>
|
||||
<div class="form-group" data-ng-show="newValidator != null">
|
||||
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
|
||||
<button class="btn btn-primary" data-ng-click="addValidator(newValidator)">{{:: 'add' | translate}}</button>
|
||||
<button class="btn btn-primary" data-ng-click="cancelAddValidator()">{{:: 'cancel' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-sm-10">
|
||||
<table class="datatable table table-striped table-bordered dataTable no-footer">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="20%">{{:: 'name' | translate}}</th>
|
||||
<th>{{:: 'config' | translate}}</th>
|
||||
<th colspan="2">{{:: 'actions' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="(key, value) in currentAttribute.validations">
|
||||
<td>{{key}}</td>
|
||||
<td>{{value}}</td>
|
||||
<td class="kc-action-cell" data-ng-click="removeValidator(key)">{{:: 'delete' | translate}}</td>
|
||||
</tr>
|
||||
<tr data-ng-show="!currentAttribute.validations || currentAttribute.validations.length == 0">
|
||||
<td class="text-muted" colspan="3">{{:: 'user.profile.attribute.validation.no.validators' | translate}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend collapsed><span class="text">{{:: 'user.profile.attribute.annotation' | translate}}</span></legend>
|
||||
<div class="form-group col-sm-10">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{:: 'key' | translate}}</th>
|
||||
<th>{{:: 'value' | translate}}</th>
|
||||
<th>{{:: 'actions' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="(key, value) in currentAttribute.annotations | toOrderedMapSortedByKey">
|
||||
<td>{{key}}</td>
|
||||
<td><input ng-model="currentAttribute.annotations[key]" class="form-control" type="text" name="{{key}}"
|
||||
id="attribute-{{key}}"/></td>
|
||||
<td class="kc-action-cell" data-ng-click="removeAnnotation(key)">{{:: 'delete' | translate}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><input ng-model="newAnnotation.key" class="form-control" type="text" id="newAnnotationKey"/></td>
|
||||
<td><input ng-model="newAnnotation.value" class="form-control" type="text" id="newAnnotationValue"/></td>
|
||||
<td class="kc-action-cell" data-ng-click="addAnnotation()"
|
||||
data-ng-disabled="!newAnnotation.key.length || !newAnnotation.value.length">{{:: 'add' | translate}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="form-group">
|
||||
<p/>
|
||||
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
|
||||
<button kc-save>{{:: 'save' | translate}}</button>
|
||||
<button kc-reset>{{:: 'cancel' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<kc-menu></kc-menu>
|
|
@ -19,5 +19,6 @@
|
|||
<a href="#/realms/{{realm.realm}}/client-policies/profiles">{{:: 'realm-tab-client-policies' | translate}}</a>
|
||||
</li>
|
||||
<li ng-class="{active: path[2] == 'defense'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/defense/headers">{{:: 'realm-tab-security-defenses' | translate}}</a></li>
|
||||
<li ng-class="{active: path[2] == 'user-profile'}" data-ng-show="access.viewRealm && serverInfo.featureEnabled('DECLARATIVE_USER_PROFILE')"><a href="#/realms/{{realm.realm}}/user-profile">{{:: 'realm-tab-user-profile' | translate}}</a></li>
|
||||
</ul>
|
||||
</div>
|
47
themes/src/main/resources/theme/base/login/verify-profile.ftl
Executable file
47
themes/src/main/resources/theme/base/login/verify-profile.ftl
Executable file
|
@ -0,0 +1,47 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','email','firstName','lastName'); section>
|
||||
<#if section = "header">
|
||||
${msg("loginProfileTitle")}
|
||||
<#elseif section = "form">
|
||||
<form id="kc-update-profile-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
|
||||
|
||||
<#list profile.attributes as attribute>
|
||||
<div class="${properties.kcFormGroupClass!}">
|
||||
<div class="${properties.kcLabelWrapperClass!}">
|
||||
<label for="${attribute.name}" class="${properties.kcLabelClass!}">${msg("${attribute.name}")}</label>
|
||||
<#if attribute.required>*</#if>
|
||||
</div>
|
||||
<div class="${properties.kcInputWrapperClass!}">
|
||||
<input type="text" id="${attribute.name}" name="${attribute.name}" value="${(attribute.value!'')}"
|
||||
class="${properties.kcInputClass!}"
|
||||
aria-invalid="<#if messagesPerField.existsError('${attribute.name}')>true</#if>"
|
||||
<#if attribute.readOnly>disabled</#if>
|
||||
/>
|
||||
|
||||
<#if messagesPerField.existsError('${attribute.name}')>
|
||||
<span id="input-error-${attribute.name}" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
|
||||
${kcSanitize(messagesPerField.get('${attribute.name}'))?no_esc}
|
||||
</span>
|
||||
</#if>
|
||||
</div>
|
||||
</div>
|
||||
</#list>
|
||||
|
||||
<div class="${properties.kcFormGroupClass!}">
|
||||
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
|
||||
<div class="${properties.kcFormOptionsWrapperClass!}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
|
||||
<#if isAppInitiatedAction??>
|
||||
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
|
||||
<button class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" type="submit" name="cancel-aia" value="true" />${msg("doCancel")}</button>
|
||||
<#else>
|
||||
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
|
||||
</#if>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</#if>
|
||||
</@layout.registrationLayout>
|
|
@ -119,3 +119,16 @@ doDelete=Delete
|
|||
deleteAccountSummary=Deleting your account will erase all your data and log you out immediately.
|
||||
deleteAccount=Delete Account
|
||||
deleteAccountWarning=This is irreversible. All your data will be permanently destroyed, and irretrievable.
|
||||
|
||||
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-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-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}''.
|
|
@ -105,9 +105,13 @@ export class AccountServiceClient {
|
|||
}
|
||||
|
||||
if (response !== null && response.data != null) {
|
||||
if (response.data['errors'] != null) {
|
||||
for(let err of response.data['errors'])
|
||||
ContentAlert.danger(err['errorMessage'], err['params']);
|
||||
} else {
|
||||
ContentAlert.danger(
|
||||
`${response.statusText}: ${response.data['errorMessage'] ? response.data['errorMessage'] : ''} ${response.data['error'] ? response.data['error'] : ''}`
|
||||
);
|
||||
`${response.statusText}: ${response.data['errorMessage'] ? response.data['errorMessage'] : ''} ${response.data['error'] ? response.data['error'] : ''}`);
|
||||
};
|
||||
} else {
|
||||
ContentAlert.danger(response.statusText);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue