[KEYCLOAK-17399] - Review User Profile SPI
Co-Authored-By: Vlastimil Elias <vlastimil.elias@worldonline.cz>
This commit is contained in:
parent
1c283cdebc
commit
a0f8d2bc0e
80 changed files with 5032 additions and 2005 deletions
|
@ -22,6 +22,7 @@ import org.keycloak.json.StringListMapDeserializer;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -106,4 +107,28 @@ public class UserRepresentation {
|
|||
return this.attributes == null ? null : this.attributes.containsKey(key) ? this.attributes.get(key).get(0) : null;
|
||||
}
|
||||
|
||||
public Map<String, List<String>> toAttributes() {
|
||||
Map<String, List<String>> attrs = new HashMap<>();
|
||||
|
||||
if (getAttributes() != null) attrs.putAll(getAttributes());
|
||||
|
||||
if (getUsername() != null)
|
||||
attrs.put("username", Collections.singletonList(getUsername()));
|
||||
else
|
||||
attrs.remove("username");
|
||||
|
||||
if (getEmail() != null)
|
||||
attrs.put("email", Collections.singletonList(getEmail()));
|
||||
else
|
||||
attrs.remove("email");
|
||||
|
||||
if (getLastName() != null)
|
||||
attrs.put("lastName", Collections.singletonList(getLastName()));
|
||||
|
||||
if (getFirstName() != null)
|
||||
attrs.put("firstName", Collections.singletonList(getFirstName()));
|
||||
|
||||
|
||||
return attrs;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
|||
import org.keycloak.json.StringListMapDeserializer;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
@ -287,4 +288,28 @@ public class UserRepresentation {
|
|||
public void setAccess(Map<String, Boolean> access) {
|
||||
this.access = access;
|
||||
}
|
||||
|
||||
public Map<String, List<String>> toAttributes() {
|
||||
Map<String, List<String>> attrs = new HashMap<>();
|
||||
|
||||
if (getAttributes() != null) attrs.putAll(getAttributes());
|
||||
|
||||
if (getUsername() != null)
|
||||
attrs.put("username", Collections.singletonList(getUsername()));
|
||||
else
|
||||
attrs.remove("username");
|
||||
|
||||
if (getEmail() != null)
|
||||
attrs.put("email", Collections.singletonList(getEmail()));
|
||||
else
|
||||
attrs.remove("email");
|
||||
|
||||
if (getLastName() != null)
|
||||
attrs.put("lastName", Collections.singletonList(getLastName()));
|
||||
|
||||
if (getFirstName() != null)
|
||||
attrs.put("firstName", Collections.singletonList(getFirstName()));
|
||||
|
||||
return attrs;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@ public class UserAdapter implements CachedUserModel.Streams {
|
|||
|
||||
@Override
|
||||
public String getFirstName() {
|
||||
if (updated != null) return updated.getFirstName();
|
||||
return getFirstAttribute(FIRST_NAME);
|
||||
}
|
||||
|
||||
|
@ -71,6 +72,7 @@ public class UserAdapter implements CachedUserModel.Streams {
|
|||
|
||||
@Override
|
||||
public String getLastName() {
|
||||
if (updated != null) return updated.getLastName();
|
||||
return getFirstAttribute(LAST_NAME);
|
||||
}
|
||||
|
||||
|
@ -81,6 +83,7 @@ public class UserAdapter implements CachedUserModel.Streams {
|
|||
|
||||
@Override
|
||||
public String getEmail() {
|
||||
if (updated != null) return updated.getEmail();
|
||||
return getFirstAttribute(EMAIL);
|
||||
}
|
||||
|
||||
|
@ -132,6 +135,7 @@ public class UserAdapter implements CachedUserModel.Streams {
|
|||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
if (updated != null) return updated.getUsername();
|
||||
return getFirstAttribute(UserModel.USERNAME);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package org.keycloak.models.jpa;
|
||||
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.ObjectUtil;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -316,6 +317,9 @@ public class UserAdapter implements UserModel.Streams, JpaModel<UserEntity> {
|
|||
|
||||
@Override
|
||||
public void setEmail(String email) {
|
||||
if (ObjectUtil.isBlank(email)) {
|
||||
email = null;
|
||||
}
|
||||
email = KeycloakModelUtils.toLowerCaseSafe(email);
|
||||
user.setEmail(email, realm.isDuplicateEmailsAllowed());
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package org.keycloak.models.map.user;
|
||||
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.ObjectUtil;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -205,7 +206,14 @@ public abstract class MapUserAdapter<K> extends AbstractUserModel<MapUserEntity<
|
|||
@Override
|
||||
public void setEmail(String email) {
|
||||
email = KeycloakModelUtils.toLowerCaseSafe(email);
|
||||
if (email != null && email.equals(entity.getEmail())) return;
|
||||
if (email != null) {
|
||||
if (email.equals(entity.getEmail())) {
|
||||
return;
|
||||
}
|
||||
if (ObjectUtil.isBlank(email)) {
|
||||
email = null;
|
||||
}
|
||||
}
|
||||
boolean duplicatesAllowed = realm.isDuplicateEmailsAllowed();
|
||||
|
||||
if (!duplicatesAllowed && email != null && checkEmailUniqueness(realm, email)) {
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
*
|
||||
* * Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* * and other contributors as indicated by the @author tags.
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public final class AttributeContext {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final Map.Entry<String, List<String>> attribute;
|
||||
private final UserModel user;
|
||||
private final AttributeMetadata metadata;
|
||||
private UserProfileContext context;
|
||||
|
||||
public AttributeContext(UserProfileContext context, KeycloakSession session, Map.Entry<String, List<String>> attribute,
|
||||
UserModel user, AttributeMetadata metadata) {
|
||||
this.context = context;
|
||||
this.session = session;
|
||||
this.attribute = attribute;
|
||||
this.user = user;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
public KeycloakSession getSession() {
|
||||
return session;
|
||||
}
|
||||
|
||||
public Map.Entry<String, List<String>> getAttribute() {
|
||||
return attribute;
|
||||
}
|
||||
|
||||
public UserModel getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public UserProfileContext getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
public AttributeMetadata getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
*
|
||||
* * Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* * and other contributors as indicated by the @author tags.
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.models.ClientScopeProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public final class AttributeMetadata {
|
||||
|
||||
public static final Predicate<AttributeContext> ALWAYS_TRUE = context -> true;
|
||||
public static final Predicate<AttributeContext> ALWAYS_FALSE = context -> false;
|
||||
|
||||
private final String attributeName;
|
||||
private final Predicate<AttributeContext> selector;
|
||||
private final Predicate<AttributeContext> readOnly;
|
||||
/** Predicate to decide if attribute is required, it is handled as required if predicate is null */
|
||||
private final Predicate<AttributeContext> required;
|
||||
private List<AttributeValidatorMetadata> validators;
|
||||
private Map<String, Object> annotations;
|
||||
|
||||
AttributeMetadata(String attributeName) {
|
||||
this(attributeName, ALWAYS_TRUE, ALWAYS_FALSE, ALWAYS_TRUE);
|
||||
}
|
||||
|
||||
AttributeMetadata(String attributeName, Predicate<AttributeContext> readOnly, Predicate<AttributeContext> required) {
|
||||
this(attributeName, ALWAYS_TRUE, readOnly, required);
|
||||
}
|
||||
|
||||
AttributeMetadata(String attributeName, Predicate<AttributeContext> selector) {
|
||||
this(attributeName, selector, ALWAYS_FALSE, ALWAYS_TRUE);
|
||||
}
|
||||
|
||||
AttributeMetadata(String attributeName, List<String> scopes, Predicate<AttributeContext> readOnly, Predicate<AttributeContext> required) {
|
||||
this(attributeName, context -> {
|
||||
KeycloakSession session = context.getSession();
|
||||
AuthenticationSessionModel authSession = session.getContext().getAuthenticationSession();
|
||||
|
||||
if (authSession == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ClientScopeProvider clientScopes = session.clientScopes();
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
// TODO UserProfile - LOOKS LIKE THIS DOESN'T WORK FOR SOME AUTH FLOWS, LIKE
|
||||
// REGISTER?
|
||||
if (authSession.getClientScopes().stream().anyMatch(scopes::contains)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return authSession.getClientScopes().stream()
|
||||
.map(id -> clientScopes.getClientScopeById(realm, id).getName()).anyMatch(scopes::contains);
|
||||
}, readOnly, required);
|
||||
}
|
||||
|
||||
AttributeMetadata(String attributeName, Predicate<AttributeContext> selector, Predicate<AttributeContext> readOnly, Predicate<AttributeContext> required) {
|
||||
this.attributeName = attributeName;
|
||||
this.selector = selector;
|
||||
this.readOnly = readOnly;
|
||||
this.required = required;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return attributeName;
|
||||
}
|
||||
|
||||
public boolean isSelected(AttributeContext context) {
|
||||
return selector.test(context);
|
||||
}
|
||||
|
||||
public boolean isReadOnly(AttributeContext context) {
|
||||
return readOnly.test(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if attribute is required based on it's predicate, it is handled as required if predicate is null
|
||||
* @param context to evaluate requirement of the attribute from
|
||||
* @return true if attribute is required in provided context
|
||||
*/
|
||||
public boolean isRequired(AttributeContext context) {
|
||||
return required == null || required.test(context);
|
||||
}
|
||||
|
||||
public List<AttributeValidatorMetadata> getValidators() {
|
||||
return validators;
|
||||
}
|
||||
|
||||
public AttributeMetadata addValidator(List<AttributeValidatorMetadata> validators) {
|
||||
if (this.validators == null) {
|
||||
this.validators = new ArrayList<>();
|
||||
}
|
||||
|
||||
this.validators.addAll(validators.stream().filter(Objects::nonNull).collect(Collectors.toList()));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public AttributeMetadata addValidator(AttributeValidatorMetadata validator) {
|
||||
addValidator(Arrays.asList(validator));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Map<String, Object> getAnnotations() {
|
||||
return annotations;
|
||||
}
|
||||
|
||||
public AttributeMetadata addAnnotations(Map<String, Object> annotations) {
|
||||
if(annotations != null) {
|
||||
if(this.annotations == null) {
|
||||
this.annotations = new HashMap<>();
|
||||
}
|
||||
|
||||
this.annotations.putAll(annotations);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttributeMetadata clone() {
|
||||
AttributeMetadata cloned = new AttributeMetadata(attributeName, selector, readOnly, required);
|
||||
// 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) {
|
||||
cloned.addValidator(validators);
|
||||
}
|
||||
//we clone annotations map to allow adding to or removing from it
|
||||
if(annotations != null) {
|
||||
cloned.addAnnotations(annotations);
|
||||
}
|
||||
return cloned;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
*
|
||||
* * Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* * and other contributors as indicated by the @author tags.
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import org.keycloak.userprofile.AttributeContext;
|
||||
import org.keycloak.userprofile.validation.Validator;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public final class AttributeValidatorMetadata implements Validator {
|
||||
|
||||
private final String message;
|
||||
private final Validator validator;
|
||||
|
||||
public AttributeValidatorMetadata(String message, Validator validator) {
|
||||
this.message = message;
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validate(AttributeContext context) {
|
||||
return validator.validate(context);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
*
|
||||
* * Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* * and other contributors as indicated by the @author tags.
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* <p>This interface wraps the attributes associated with a user profile. Different operations are provided to access and
|
||||
* manage these attributes.
|
||||
*
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public interface Attributes {
|
||||
|
||||
/**
|
||||
* Default value for attributes with no value set.
|
||||
*/
|
||||
List<String> EMPTY_VALUE = Collections.emptyList();
|
||||
|
||||
/**
|
||||
* Returns the first value associated with the attribute with the given {@name}.
|
||||
*
|
||||
* @param name the name of the attribute
|
||||
*
|
||||
* @return the first value
|
||||
*/
|
||||
default String getFirstValue(String name) {
|
||||
List<String> values = getValues(name);
|
||||
|
||||
if (values.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return values.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all values for an attribute with the given {@code name}.
|
||||
*
|
||||
* @param name the name of the attribute
|
||||
*
|
||||
* @return the attribute values
|
||||
*/
|
||||
List<String> getValues(String name);
|
||||
|
||||
/**
|
||||
* Checks whether an attribute is read-only.
|
||||
*
|
||||
* @param key
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
boolean isReadOnly(String key);
|
||||
|
||||
/**
|
||||
* <Validates the attribute with the given {@code name}.
|
||||
*
|
||||
* @param name the name of the attribute
|
||||
* @param listeners the listeners for listening for errors
|
||||
*
|
||||
* @return {@code true} if validation is successful. Otherwise, {@code false}. In case there is no attribute with the given {@code name},
|
||||
* {@code false} is also returned but without triggering listeners
|
||||
*/
|
||||
boolean validate(String name, BiConsumer<Map.Entry<String, List<String>>, String>... listeners);
|
||||
|
||||
/**
|
||||
* A simpler variant of {@link #validate(String, BiConsumer[])} for those only interested on error messages.
|
||||
*
|
||||
* @param name the name of the attribute
|
||||
* @param listeners the listeners for listening for errors
|
||||
* @return {@code true} if validation is successful. Otherwise, {@code false}. In case there is no attribute with the given {@code name},
|
||||
* {@code false} is also returned but without triggering listeners
|
||||
*/
|
||||
default boolean validate(String name, Consumer<String>... listeners) {
|
||||
return validate(name, (attribute, error) -> {
|
||||
for (Consumer<String> consumer : listeners) {
|
||||
consumer.accept(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an attribute with the given {@code name} is defined.
|
||||
*
|
||||
* @param name the name of the attribute
|
||||
*
|
||||
* @return {@code true} if the attribute is defined. Otherwise, {@code false}
|
||||
*/
|
||||
boolean contains(String name);
|
||||
|
||||
/**
|
||||
* Returns the names of all defined attributes.
|
||||
*
|
||||
* @return the set of attribute names
|
||||
*/
|
||||
Set<String> nameSet();
|
||||
|
||||
/**
|
||||
* Returns all attributes defined.
|
||||
*
|
||||
* @return the attributes
|
||||
*/
|
||||
Set<Map.Entry<String, List<String>>> attributeSet();
|
||||
}
|
|
@ -0,0 +1,306 @@
|
|||
/*
|
||||
*
|
||||
* * Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* * and other contributors as indicated by the @author tags.
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
/**
|
||||
* <p>The default implementation for {@link Attributes}. Should be reused as much as possible by the different implementations
|
||||
* of {@link UserProfileProvider}.
|
||||
*
|
||||
* <p>One of the main aspects of this implementation is to allow normalizing attributes accordingly to the profile
|
||||
* configuration and current context. As such, it provides some common normalization to common profile attributes (e.g.: username,
|
||||
* email, first and last names, dynamic read-only attributes).
|
||||
*
|
||||
* <p>This implementation is not specific to any user profile implementation.
|
||||
*
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public final 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.
|
||||
* We should probably remove that once we remove the legacy provider, because this will come from the configuration.
|
||||
*/
|
||||
public static final String READ_ONLY_ATTRIBUTE_KEY = "kc.read.only";
|
||||
|
||||
private final UserProfileContext context;
|
||||
private final KeycloakSession session;
|
||||
private final Map<String, AttributeMetadata> metadataByAttribute;
|
||||
private final UserModel user;
|
||||
|
||||
public DefaultAttributes(UserProfileContext context, Map<String, ?> attributes, UserModel user,
|
||||
UserProfileMetadata profileMetadata,
|
||||
KeycloakSession session) {
|
||||
this.context = context;
|
||||
this.user = user;
|
||||
this.session = session;
|
||||
this.metadataByAttribute = configureMetadata(profileMetadata.getAttributes());
|
||||
putAll(Collections.unmodifiableMap(normalizeAttributes(attributes)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadOnly(String attributeName) {
|
||||
return isReadOnlyFromMetadata(attributeName) || isReadOnlyInternalAttribute(attributeName);
|
||||
}
|
||||
|
||||
private boolean isReadOnlyFromMetadata(String attributeName) {
|
||||
AttributeMetadata attributeMetadata = metadataByAttribute.get(attributeName);
|
||||
|
||||
if (attributeMetadata != null && attributeMetadata.isReadOnly(createAttributeContext(attributeName, attributeMetadata))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validate(String name, BiConsumer<Entry<String, List<String>>, String>... listeners) {
|
||||
Entry<String, List<String>> attribute = createAttribute(name);
|
||||
List<AttributeMetadata> metadatas = new ArrayList<>();
|
||||
|
||||
metadatas.addAll(Optional.ofNullable(this.metadataByAttribute.get(attribute.getKey()))
|
||||
.map(Collections::singletonList).orElse(Collections.emptyList()));
|
||||
metadatas.addAll(Optional.ofNullable(this.metadataByAttribute.get(READ_ONLY_ATTRIBUTE_KEY))
|
||||
.map(Collections::singletonList).orElse(Collections.emptyList()));
|
||||
|
||||
List<AttributeValidatorMetadata> failingValidators = Collections.emptyList();
|
||||
|
||||
for (AttributeMetadata metadata : metadatas) {
|
||||
for (AttributeValidatorMetadata validator : metadata.getValidators()) {
|
||||
if (!validator.validate(createAttributeContext(attribute, metadata))) {
|
||||
if (failingValidators.equals(Collections.emptyList())) {
|
||||
failingValidators = new ArrayList<>();
|
||||
}
|
||||
failingValidators.add(validator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (listeners != null) {
|
||||
for (AttributeValidatorMetadata failingValidator : failingValidators) {
|
||||
for (BiConsumer<Entry<String, List<String>>, String> consumer : listeners) {
|
||||
consumer.accept(attribute, failingValidator.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return failingValidators.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getValues(String name) {
|
||||
return getOrDefault(name, EMPTY_VALUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(String name) {
|
||||
return containsKey(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> nameSet() {
|
||||
return keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Entry<String, List<String>>> attributeSet() {
|
||||
return entrySet();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private Map<String, AttributeMetadata> configureMetadata(List<AttributeMetadata> attributes) {
|
||||
Map<String, AttributeMetadata> metadatas = new HashMap<>();
|
||||
|
||||
for (AttributeMetadata metadata : attributes) {
|
||||
// checks whether the attribute is selected for the current profile
|
||||
if (metadata.isSelected(createAttributeContext(metadata.getName(), metadata))) {
|
||||
metadatas.put(metadata.getName(), metadata);
|
||||
}
|
||||
}
|
||||
|
||||
return metadatas;
|
||||
}
|
||||
|
||||
private SimpleImmutableEntry<String, List<String>> createAttribute(String name) {
|
||||
return new SimpleImmutableEntry<String, List<String>>(name, null) {
|
||||
@Override
|
||||
public List<String> getValue() {
|
||||
List<String> values = get(name);
|
||||
|
||||
if (values == null) {
|
||||
return EMPTY_VALUE;
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the given {@code attributes} (as they were provided when creating a profile) accordingly to the
|
||||
* profile configuration and the current context.
|
||||
*
|
||||
* @param attributes the denormalized map of attributes
|
||||
*
|
||||
* @return a normalized map of attributes
|
||||
*/
|
||||
private Map<String, List<String>> normalizeAttributes(Map<String, ?> attributes) {
|
||||
Map<String, List<String>> newAttributes = new HashMap<>();
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
if (attributes != null && !attributes.isEmpty()) {
|
||||
for (Map.Entry<String, ?> entry : attributes.entrySet()) {
|
||||
Object value = entry.getValue();
|
||||
String key = entry.getKey();
|
||||
|
||||
if (!isSupportedAttribute(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key.startsWith(Constants.USER_ATTRIBUTES_PREFIX)) {
|
||||
key = key.substring(Constants.USER_ATTRIBUTES_PREFIX.length());
|
||||
}
|
||||
|
||||
List<String> values;
|
||||
|
||||
if (value instanceof String) {
|
||||
values = Collections.singletonList((String) value);
|
||||
} else {
|
||||
values = (List<String>) value;
|
||||
}
|
||||
|
||||
if (key.equals(UserModel.USERNAME)) {
|
||||
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 (user != null) {
|
||||
List<String> username = newAttributes.get(UserModel.USERNAME);
|
||||
|
||||
if (username == null || username.isEmpty() || (!realm.isEditUsernameAllowed() && UserProfileContext.USER_API.equals(context))) {
|
||||
newAttributes.put(UserModel.USERNAME, Collections.singletonList(user.getUsername()));
|
||||
}
|
||||
}
|
||||
|
||||
List<String> email = newAttributes.get(UserModel.EMAIL);
|
||||
|
||||
if (email != null && realm.isRegistrationEmailAsUsername()) {
|
||||
newAttributes.put(UserModel.USERNAME, email);
|
||||
}
|
||||
|
||||
return newAttributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks whether an attribute is support by the profile configuration and the current context.
|
||||
*
|
||||
* <p>This method can be used to avoid unexpected attributes from being added as an attribute because
|
||||
* the attribute source is a regular {@link Map} and not normalized.
|
||||
*
|
||||
* @param name the name of the attribute
|
||||
* @return
|
||||
*/
|
||||
private boolean isSupportedAttribute(String name) {
|
||||
if (READ_ONLY_ATTRIBUTE_KEY.equals(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (metadataByAttribute.containsKey(name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// expect any attribute if managing the user profile using REST
|
||||
if (UserProfileContext.USER_API.equals(context) || UserProfileContext.ACCOUNT.equals(context)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// attributes managed using forms with a pre-defined prefix are supported
|
||||
if (name.startsWith(Constants.USER_ATTRIBUTES_PREFIX)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isReadOnly(name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
private boolean isReadOnlyInternalAttribute(String attributeName) {
|
||||
// read-only can be configured through the provider so we try to validate global validations
|
||||
AttributeMetadata readonlyMetadata = metadataByAttribute.get(READ_ONLY_ATTRIBUTE_KEY);
|
||||
|
||||
if (readonlyMetadata == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SimpleImmutableEntry<String, List<String>> attribute = createAttribute(attributeName);
|
||||
|
||||
for (AttributeValidatorMetadata validator : readonlyMetadata.getValidators()) {
|
||||
if (!validator.validate(createAttributeContext(attribute, readonlyMetadata))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
*
|
||||
* * Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* * and other contributors as indicated by the @author tags.
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
/**
|
||||
* <p>The default implementation for {@link UserProfile}. Should be reused as much as possible by the different implementations
|
||||
* of {@link UserProfileProvider}.
|
||||
*
|
||||
* <p>This implementation is not specific to any user profile implementation.
|
||||
*
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public final class DefaultUserProfile implements UserProfile {
|
||||
|
||||
private final Function<Attributes, UserModel> userSupplier;
|
||||
private final Attributes attributes;
|
||||
private boolean validated;
|
||||
private UserModel user;
|
||||
|
||||
public DefaultUserProfile(Attributes attributes, Function<Attributes, UserModel> userCreator, UserModel user) {
|
||||
this.userSupplier = userCreator;
|
||||
this.attributes = attributes;
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() {
|
||||
ValidationException validationException = new ValidationException();
|
||||
|
||||
for (String attributeName : attributes.nameSet()) {
|
||||
this.attributes.validate(attributeName,
|
||||
(attribute, message) -> validationException.addError(new ValidationException.Error(attribute, message)));
|
||||
}
|
||||
|
||||
if (validationException.hasError()) {
|
||||
throw validationException;
|
||||
}
|
||||
|
||||
validated = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserModel create() throws ValidationException {
|
||||
if (user != null) {
|
||||
throw new RuntimeException("User already created");
|
||||
}
|
||||
|
||||
if (!validated) {
|
||||
validate();
|
||||
}
|
||||
|
||||
user = userSupplier.apply(this.attributes);
|
||||
|
||||
return updateInternal(user, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(boolean removeAttributes, BiConsumer<String, UserModel>... changeListener) {
|
||||
if (!validated) {
|
||||
validate();
|
||||
}
|
||||
|
||||
updateInternal(user, removeAttributes, changeListener);
|
||||
}
|
||||
|
||||
private UserModel updateInternal(UserModel user, boolean removeAttributes, BiConsumer<String, UserModel>... changeListener) {
|
||||
if (user == null) {
|
||||
throw new RuntimeException("No user model provided for persisting changes");
|
||||
}
|
||||
|
||||
try {
|
||||
for (Map.Entry<String, List<String>> attribute : attributes.attributeSet()) {
|
||||
String name = attribute.getKey();
|
||||
|
||||
if (attributes.isReadOnly(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<String> currentValue = user.getAttributeStream(name).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
List<String> updatedValue = attribute.getValue().stream().filter(Objects::nonNull).collect(Collectors.toList());
|
||||
|
||||
if (currentValue.size() != updatedValue.size() || !currentValue.containsAll(updatedValue)) {
|
||||
user.setAttribute(name, updatedValue);
|
||||
for (BiConsumer<String, UserModel> listener : changeListener) {
|
||||
listener.accept(name, user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this is a workaround for supporting contexts where the decision to whether attributes should be removed depends on
|
||||
// specific aspect. For instance, old account should never remove attributes, the admin rest api should only remove if
|
||||
// the attribute map was sent.
|
||||
if (removeAttributes) {
|
||||
Set<String> attrsToRemove = new HashSet<>(user.getAttributes().keySet());
|
||||
attrsToRemove.removeAll(attributes.nameSet());
|
||||
|
||||
for (String attr : attrsToRemove) {
|
||||
if (this.attributes.isReadOnly(attr)) {
|
||||
continue;
|
||||
}
|
||||
user.removeAttribute(attr);
|
||||
}
|
||||
}
|
||||
} catch (ModelException me) {
|
||||
// some client code relies on this exception to react to exceptions from the storage
|
||||
throw me;
|
||||
} catch (Exception cause) {
|
||||
throw new RuntimeException("Unexpected error when persisting user profile", cause);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Attributes getAttributes() {
|
||||
return attributes;
|
||||
}
|
||||
}
|
|
@ -17,16 +17,72 @@
|
|||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
/**
|
||||
* Abstraction, which allows to update the user in various contexts (Required action of already existing user, or first identity provider
|
||||
* login when user doesn't yet exists in Keycloak DB)
|
||||
* <p>An interface providing as an entry point for managing users.
|
||||
*
|
||||
* <p>A {@code UserProfile} provides a manageable view for user information that also takes into account the context where it is being used.
|
||||
* The context represents the different places in Keycloak where users are created, updated, or validated.
|
||||
* Examples of contexts are: managing users through the Admin API, or through the Account API.
|
||||
*
|
||||
* <p>By taking the context into account, the state and behavior of {@link UserProfile} instances depend on the context they
|
||||
* are associated with, where validating, updating, creating, or obtaining representations of users is based on the configuration
|
||||
* and constraints associated with a context.
|
||||
*
|
||||
* <p>A {@code UserProfile} instance can be obtained through the {@link UserProfileProvider}.
|
||||
*
|
||||
* @see UserProfileContext
|
||||
* @see UserProfileProvider
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public interface UserProfile {
|
||||
|
||||
String getId();
|
||||
/**
|
||||
* Validates the attributes associated with this instance.
|
||||
*
|
||||
* @throws ValidationException in case
|
||||
*/
|
||||
void validate() throws ValidationException;
|
||||
|
||||
UserProfileAttributes getAttributes();
|
||||
/**
|
||||
* Creates a new {@link UserModel} based on the attributes associated with this instance.
|
||||
*
|
||||
* @throws ValidationException in case validation fails
|
||||
*
|
||||
* @return the {@link UserModel} instance created from this profile
|
||||
*/
|
||||
UserModel create() throws ValidationException;
|
||||
|
||||
/**
|
||||
* <p>Updates the {@link UserModel} associated with this instance. If no {@link UserModel} is associated with this instance, this operation has no effect.
|
||||
*
|
||||
* <p>Before updating the {@link UserModel}, this method first checks whether the {@link #validate()} method was previously
|
||||
* invoked. If not, the validation step is performed prior to updating the model.
|
||||
*
|
||||
* @param removeAttributes if attributes should be removed from the {@link UserModel} if they are not among the attributes associated with this instance.
|
||||
* @param changeListener a set of one or more listeners to listen for attribute changes
|
||||
* @throws ValidationException in case of any validation error
|
||||
*/
|
||||
void update(boolean removeAttributes, BiConsumer<String, UserModel>... changeListener) throws ValidationException;
|
||||
|
||||
/**
|
||||
* <p>The same as {@link #update(boolean, BiConsumer[])} but forcing the removal of attributes.
|
||||
*
|
||||
* @param changeListener a set of one or more listeners to listen for attribute changes
|
||||
* @throws ValidationException in case of any validation error
|
||||
*/
|
||||
default void update(BiConsumer<String, UserModel>... changeListener) throws ValidationException, RuntimeException {
|
||||
update(true, changeListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the attributes associated with this instance. Note that the attributes returned by this method are not necessarily
|
||||
* the same from the {@link UserModel}, but those that should be validated and possibly updated to the {@link UserModel}.
|
||||
*
|
||||
* @return the attributes associated with this instance.
|
||||
*/
|
||||
Attributes getAttributes();
|
||||
}
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
public class UserProfileAttributes extends HashMap<String, List<String>> {
|
||||
|
||||
private final UserProfileProvider profileProvider;
|
||||
|
||||
public UserProfileAttributes(Map<String, List<String>> attribtues,
|
||||
UserProfileProvider profileProvider){
|
||||
this.profileProvider = profileProvider;
|
||||
this.putAll(attribtues);
|
||||
}
|
||||
|
||||
public UserProfileAttributes(Map<String, List<String>> attribtues){
|
||||
this(attribtues, null);
|
||||
}
|
||||
|
||||
public void setAttribute(String key, List<String> value){
|
||||
this.put(key,value);
|
||||
}
|
||||
|
||||
public void setSingleAttribute(String key, String value) {
|
||||
this.setAttribute(key, Collections.singletonList(value));
|
||||
}
|
||||
|
||||
public String getFirstAttribute(String key) {
|
||||
return this.get(key) == null ? null : this.get(key).isEmpty()? null : this.get(key).get(0);
|
||||
}
|
||||
|
||||
public List<String> getAttribute(String key) {
|
||||
return this.get(key);
|
||||
}
|
||||
|
||||
public void removeAttribute(String attr) {
|
||||
this.remove(attr);
|
||||
}
|
||||
|
||||
public boolean isReadOnlyAttribute(String key) {
|
||||
return profileProvider != null && profileProvider.isReadOnlyAttribute(key);
|
||||
}
|
||||
}
|
|
@ -1,31 +1,40 @@
|
|||
/*
|
||||
* Copyright 2020 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
|
||||
* * 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.
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
||||
import org.keycloak.userprofile.validation.UserUpdateEvent;
|
||||
|
||||
/**
|
||||
* <p>This interface represents the different contexts from where user profiles are managed. The core contexts are already
|
||||
* available here representing the different parts in Keycloak where user profiles are managed.
|
||||
*
|
||||
* <p>The context is crucial to drive the conditions that should be respected when managing user profiles. It might be possible
|
||||
* to include in the future metadata about contexts. As well as support custom contexts.
|
||||
*
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public interface UserProfileContext {
|
||||
public enum UserProfileContext {
|
||||
|
||||
UserUpdateEvent getUpdateEvent();
|
||||
UserProfile getCurrentProfile();
|
||||
UserProfileValidationResult validate();
|
||||
UPDATE_PROFILE,
|
||||
USER_API,
|
||||
ACCOUNT,
|
||||
ACCOUNT_OLD,
|
||||
IDP_REVIEW,
|
||||
REGISTRATION_PROFILE,
|
||||
REGISTRATION_USER_CREATION;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
*
|
||||
* * Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* * and other contributors as indicated by the @author tags.
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public final class UserProfileMetadata implements Cloneable {
|
||||
|
||||
private final UserProfileContext context;
|
||||
private List<AttributeMetadata> attributes;
|
||||
|
||||
public UserProfileMetadata(UserProfileContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public List<AttributeMetadata> getAttributes() {
|
||||
return attributes;
|
||||
}
|
||||
|
||||
public void addAttributes(AttributeMetadata... metadata) {
|
||||
addAttributes(Arrays.asList(metadata));
|
||||
}
|
||||
|
||||
public void addAttributes(List<AttributeMetadata> metadata) {
|
||||
if (attributes == null) {
|
||||
attributes = new ArrayList<>();
|
||||
}
|
||||
attributes.addAll(metadata);
|
||||
}
|
||||
|
||||
public AttributeMetadata addAttribute(AttributeMetadata metadata) {
|
||||
addAttributes(Arrays.asList(metadata));
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public AttributeMetadata addAttribute(String name, AttributeValidatorMetadata... validator) {
|
||||
return addAttribute(name, Arrays.asList(validator));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get existing AttributeMetadata for attribute of given name.
|
||||
*
|
||||
* @param name of the attribute
|
||||
* @return list of existing metadata for given attribute, never null
|
||||
*/
|
||||
public List<AttributeMetadata> getAttribute(String name) {
|
||||
if (attributes == null)
|
||||
return Collections.emptyList();
|
||||
return attributes.stream().filter((c) -> name.equals(c.getName())).collect(Collectors.toList());
|
||||
|
||||
}
|
||||
|
||||
public UserProfileContext getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserProfileMetadata clone() {
|
||||
UserProfileMetadata metadata = new UserProfileMetadata(this.context);
|
||||
|
||||
//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()));
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof UserProfileMetadata)) return false;
|
||||
|
||||
UserProfileMetadata that = (UserProfileMetadata) o;
|
||||
return that.getContext().equals(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getContext().hashCode();
|
||||
}
|
||||
}
|
|
@ -17,15 +17,74 @@
|
|||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
||||
|
||||
/**
|
||||
* <p>The provider responsible for creating {@link UserProfile} instances.
|
||||
*
|
||||
* @see UserProfile
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public interface UserProfileProvider extends Provider {
|
||||
|
||||
UserProfileValidationResult validate(UserProfileContext updateContext, UserProfile updatedProfile);
|
||||
/**
|
||||
* <p>Creates a new {@link UserProfile} instance only for validation purposes to check whether its attributes are in conformance
|
||||
* with the given {@code context} and profile configuration.
|
||||
*
|
||||
* @param context the context
|
||||
* @param user an existing user
|
||||
*
|
||||
* @return the user profile instance
|
||||
*/
|
||||
UserProfile create(UserProfileContext context, UserModel user);
|
||||
|
||||
boolean isReadOnlyAttribute(String key);
|
||||
/**
|
||||
* <p>Creates a new {@link UserProfile} instance for a given {@code context} and {@code attributes} for validation purposes.
|
||||
*
|
||||
* <p>Instances created from this method are usually related to contexts where validation and updates are performed in different
|
||||
* steps, or when creating new users based on the given {@code attributes}.
|
||||
*
|
||||
* @param context the context
|
||||
* @param attributes the attributes to associate with the instance returned from this method
|
||||
*
|
||||
* @return the user profile instance
|
||||
*/
|
||||
UserProfile create(UserProfileContext context, Map<String, ?> attributes);
|
||||
|
||||
/**
|
||||
* <p>Creates a new {@link UserProfile} instance for a given {@code context} and {@code attributes} for update purposes.
|
||||
*
|
||||
* <p>Instances created from this method are going to run validations and updates based on the given {@code user}. This
|
||||
* might be useful when updating an existing user.
|
||||
*
|
||||
* @param context the context
|
||||
* @param attributes the attributes to associate with the instance returned from this method
|
||||
* @param user the user to eventually update with the given {@code attributes}
|
||||
*
|
||||
* @return the user profile instance
|
||||
*/
|
||||
UserProfile create(UserProfileContext context, Map<String, ?> attributes, UserModel user);
|
||||
|
||||
/**
|
||||
* Get current UserProfile configuration. JSON formatted file is expected, but
|
||||
* depends on the implementation.
|
||||
*
|
||||
* @return current UserProfile configuration
|
||||
* @see #setConfiguration(String)
|
||||
*/
|
||||
String getConfiguration();
|
||||
|
||||
/**
|
||||
* Set new UserProfile configuration. It is persisted inside of the provider.
|
||||
*
|
||||
* @param configuration to be set
|
||||
* @throws RuntimeException if configuration is invalid (exact exception class
|
||||
* depends on the implementation) or configuration
|
||||
* can't be persisted.
|
||||
* @see #getConfiguration()
|
||||
*/
|
||||
void setConfiguration(String configuration);
|
||||
}
|
||||
|
|
|
@ -22,6 +22,6 @@ import org.keycloak.provider.ProviderFactory;
|
|||
/**
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public interface UserProfileProviderFactory extends ProviderFactory<UserProfileProvider> {
|
||||
public interface UserProfileProviderFactory<U extends UserProfileProvider> extends ProviderFactory<U> {
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
*
|
||||
* * Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* * and other contributors as indicated by the @author tags.
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public final class ValidationException extends RuntimeException {
|
||||
|
||||
private final Map<String, List<Error>> errors = new HashMap<>();
|
||||
|
||||
public List<Error> getErrors() {
|
||||
return errors.values().stream().reduce(new ArrayList<>(),
|
||||
(l, r) -> {
|
||||
l.addAll(r);
|
||||
return l;
|
||||
}, (l, r) -> l);
|
||||
}
|
||||
|
||||
public boolean hasError(String... types) {
|
||||
if (types.length == 0) {
|
||||
return !errors.isEmpty();
|
||||
}
|
||||
|
||||
for (String type : types) {
|
||||
if (errors.containsKey(type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there are validation errors related to the attribute with the given {@code name}.
|
||||
*
|
||||
* @param name
|
||||
* @return
|
||||
*/
|
||||
public boolean isAttributeOnError(String... name) {
|
||||
if (name.length == 0) {
|
||||
return !errors.isEmpty();
|
||||
}
|
||||
|
||||
List<String> names = Arrays.asList(name);
|
||||
|
||||
return errors.values().stream().flatMap(Collection::stream)
|
||||
.anyMatch(error -> names.contains(error.attribute.getKey()));
|
||||
}
|
||||
|
||||
void addError(Error error) {
|
||||
List<Error> errors = this.errors.computeIfAbsent(error.getMessage(), (k) -> new ArrayList<>());
|
||||
errors.add(error);
|
||||
}
|
||||
|
||||
public static class Error {
|
||||
|
||||
private final Map.Entry<String, List<String>> attribute;
|
||||
private final String message;
|
||||
|
||||
public Error(Map.Entry<String, List<String>> attribute, String message) {
|
||||
this.attribute = attribute;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getAttribute() {
|
||||
return attribute.getKey();
|
||||
}
|
||||
|
||||
//TODO: support parameters to messsages for formatting purposes. Message key and parameters.
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile.validation;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public class AttributeValidationResult {
|
||||
|
||||
private final String attributeKey;
|
||||
private final boolean changed;
|
||||
List<ValidationResult> validationResults;
|
||||
|
||||
public List<ValidationResult> getValidationResults() {
|
||||
return validationResults;
|
||||
}
|
||||
|
||||
public List<ValidationResult> getFailedValidations() {
|
||||
return validationResults == null ? null : validationResults.stream().filter(ValidationResult::isInvalid).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
public AttributeValidationResult(String attributeKey, boolean changed, List<ValidationResult> validationResults) {
|
||||
this.attributeKey = attributeKey;
|
||||
this.validationResults = validationResults;
|
||||
this.changed = changed;
|
||||
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return validationResults.stream().allMatch(ValidationResult::isValid);
|
||||
}
|
||||
|
||||
protected boolean isInvalid() {
|
||||
return !isValid();
|
||||
}
|
||||
|
||||
public boolean hasChanged() {
|
||||
return changed;
|
||||
}
|
||||
|
||||
public String getField() {
|
||||
return attributeKey;
|
||||
}
|
||||
|
||||
public boolean hasFailureOfErrorType(String... errorKeys) {
|
||||
return this.validationResults != null
|
||||
&& this.getFailedValidations().stream().anyMatch(o -> o.getErrorType() != null
|
||||
&& Arrays.stream(errorKeys).anyMatch(a -> a.equals(o.getErrorType())));
|
||||
}
|
||||
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile.validation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public class UserProfileValidationResult {
|
||||
|
||||
|
||||
List<AttributeValidationResult> attributeValidationResults;
|
||||
private final UserProfile updatedProfile;
|
||||
|
||||
public UserProfileValidationResult(List<AttributeValidationResult> attributeValidationResults,
|
||||
UserProfile updatedProfile) {
|
||||
this.attributeValidationResults = attributeValidationResults;
|
||||
this.updatedProfile = updatedProfile;
|
||||
}
|
||||
|
||||
public List<AttributeValidationResult> getValidationResults() {
|
||||
return attributeValidationResults;
|
||||
}
|
||||
|
||||
public List<AttributeValidationResult> getErrors() {
|
||||
return attributeValidationResults.stream().filter(AttributeValidationResult::isInvalid).collect(Collectors.toCollection(ArrayList::new));
|
||||
}
|
||||
|
||||
|
||||
public boolean hasFailureOfErrorType(String... errorKeys) {
|
||||
return this.attributeValidationResults != null
|
||||
&& this.attributeValidationResults.stream().anyMatch(attributeValidationResult -> attributeValidationResult.hasFailureOfErrorType(errorKeys));
|
||||
}
|
||||
|
||||
public boolean hasAttributeChanged(String attribute) {
|
||||
return this.attributeValidationResults.stream().filter(o -> o.getField().equals(attribute)).collect(Collectors.toList()).get(0).hasChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link UserProfile} used during validations.
|
||||
*
|
||||
* @return the profile user during validations
|
||||
*/
|
||||
public UserProfile getProfile() {
|
||||
return updatedProfile;
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile.validation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public enum UserUpdateEvent {
|
||||
UpdateProfile,
|
||||
UserResource,
|
||||
Account,
|
||||
IdpReview,
|
||||
RegistrationProfile,
|
||||
RegistrationUserCreation
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile.validation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public class ValidationResult {
|
||||
boolean valid;
|
||||
|
||||
String errorType;
|
||||
|
||||
public ValidationResult( boolean valid, String errorType) {
|
||||
this.errorType = errorType;
|
||||
this.valid = valid;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return valid;
|
||||
}
|
||||
|
||||
protected boolean isInvalid() {
|
||||
return !isValid();
|
||||
}
|
||||
|
||||
public String getErrorType() {
|
||||
return errorType;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
*
|
||||
* * Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* * and other contributors as indicated by the @author tags.
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile.validation;
|
||||
|
||||
import org.keycloak.userprofile.AttributeContext;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public interface Validator {
|
||||
|
||||
/**
|
||||
* @returns true if validation success, false if validation fails
|
||||
*/
|
||||
boolean validate(AttributeContext context);
|
||||
|
||||
}
|
|
@ -132,6 +132,10 @@ public class ComponentModel implements Serializable {
|
|||
notes.put(key, object);
|
||||
}
|
||||
|
||||
public void removeNote(String key) {
|
||||
notes.remove(key);
|
||||
}
|
||||
|
||||
public String getProviderId() {
|
||||
return providerId;
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ public abstract class UserModelDefaultMethods implements UserModel {
|
|||
|
||||
@Override
|
||||
public void setEmail(String email) {
|
||||
email = email == null ? null : email.toLowerCase();
|
||||
email = email == null || email.trim().isEmpty() ? null : email.toLowerCase();
|
||||
setSingleAttribute(EMAIL, email);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
|
||||
package org.keycloak.authentication.authenticators.broker;
|
||||
|
||||
import static org.keycloak.userprofile.profile.UserProfileContextFactory.forIdpReview;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
||||
|
@ -36,9 +34,10 @@ import org.keycloak.models.utils.FormMessage;
|
|||
import org.keycloak.models.utils.UserModelDelegate;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.services.validation.Validation;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.userprofile.ValidationException;
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.utils.UserUpdateHelper;
|
||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
@ -102,23 +101,13 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
|
|||
EventBuilder event = context.getEvent();
|
||||
event.event(EventType.UPDATE_PROFILE);
|
||||
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||
UserProfileValidationResult result = forIdpReview(userCtx, formData, context.getSession()).validate();
|
||||
UserModelDelegate updatedProfile = new UserModelDelegate(null) {
|
||||
|
||||
List<FormMessage> errors = Validation.getFormErrorsFromValidation(result);
|
||||
|
||||
if (errors != null && !errors.isEmpty()) {
|
||||
Response challenge = context.form()
|
||||
.setErrors(errors)
|
||||
.setAttribute(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR, userCtx)
|
||||
.setFormData(formData)
|
||||
.createUpdateProfilePage();
|
||||
context.challenge(challenge);
|
||||
return;
|
||||
@Override
|
||||
public String getId() {
|
||||
return userCtx.getId();
|
||||
}
|
||||
|
||||
UserProfile updatedProfile = result.getProfile();
|
||||
|
||||
UserUpdateHelper.updateIdpReview(context.getRealm(), new UserModelDelegate(null) {
|
||||
@Override
|
||||
public Map<String, List<String>> getAttributes() {
|
||||
return userCtx.getAttributes();
|
||||
|
@ -138,19 +127,50 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
|
|||
public void removeAttribute(String name) {
|
||||
userCtx.getAttributes().remove(name);
|
||||
}
|
||||
}, updatedProfile);
|
||||
|
||||
@Override
|
||||
public String getFirstAttribute(String name) {
|
||||
return userCtx.getFirstAttribute(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return userCtx.getUsername();
|
||||
}
|
||||
};
|
||||
|
||||
UserProfileProvider profileProvider = context.getSession().getProvider(UserProfileProvider.class);
|
||||
UserProfile profile = profileProvider.create(UserProfileContext.IDP_REVIEW, formData, updatedProfile);
|
||||
|
||||
try {
|
||||
String oldEmail = userCtx.getEmail();
|
||||
|
||||
profile.update((attributeName, userModel) -> {
|
||||
if (attributeName.equals(UserModel.EMAIL)) {
|
||||
context.getAuthenticationSession().setAuthNote(UPDATE_PROFILE_EMAIL_CHANGED, "true");
|
||||
event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, profile.getAttributes().getFirstValue(UserModel.EMAIL)).success();
|
||||
}
|
||||
});
|
||||
} catch (ValidationException pve) {
|
||||
List<FormMessage> errors = Validation.getFormErrorsFromValidation(pve.getErrors());
|
||||
|
||||
Response challenge = context.form()
|
||||
.setErrors(errors)
|
||||
.setAttribute(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR, userCtx)
|
||||
.setFormData(formData)
|
||||
.createUpdateProfilePage();
|
||||
|
||||
context.challenge(challenge);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
userCtx.saveToAuthenticationSession(context.getAuthenticationSession(), BROKERED_CONTEXT_NOTE);
|
||||
|
||||
logger.debugf("Profile updated successfully after first authentication with identity provider '%s' for broker user '%s'.", brokerContext.getIdpConfig().getAlias(), userCtx.getUsername());
|
||||
|
||||
String oldEmail = userCtx.getEmail();
|
||||
String newEmail = updatedProfile.getAttributes().getFirstAttribute(UserModel.EMAIL);
|
||||
String newEmail = profile.getAttributes().getFirstValue(UserModel.EMAIL);
|
||||
|
||||
if (result.hasAttributeChanged(UserModel.EMAIL)) {
|
||||
context.getAuthenticationSession().setAuthNote(UPDATE_PROFILE_EMAIL_CHANGED, "true");
|
||||
event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, newEmail).success();
|
||||
}
|
||||
event.detail(Details.UPDATED_EMAIL, newEmail);
|
||||
|
||||
// Ensure page is always shown when user later returns to it - for example with form "back" button
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
|
||||
package org.keycloak.authentication.forms;
|
||||
|
||||
import static org.keycloak.userprofile.profile.UserProfileContextFactory.forRegistrationProfile;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.FormAction;
|
||||
import org.keycloak.authentication.FormActionFactory;
|
||||
|
@ -34,12 +32,11 @@ import org.keycloak.models.UserModel;
|
|||
import org.keycloak.models.utils.FormMessage;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.AttributeFormDataProcessor;
|
||||
import org.keycloak.services.validation.Validation;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.userprofile.ValidationException;
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.profile.representations.AttributeUserProfile;
|
||||
import org.keycloak.userprofile.utils.UserUpdateHelper;
|
||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import java.util.List;
|
||||
|
@ -67,33 +64,37 @@ public class RegistrationProfile implements FormAction, FormActionFactory {
|
|||
|
||||
context.getEvent().detail(Details.REGISTER_METHOD, "form");
|
||||
|
||||
UserProfileValidationResult result = forRegistrationProfile(context.getSession(), formData).validate();
|
||||
List<FormMessage> errors = Validation.getFormErrorsFromValidation(result);
|
||||
UserProfileProvider profileProvider = context.getSession().getProvider(UserProfileProvider.class);
|
||||
UserProfile profile = profileProvider.create(UserProfileContext.REGISTRATION_PROFILE, formData);
|
||||
|
||||
if (errors.size() > 0) {
|
||||
if (result.hasFailureOfErrorType(Messages.EMAIL_EXISTS, Messages.INVALID_EMAIL)) {
|
||||
UserProfile updatedProfile = result.getProfile();
|
||||
context.getEvent().detail(Details.EMAIL, updatedProfile.getAttributes().getFirstAttribute(UserModel.EMAIL));
|
||||
try {
|
||||
profile.validate();
|
||||
} catch (ValidationException pve) {
|
||||
List<FormMessage> errors = Validation.getFormErrorsFromValidation(pve.getErrors());
|
||||
|
||||
if (pve.hasError(Messages.EMAIL_EXISTS, Messages.INVALID_EMAIL)) {
|
||||
context.getEvent().detail(Details.EMAIL, profile.getAttributes().getFirstValue(UserModel.EMAIL));
|
||||
}
|
||||
|
||||
if (result.hasFailureOfErrorType(Messages.EMAIL_EXISTS)) {
|
||||
if (pve.hasError(Messages.EMAIL_EXISTS)) {
|
||||
context.error(Errors.EMAIL_IN_USE);
|
||||
formData.remove("email");
|
||||
} else
|
||||
context.error(Errors.INVALID_REGISTRATION);
|
||||
context.validationError(formData, errors);
|
||||
return;
|
||||
|
||||
} else {
|
||||
context.success();
|
||||
context.validationError(formData, errors);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
context.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void success(FormContext context) {
|
||||
UserModel user = context.getUser();
|
||||
AttributeUserProfile updatedProfile = AttributeFormDataProcessor.toUserProfile(context.getHttpRequest().getDecodedFormParameters());
|
||||
UserUpdateHelper.updateRegistrationProfile(context.getRealm(), user, updatedProfile);
|
||||
UserProfileProvider provider = context.getSession().getProvider(UserProfileProvider.class);
|
||||
provider.create(UserProfileContext.REGISTRATION_PROFILE, context.getHttpRequest().getDecodedFormParameters(), user).update();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
|
||||
package org.keycloak.authentication.forms;
|
||||
|
||||
import static org.keycloak.userprofile.profile.UserProfileContextFactory.forRegistrationUserCreation;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.FormAction;
|
||||
import org.keycloak.authentication.FormActionFactory;
|
||||
|
@ -37,12 +35,11 @@ import org.keycloak.models.utils.FormMessage;
|
|||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.AttributeFormDataProcessor;
|
||||
import org.keycloak.services.validation.Validation;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.userprofile.ValidationException;
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.profile.representations.AttributeUserProfile;
|
||||
import org.keycloak.userprofile.utils.UserUpdateHelper;
|
||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import java.util.List;
|
||||
|
@ -70,33 +67,37 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
|
|||
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||
context.getEvent().detail(Details.REGISTER_METHOD, "form");
|
||||
|
||||
KeycloakSession session = context.getSession();
|
||||
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
|
||||
UserProfile profile = profileProvider.create(UserProfileContext.REGISTRATION_USER_CREATION, formData);
|
||||
String email = profile.getAttributes().getFirstValue(UserModel.EMAIL);
|
||||
|
||||
UserProfileValidationResult result = forRegistrationUserCreation(context.getSession(), formData).validate();
|
||||
UserProfile newProfile = result.getProfile();
|
||||
String email = newProfile.getAttributes().getFirstAttribute(UserModel.EMAIL);
|
||||
|
||||
String username = newProfile.getAttributes().getFirstAttribute(UserModel.USERNAME);
|
||||
String firstName = newProfile.getAttributes().getFirstAttribute(UserModel.FIRST_NAME);
|
||||
String lastName = newProfile.getAttributes().getFirstAttribute(UserModel.LAST_NAME);
|
||||
String username = profile.getAttributes().getFirstValue(UserModel.USERNAME);
|
||||
String firstName = profile.getAttributes().getFirstValue(UserModel.FIRST_NAME);
|
||||
String lastName = profile.getAttributes().getFirstValue(UserModel.LAST_NAME);
|
||||
context.getEvent().detail(Details.EMAIL, email);
|
||||
|
||||
context.getEvent().detail(Details.USERNAME, username);
|
||||
context.getEvent().detail(Details.FIRST_NAME, firstName);
|
||||
context.getEvent().detail(Details.LAST_NAME, lastName);
|
||||
|
||||
List<FormMessage> errors = Validation.getFormErrorsFromValidation(result);
|
||||
if (context.getRealm().isRegistrationEmailAsUsername()) {
|
||||
context.getEvent().detail(Details.USERNAME, email);
|
||||
}
|
||||
if (errors.size() > 0) {
|
||||
if (result.hasFailureOfErrorType(Messages.EMAIL_EXISTS)) {
|
||||
|
||||
try {
|
||||
profile.validate();
|
||||
} catch (ValidationException pve) {
|
||||
List<FormMessage> errors = Validation.getFormErrorsFromValidation(pve.getErrors());
|
||||
|
||||
if (pve.hasError(Messages.EMAIL_EXISTS)) {
|
||||
context.error(Errors.EMAIL_IN_USE);
|
||||
formData.remove(RegistrationPage.FIELD_EMAIL);
|
||||
} else if (result.hasFailureOfErrorType(Messages.MISSING_EMAIL, Messages.MISSING_USERNAME, Messages.INVALID_EMAIL)) {
|
||||
if (result.hasFailureOfErrorType(Messages.INVALID_EMAIL))
|
||||
} else if (pve.hasError(Messages.MISSING_EMAIL, Messages.MISSING_USERNAME, Messages.INVALID_EMAIL)) {
|
||||
if (pve.hasError(Messages.INVALID_EMAIL))
|
||||
formData.remove(Validation.FIELD_EMAIL);
|
||||
context.error(Errors.INVALID_REGISTRATION);
|
||||
} else if (result.hasFailureOfErrorType(Messages.USERNAME_EXISTS)) {
|
||||
} else if (pve.hasError(Messages.USERNAME_EXISTS)) {
|
||||
context.error(Errors.USERNAME_IN_USE);
|
||||
formData.remove(Validation.FIELD_USERNAME);
|
||||
}
|
||||
|
@ -114,24 +115,31 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
|
|||
|
||||
@Override
|
||||
public void success(FormContext context) {
|
||||
AttributeUserProfile updatedProfile = AttributeFormDataProcessor.toUserProfile(context.getHttpRequest().getDecodedFormParameters());
|
||||
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||
|
||||
String email = formData.getFirst(UserModel.EMAIL);
|
||||
String username = formData.getFirst(UserModel.USERNAME);
|
||||
|
||||
String email = updatedProfile.getAttributes().getFirstAttribute(UserModel.EMAIL);
|
||||
String username = updatedProfile.getAttributes().getFirstAttribute(UserModel.USERNAME);
|
||||
if (context.getRealm().isRegistrationEmailAsUsername()) {
|
||||
username = email;
|
||||
}
|
||||
|
||||
context.getEvent().detail(Details.USERNAME, username)
|
||||
.detail(Details.REGISTER_METHOD, "form")
|
||||
.detail(Details.EMAIL, email);
|
||||
|
||||
UserModel user = context.getSession().users().addUser(context.getRealm(), username);
|
||||
KeycloakSession session = context.getSession();
|
||||
|
||||
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
|
||||
UserProfile profile = profileProvider.create(UserProfileContext.REGISTRATION_USER_CREATION, formData);
|
||||
UserModel user = profile.create();
|
||||
|
||||
user.setEnabled(true);
|
||||
UserUpdateHelper.updateRegistrationUserCreation(context.getRealm(), user, updatedProfile);
|
||||
|
||||
context.setUser(user);
|
||||
|
||||
context.getAuthenticationSession().setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username);
|
||||
|
||||
context.setUser(user);
|
||||
context.getEvent().user(user);
|
||||
context.getEvent().success();
|
||||
context.newEvent().event(EventType.LOGIN);
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
|
||||
package org.keycloak.authentication.requiredactions;
|
||||
|
||||
import static org.keycloak.userprofile.profile.UserProfileContextFactory.forUpdateProfile;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.authentication.DisplayTypeRequiredActionFactory;
|
||||
|
@ -34,9 +32,10 @@ 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.UserProfileContext;
|
||||
import org.keycloak.userprofile.ValidationException;
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.utils.UserUpdateHelper;
|
||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
@ -73,36 +72,34 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
|
|||
String oldFirstName = user.getFirstName();
|
||||
String oldLastName = user.getLastName();
|
||||
String oldEmail = user.getEmail();
|
||||
UserProfileValidationResult result = forUpdateProfile(user, formData, context.getSession()).validate();
|
||||
final UserProfile updatedProfile = result.getProfile();
|
||||
List<FormMessage> errors = Validation.getFormErrorsFromValidation(result);
|
||||
UserProfileProvider provider = context.getSession().getProvider(UserProfileProvider.class);
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, formData, user);
|
||||
|
||||
try {
|
||||
// backward compatibility with old account console where attributes are not removed if missing
|
||||
profile.update(false, (attributeName, userModel) -> {
|
||||
if (attributeName.equals(UserModel.FIRST_NAME)) {
|
||||
event.detail(Details.PREVIOUS_FIRST_NAME, oldFirstName).detail(Details.UPDATED_FIRST_NAME, user.getFirstName());
|
||||
}
|
||||
if (attributeName.equals(UserModel.LAST_NAME)) {
|
||||
event.detail(Details.PREVIOUS_LAST_NAME, oldLastName).detail(Details.UPDATED_LAST_NAME, user.getLastName());
|
||||
}
|
||||
if (attributeName.equals(UserModel.EMAIL)) {
|
||||
user.setEmailVerified(false);
|
||||
event.detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, user.getEmail());
|
||||
}
|
||||
});
|
||||
|
||||
context.success();
|
||||
} catch (ValidationException pve) {
|
||||
List<FormMessage> errors = Validation.getFormErrorsFromValidation(pve.getErrors());
|
||||
|
||||
if (errors != null && !errors.isEmpty()) {
|
||||
Response challenge = context.form()
|
||||
.setErrors(errors)
|
||||
.setFormData(formData)
|
||||
.createResponse(UserModel.RequiredAction.UPDATE_PROFILE);
|
||||
context.challenge(challenge);
|
||||
return;
|
||||
}
|
||||
|
||||
String newEmail = updatedProfile.getAttributes().getFirstAttribute(UserModel.EMAIL);
|
||||
String newFirstName = updatedProfile.getAttributes().getFirstAttribute(UserModel.FIRST_NAME);
|
||||
String newLastName = updatedProfile.getAttributes().getFirstAttribute(UserModel.LAST_NAME);
|
||||
|
||||
UserUpdateHelper.updateUserProfile(context.getRealm(), user, updatedProfile);
|
||||
if (result.hasAttributeChanged(UserModel.FIRST_NAME)) {
|
||||
event.detail(Details.PREVIOUS_FIRST_NAME, oldFirstName).detail(Details.UPDATED_FIRST_NAME, newFirstName);
|
||||
}
|
||||
if (result.hasAttributeChanged(UserModel.LAST_NAME)) {
|
||||
event.detail(Details.PREVIOUS_LAST_NAME, oldLastName).detail(Details.UPDATED_LAST_NAME, newLastName);
|
||||
}
|
||||
if (result.hasAttributeChanged(UserModel.EMAIL)) {
|
||||
user.setEmailVerified(false);
|
||||
event.detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, newEmail);
|
||||
}
|
||||
context.success();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
/*
|
||||
* 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.services.resources;
|
||||
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.userprofile.profile.representations.AttributeUserProfile;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class AttributeFormDataProcessor {
|
||||
|
||||
|
||||
public static AttributeUserProfile process(MultivaluedMap<String, String> formData) {
|
||||
Map<String, List<String>> attributes= new HashMap<>();
|
||||
for (String key : formData.keySet()) {
|
||||
if (!key.startsWith(Constants.USER_ATTRIBUTES_PREFIX)) continue;
|
||||
String attribute = key.substring(Constants.USER_ATTRIBUTES_PREFIX.length());
|
||||
|
||||
// Need to handle case when attribute has multiple values, but in UI was displayed just first value
|
||||
List<String> modelValue = new ArrayList<String>();
|
||||
|
||||
int index = 0;
|
||||
for (String value : formData.get(key)) {
|
||||
addOrSetValue(modelValue, index, value);
|
||||
index++;
|
||||
}
|
||||
|
||||
attributes.put(attribute, modelValue);
|
||||
}
|
||||
return new AttributeUserProfile(attributes);
|
||||
}
|
||||
|
||||
public static AttributeUserProfile toUserProfile(MultivaluedMap<String, String> formData) {
|
||||
AttributeUserProfile profile = process(formData);
|
||||
|
||||
copyAttribute(UserModel.USERNAME, formData, profile);
|
||||
copyAttribute(UserModel.FIRST_NAME, formData, profile);
|
||||
copyAttribute(UserModel.LAST_NAME, formData, profile);
|
||||
copyAttribute(UserModel.EMAIL, formData, profile);
|
||||
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
private static void copyAttribute(String key, MultivaluedMap<String, String> formData, AttributeUserProfile rep) {
|
||||
if (formData.getFirst(key) != null)
|
||||
rep.getAttributes().setSingleAttribute(key, formData.getFirst(key));
|
||||
}
|
||||
|
||||
|
||||
private static void addOrSetValue(List<String> list, int index, String value) {
|
||||
if (list.size() > index) {
|
||||
list.set(index, value);
|
||||
} else {
|
||||
list.add(value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,8 +16,6 @@
|
|||
*/
|
||||
package org.keycloak.services.resources.account;
|
||||
|
||||
import static org.keycloak.userprofile.profile.UserProfileContextFactory.forOldAccount;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.authorization.AuthorizationProvider;
|
||||
import org.keycloak.authorization.model.PermissionTicket;
|
||||
|
@ -66,7 +64,6 @@ import org.keycloak.services.managers.Auth;
|
|||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||
import org.keycloak.services.managers.UserConsentManager;
|
||||
import org.keycloak.services.managers.UserSessionManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.AbstractSecuredLocalService;
|
||||
import org.keycloak.services.resources.RealmsResource;
|
||||
|
@ -74,9 +71,10 @@ import org.keycloak.services.util.ResolveRelative;
|
|||
import org.keycloak.services.validation.Validation;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.storage.ReadOnlyException;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.userprofile.ValidationException;
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.utils.UserUpdateHelper;
|
||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.utils.CredentialHelper;
|
||||
|
||||
|
@ -371,47 +369,43 @@ public class AccountFormService extends AbstractSecuredLocalService {
|
|||
|
||||
event.event(EventType.UPDATE_PROFILE).client(auth.getClient()).user(auth.getUser());
|
||||
|
||||
UserProfileValidationResult result = forOldAccount(user, formData, session).validate();
|
||||
List<FormMessage> errors = Validation.getFormErrorsFromValidation(result);
|
||||
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
|
||||
UserProfile profile = profileProvider.create(UserProfileContext.ACCOUNT_OLD, formData, user);
|
||||
|
||||
try {
|
||||
// backward compatibility with old account console where attributes are not removed if missing
|
||||
profile.update(false, (attributeName, userModel) -> {
|
||||
if (attributeName.equals(UserModel.EMAIL)) {
|
||||
user.setEmailVerified(false);
|
||||
event.detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, user.getEmail()).success();
|
||||
}
|
||||
if (attributeName.equals(UserModel.FIRST_NAME)) {
|
||||
event.detail(Details.PREVIOUS_FIRST_NAME, oldFirstName).detail(Details.UPDATED_FIRST_NAME, user.getFirstName());
|
||||
}
|
||||
if (attributeName.equals(UserModel.LAST_NAME)) {
|
||||
event.detail(Details.PREVIOUS_LAST_NAME, oldLastName).detail(Details.UPDATED_LAST_NAME, user.getLastName());
|
||||
}
|
||||
});
|
||||
} catch (ValidationException pve) {
|
||||
List<FormMessage> errors = Validation.getFormErrorsFromValidation(pve.getErrors());
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
setReferrerOnPage();
|
||||
Response.Status status = Status.OK;
|
||||
|
||||
if (result.hasFailureOfErrorType(Messages.READ_ONLY_USERNAME)) {
|
||||
if (pve.hasError(Messages.READ_ONLY_USERNAME)) {
|
||||
status = Response.Status.BAD_REQUEST;
|
||||
} else if (result.hasFailureOfErrorType(Messages.EMAIL_EXISTS, Messages.USERNAME_EXISTS)) {
|
||||
} else if (pve.hasError(Messages.EMAIL_EXISTS, Messages.USERNAME_EXISTS)) {
|
||||
status = Response.Status.CONFLICT;
|
||||
}
|
||||
|
||||
return account.setErrors(status, errors).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT);
|
||||
}
|
||||
|
||||
UserProfile updatedProfile = result.getProfile();
|
||||
String newEmail = updatedProfile.getAttributes().getFirstAttribute(UserModel.EMAIL);
|
||||
String newFirstName = updatedProfile.getAttributes().getFirstAttribute(UserModel.FIRST_NAME);
|
||||
String newLastName = updatedProfile.getAttributes().getFirstAttribute(UserModel.LAST_NAME);
|
||||
|
||||
|
||||
try {
|
||||
// backward compatibility with old account console where attributes are not removed if missing
|
||||
UserUpdateHelper.updateAccountOldConsole(realm, user, updatedProfile);
|
||||
} catch (ReadOnlyException e) {
|
||||
setReferrerOnPage();
|
||||
return account.setError(Response.Status.BAD_REQUEST, Messages.READ_ONLY_USER).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT);
|
||||
}
|
||||
|
||||
if (result.hasAttributeChanged(UserModel.FIRST_NAME)) {
|
||||
event.detail(Details.PREVIOUS_FIRST_NAME, oldFirstName).detail(Details.UPDATED_FIRST_NAME, newFirstName);
|
||||
}
|
||||
if (result.hasAttributeChanged(UserModel.LAST_NAME)) {
|
||||
event.detail(Details.PREVIOUS_LAST_NAME, oldLastName).detail(Details.UPDATED_LAST_NAME, newLastName);
|
||||
}
|
||||
if (result.hasAttributeChanged(UserModel.EMAIL)) {
|
||||
user.setEmailVerified(false);
|
||||
event.detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, newEmail);
|
||||
}
|
||||
|
||||
event.success();
|
||||
setReferrerOnPage();
|
||||
return account.setSuccess(Messages.ACCOUNT_UPDATED).createResponse(AccountPages.ACCOUNT);
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
*/
|
||||
package org.keycloak.services.resources.account;
|
||||
|
||||
import static org.keycloak.userprofile.profile.UserProfileContextFactory.forAccountService;
|
||||
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
|
@ -42,14 +40,15 @@ import org.keycloak.representations.account.UserRepresentation;
|
|||
import org.keycloak.services.ErrorResponse;
|
||||
import org.keycloak.services.managers.Auth;
|
||||
import org.keycloak.services.managers.UserConsentManager;
|
||||
import org.keycloak.services.managers.UserSessionManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.account.resources.ResourcesService;
|
||||
import org.keycloak.services.util.ResolveRelative;
|
||||
import org.keycloak.storage.ReadOnlyException;
|
||||
import org.keycloak.theme.Theme;
|
||||
import org.keycloak.userprofile.utils.UserUpdateHelper;
|
||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.userprofile.ValidationException;
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
|
@ -158,25 +157,26 @@ public class AccountRestService {
|
|||
|
||||
event.event(EventType.UPDATE_PROFILE).client(auth.getClient()).user(auth.getUser());
|
||||
|
||||
UserProfileValidationResult result = forAccountService(user, rep, session).validate();
|
||||
|
||||
if (result.hasFailureOfErrorType(Messages.READ_ONLY_USERNAME))
|
||||
return ErrorResponse.error(Messages.READ_ONLY_USERNAME, Response.Status.BAD_REQUEST);
|
||||
if (result.hasFailureOfErrorType(Messages.USERNAME_EXISTS))
|
||||
return ErrorResponse.exists(Messages.USERNAME_EXISTS);
|
||||
if (result.hasFailureOfErrorType(Messages.EMAIL_EXISTS))
|
||||
return ErrorResponse.exists(Messages.EMAIL_EXISTS);
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
// Here should be possibility to somehow return all errors?
|
||||
String firstErrorMessage = result.getErrors().get(0).getFailedValidations().get(0).getErrorType();
|
||||
return ErrorResponse.error(firstErrorMessage, Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
UserUpdateHelper.updateAccount(realm, user, result.getProfile());
|
||||
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
|
||||
UserProfile profile = profileProvider.create(UserProfileContext.ACCOUNT, rep.toAttributes(), auth.getUser());
|
||||
|
||||
profile.update();
|
||||
|
||||
event.success();
|
||||
|
||||
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);
|
||||
} catch (ReadOnlyException e) {
|
||||
return ErrorResponse.error(Messages.READ_ONLY_USER, Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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.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.resources.admin.permissions.AdminPermissionEvaluator;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*
|
||||
*/
|
||||
public class UserProfileResource {
|
||||
|
||||
@Context
|
||||
protected KeycloakSession session;
|
||||
|
||||
protected RealmModel realm;
|
||||
private AdminPermissionEvaluator auth;
|
||||
|
||||
public UserProfileResource(RealmModel realm, AdminPermissionEvaluator auth) {
|
||||
this.realm = realm;
|
||||
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();
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("configuration")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public Response updateConfiguration(String text) throws IOException {
|
||||
|
||||
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 Response.ok(t.getConfiguration()).type(MediaType.APPLICATION_JSON).build();
|
||||
}
|
||||
|
||||
}
|
|
@ -72,10 +72,9 @@ import org.keycloak.services.resources.account.AccountFormService;
|
|||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||
import org.keycloak.services.validation.Validation;
|
||||
import org.keycloak.storage.ReadOnlyException;
|
||||
import org.keycloak.userprofile.utils.UserUpdateHelper;
|
||||
import org.keycloak.userprofile.validation.AttributeValidationResult;
|
||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
||||
import org.keycloak.userprofile.validation.ValidationResult;
|
||||
import org.keycloak.userprofile.ValidationException;
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
import org.keycloak.utils.ProfileHelper;
|
||||
|
||||
import javax.ws.rs.BadRequestException;
|
||||
|
@ -113,7 +112,7 @@ import java.util.stream.Stream;
|
|||
|
||||
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
|
||||
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
|
||||
import static org.keycloak.userprofile.profile.UserProfileContextFactory.forUserResource;
|
||||
import static org.keycloak.userprofile.UserProfileContext.USER_API;
|
||||
|
||||
/**
|
||||
* Base resource for managing users
|
||||
|
@ -170,11 +169,14 @@ public class UserResource {
|
|||
wasPermanentlyLockedOut = session.getProvider(BruteForceProtector.class).isPermanentlyLockedOut(session, realm, user);
|
||||
}
|
||||
|
||||
Response response = validateUserProfile(user, rep, session);
|
||||
UserProfile profile = session.getProvider(UserProfileProvider.class).create(USER_API, rep.toAttributes(), user);
|
||||
|
||||
Response response = validateUserProfile(profile);
|
||||
if (response != null) {
|
||||
return response;
|
||||
}
|
||||
updateUserFromRep(user, rep, session, true);
|
||||
profile.update(rep.getAttributes() != null);
|
||||
updateUserFromRep(profile, user, rep, session, true);
|
||||
RepresentationToModel.createCredentials(rep, session, realm, user, true);
|
||||
|
||||
// we need to do it here as the attributes would be overwritten by what is in the rep
|
||||
|
@ -203,25 +205,25 @@ public class UserResource {
|
|||
}
|
||||
}
|
||||
|
||||
public static Response validateUserProfile(UserModel user, UserRepresentation rep, KeycloakSession session) {
|
||||
UserProfileValidationResult result = forUserResource(user, rep, session).validate();
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
for (AttributeValidationResult attrValidation : result.getErrors()) {
|
||||
StringBuilder s = new StringBuilder("Failed to update attribute " + attrValidation.getField() + ": ");
|
||||
for (ValidationResult valResult : attrValidation.getFailedValidations()) {
|
||||
s.append(valResult.getErrorType() + ", ");
|
||||
}
|
||||
public static Response validateUserProfile(UserProfile profile) {
|
||||
try {
|
||||
profile.validate();
|
||||
} catch (ValidationException pve) {
|
||||
for (ValidationException.Error error : pve.getErrors()) {
|
||||
StringBuilder s = new StringBuilder("Failed to update attribute " + error.getAttribute() + ": ");
|
||||
|
||||
s.append(error.getMessage()).append(", ");
|
||||
|
||||
logger.warn(s);
|
||||
}
|
||||
return ErrorResponse.error("Could not update user! See server log for more details", Response.Status.BAD_REQUEST);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void updateUserFromRep(UserModel user, UserRepresentation rep, KeycloakSession session, boolean isUpdateExistingUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void updateUserFromRep(UserProfile profile, UserModel user, UserRepresentation rep, KeycloakSession session, boolean isUpdateExistingUser) {
|
||||
boolean removeMissingRequiredActions = isUpdateExistingUser;
|
||||
UserUpdateHelper.updateUserResource(session, user, rep, rep.getAttributes() != null);
|
||||
|
||||
if (rep.isEnabled() != null) user.setEnabled(rep.isEnabled());
|
||||
if (rep.isEmailVerified() != null) user.setEmailVerified(rep.isEmailVerified());
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
*/
|
||||
package org.keycloak.services.resources.admin;
|
||||
|
||||
import static org.keycloak.userprofile.UserProfileContext.USER_API;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||
|
@ -39,6 +41,8 @@ import org.keycloak.services.ErrorResponse;
|
|||
import org.keycloak.services.ForbiddenException;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||
import org.keycloak.services.resources.admin.permissions.UserPermissionEvaluator;
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
|
@ -146,15 +150,19 @@ public class UsersResource {
|
|||
}
|
||||
}
|
||||
|
||||
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
|
||||
|
||||
UserProfile profile = profileProvider.create(USER_API, rep.toAttributes());
|
||||
|
||||
try {
|
||||
Response response = UserResource.validateUserProfile(null, rep, session);
|
||||
Response response = UserResource.validateUserProfile(profile);
|
||||
if (response != null) {
|
||||
return response;
|
||||
}
|
||||
|
||||
UserModel user = session.users().addUser(realm, username);
|
||||
UserModel user = profile.create();
|
||||
|
||||
UserResource.updateUserFromRep(user, rep, session, false);
|
||||
UserResource.updateUserFromRep(profile, user, rep, session, false);
|
||||
RepresentationToModel.createFederatedIdentities(rep, session, realm, user);
|
||||
RepresentationToModel.createGroups(rep, realm, user);
|
||||
|
||||
|
|
|
@ -26,21 +26,17 @@ import org.keycloak.policy.PasswordPolicyManagerProvider;
|
|||
import org.keycloak.policy.PolicyError;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.userprofile.validation.AttributeValidationResult;
|
||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
||||
import org.keycloak.userprofile.ValidationException;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class Validation {
|
||||
|
||||
public static final String FIELD_PASSWORD_CONFIRM = "password-confirm";
|
||||
public static final String FIELD_EMAIL = "email";
|
||||
public static final String FIELD_LAST_NAME = "lastName";
|
||||
public static final String FIELD_FIRST_NAME = "firstName";
|
||||
public static final String FIELD_PASSWORD = "password";
|
||||
public static final String FIELD_USERNAME = "username";
|
||||
public static final String FIELD_OTP_CODE = "totp";
|
||||
|
@ -49,76 +45,10 @@ public class Validation {
|
|||
// 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-]+)*");
|
||||
|
||||
public static List<FormMessage> validateRegistrationForm(KeycloakSession session, RealmModel realm, MultivaluedMap<String, String> formData, List<String> requiredCredentialTypes, PasswordPolicy policy) {
|
||||
List<FormMessage> errors = new ArrayList<>();
|
||||
|
||||
if (!realm.isRegistrationEmailAsUsername() && isBlank(formData.getFirst(FIELD_USERNAME))) {
|
||||
addError(errors, FIELD_USERNAME, Messages.MISSING_USERNAME);
|
||||
}
|
||||
|
||||
if (isBlank(formData.getFirst(FIELD_FIRST_NAME))) {
|
||||
addError(errors, FIELD_FIRST_NAME, Messages.MISSING_FIRST_NAME);
|
||||
}
|
||||
|
||||
if (isBlank(formData.getFirst(FIELD_LAST_NAME))) {
|
||||
addError(errors, FIELD_LAST_NAME, Messages.MISSING_LAST_NAME);
|
||||
}
|
||||
|
||||
if (isBlank(formData.getFirst(FIELD_EMAIL))) {
|
||||
addError(errors, FIELD_EMAIL, Messages.MISSING_EMAIL);
|
||||
} else if (!isEmailValid(formData.getFirst(FIELD_EMAIL))) {
|
||||
addError(errors, FIELD_EMAIL, Messages.INVALID_EMAIL);
|
||||
}
|
||||
|
||||
if (requiredCredentialTypes.contains(CredentialRepresentation.PASSWORD)) {
|
||||
if (isBlank(formData.getFirst(FIELD_PASSWORD))) {
|
||||
addError(errors, FIELD_PASSWORD, Messages.MISSING_PASSWORD);
|
||||
} else if (!formData.getFirst(FIELD_PASSWORD).equals(formData.getFirst(FIELD_PASSWORD_CONFIRM))) {
|
||||
addError(errors, FIELD_PASSWORD_CONFIRM, Messages.INVALID_PASSWORD_CONFIRM);
|
||||
}
|
||||
}
|
||||
|
||||
if (formData.getFirst(FIELD_PASSWORD) != null) {
|
||||
PolicyError err = session.getProvider(PasswordPolicyManagerProvider.class).validate(realm.isRegistrationEmailAsUsername() ? formData.getFirst(FIELD_EMAIL) : formData.getFirst(FIELD_USERNAME), formData.getFirst(FIELD_PASSWORD));
|
||||
if (err != null)
|
||||
errors.add(new FormMessage(FIELD_PASSWORD, err.getMessage(), err.getParameters()));
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
private static void addError(List<FormMessage> errors, String field, String message){
|
||||
errors.add(new FormMessage(field, message));
|
||||
}
|
||||
|
||||
public static List<FormMessage> validateUpdateProfileForm(RealmModel realm, MultivaluedMap<String, String> formData) {
|
||||
return validateUpdateProfileForm(realm, formData, realm.isEditUsernameAllowed());
|
||||
}
|
||||
|
||||
public static List<FormMessage> validateUpdateProfileForm(RealmModel realm, MultivaluedMap<String, String> formData, boolean userNameRequired) {
|
||||
List<FormMessage> errors = new ArrayList<>();
|
||||
|
||||
if (!realm.isRegistrationEmailAsUsername() && userNameRequired && isBlank(formData.getFirst(FIELD_USERNAME))) {
|
||||
addError(errors, FIELD_USERNAME, Messages.MISSING_USERNAME);
|
||||
}
|
||||
|
||||
if (isBlank(formData.getFirst(FIELD_FIRST_NAME))) {
|
||||
addError(errors, FIELD_FIRST_NAME, Messages.MISSING_FIRST_NAME);
|
||||
}
|
||||
|
||||
if (isBlank(formData.getFirst(FIELD_LAST_NAME))) {
|
||||
addError(errors, FIELD_LAST_NAME, Messages.MISSING_LAST_NAME);
|
||||
}
|
||||
|
||||
if (isBlank(formData.getFirst(FIELD_EMAIL))) {
|
||||
addError(errors, FIELD_EMAIL, Messages.MISSING_EMAIL);
|
||||
} else if (!isEmailValid(formData.getFirst(FIELD_EMAIL))) {
|
||||
addError(errors, FIELD_EMAIL, Messages.INVALID_EMAIL);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if user object contains all mandatory fields.
|
||||
*
|
||||
|
@ -155,12 +85,12 @@ public class Validation {
|
|||
}
|
||||
|
||||
|
||||
public static List<FormMessage> getFormErrorsFromValidation(UserProfileValidationResult results) {
|
||||
List<FormMessage> errors = new ArrayList<>();
|
||||
for (AttributeValidationResult result : results.getErrors()) {
|
||||
result.getFailedValidations().forEach(o -> addError(errors, result.getField(), o.getErrorType()));
|
||||
public static List<FormMessage> getFormErrorsFromValidation(List<ValidationException.Error> errors) {
|
||||
List<FormMessage> messages = new ArrayList<>();
|
||||
for (ValidationException.Error error : errors) {
|
||||
addError(messages, error.getAttribute(), error.getMessage());
|
||||
}
|
||||
return errors;
|
||||
return messages;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,152 +0,0 @@
|
|||
/*
|
||||
*
|
||||
* * Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* * and other contributors as indicated by the @author tags.
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.userprofile.validation.StaticValidators;
|
||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
||||
import org.keycloak.userprofile.validation.ValidationChainBuilder;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public class LegacyUserProfileProvider implements UserProfileProvider {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final Pattern readOnlyAttributes;
|
||||
private final Pattern adminReadOnlyAttributes;
|
||||
|
||||
public LegacyUserProfileProvider(KeycloakSession session, Pattern readOnlyAttributes, Pattern adminReadOnlyAttributes) {
|
||||
this.session = session;
|
||||
this.readOnlyAttributes = readOnlyAttributes;
|
||||
this.adminReadOnlyAttributes = adminReadOnlyAttributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserProfileValidationResult validate(UserProfileContext updateContext, UserProfile updatedProfile) {
|
||||
RealmModel realm = this.session.getContext().getRealm();
|
||||
|
||||
ValidationChainBuilder builder = ValidationChainBuilder.builder();
|
||||
switch (updateContext.getUpdateEvent()) {
|
||||
case UserResource:
|
||||
addReadOnlyAttributeValidators(builder, adminReadOnlyAttributes, updateContext, updatedProfile);
|
||||
break;
|
||||
case IdpReview:
|
||||
addBasicValidators(builder, !realm.isRegistrationEmailAsUsername());
|
||||
addReadOnlyAttributeValidators(builder, readOnlyAttributes, updateContext, updatedProfile);
|
||||
break;
|
||||
case Account:
|
||||
case RegistrationProfile:
|
||||
case UpdateProfile:
|
||||
addBasicValidators(builder, !realm.isRegistrationEmailAsUsername() && realm.isEditUsernameAllowed());
|
||||
addReadOnlyAttributeValidators(builder, readOnlyAttributes, updateContext, updatedProfile);
|
||||
addSessionValidators(builder);
|
||||
break;
|
||||
case RegistrationUserCreation:
|
||||
addUserCreationValidators(builder);
|
||||
addReadOnlyAttributeValidators(builder, readOnlyAttributes, updateContext, updatedProfile);
|
||||
break;
|
||||
}
|
||||
return new UserProfileValidationResult(builder.build().validate(updateContext,updatedProfile), updatedProfile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadOnlyAttribute(String key) {
|
||||
return readOnlyAttributes.matcher(key).find() || adminReadOnlyAttributes.matcher(key).find();
|
||||
}
|
||||
|
||||
private void addUserCreationValidators(ValidationChainBuilder builder) {
|
||||
RealmModel realm = this.session.getContext().getRealm();
|
||||
|
||||
if (realm.isRegistrationEmailAsUsername()) {
|
||||
builder.addAttributeValidator().forAttribute(UserModel.EMAIL)
|
||||
.addSingleAttributeValueValidationFunction(Messages.INVALID_EMAIL, StaticValidators.isEmailValid())
|
||||
.addSingleAttributeValueValidationFunction(Messages.MISSING_EMAIL, StaticValidators.isBlank())
|
||||
.addSingleAttributeValueValidationFunction(Messages.EMAIL_EXISTS, StaticValidators.doesEmailExist(session)).build()
|
||||
.build();
|
||||
|
||||
|
||||
} else {
|
||||
builder.addAttributeValidator().forAttribute(UserModel.USERNAME)
|
||||
.addSingleAttributeValueValidationFunction(Messages.MISSING_USERNAME, StaticValidators.isBlank())
|
||||
.addSingleAttributeValueValidationFunction(Messages.USERNAME_EXISTS,
|
||||
(value, o) -> session.users().getUserByUsername(realm, value) == null)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private void addBasicValidators(ValidationChainBuilder builder, boolean userNameExistsCondition) {
|
||||
|
||||
builder.addAttributeValidator().forAttribute(UserModel.USERNAME)
|
||||
.addSingleAttributeValueValidationFunction(Messages.MISSING_USERNAME, StaticValidators.checkUsernameExists(userNameExistsCondition)).build()
|
||||
|
||||
.addAttributeValidator().forAttribute(UserModel.FIRST_NAME)
|
||||
.addSingleAttributeValueValidationFunction(Messages.MISSING_FIRST_NAME, StaticValidators.isBlank()).build()
|
||||
|
||||
.addAttributeValidator().forAttribute(UserModel.LAST_NAME)
|
||||
.addSingleAttributeValueValidationFunction(Messages.MISSING_LAST_NAME, StaticValidators.isBlank()).build()
|
||||
|
||||
.addAttributeValidator().forAttribute(UserModel.EMAIL)
|
||||
.addSingleAttributeValueValidationFunction(Messages.MISSING_EMAIL, StaticValidators.isBlank())
|
||||
.addSingleAttributeValueValidationFunction(Messages.INVALID_EMAIL, StaticValidators.isEmailValid())
|
||||
.build();
|
||||
}
|
||||
|
||||
private void addSessionValidators(ValidationChainBuilder builder) {
|
||||
RealmModel realm = this.session.getContext().getRealm();
|
||||
builder.addAttributeValidator().forAttribute(UserModel.USERNAME)
|
||||
.addSingleAttributeValueValidationFunction(Messages.USERNAME_EXISTS, StaticValidators.userNameExists(session))
|
||||
.addSingleAttributeValueValidationFunction(Messages.READ_ONLY_USERNAME, StaticValidators.isUserMutable(realm)).build()
|
||||
|
||||
.addAttributeValidator().forAttribute(UserModel.EMAIL)
|
||||
.addSingleAttributeValueValidationFunction(Messages.EMAIL_EXISTS, StaticValidators.isEmailDuplicated(session))
|
||||
.addSingleAttributeValueValidationFunction(Messages.USERNAME_EXISTS, StaticValidators.doesEmailExistAsUsername(session)).build()
|
||||
.build();
|
||||
}
|
||||
|
||||
private void addReadOnlyAttributeValidators(ValidationChainBuilder builder, Pattern configuredReadOnlyAttrs, UserProfileContext updateContext, UserProfile updatedProfile) {
|
||||
addValidatorsForReadOnlyAttributes(builder, configuredReadOnlyAttrs, updatedProfile);
|
||||
addValidatorsForReadOnlyAttributes(builder, configuredReadOnlyAttrs, updateContext.getCurrentProfile());
|
||||
}
|
||||
|
||||
|
||||
private void addValidatorsForReadOnlyAttributes(ValidationChainBuilder builder, Pattern configuredReadOnlyAttrsPattern, UserProfile profile) {
|
||||
if (profile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
profile.getAttributes().keySet().stream()
|
||||
.filter(currentAttrName -> configuredReadOnlyAttrsPattern.matcher(currentAttrName).find())
|
||||
.forEach((currentAttrName) ->
|
||||
builder.addAttributeValidator().forAttribute(currentAttrName)
|
||||
.addValidationFunction(Messages.UPDATE_READ_ONLY_ATTRIBUTES_REJECTED, StaticValidators.isReadOnlyAttributeUnchanged(currentAttrName)).build()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public class LegacyUserProfileProviderFactory implements UserProfileProviderFactory {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(LegacyUserProfileProviderFactory.class);
|
||||
|
||||
UserProfileProvider provider;
|
||||
|
||||
// Attributes, which can't be updated by user himself
|
||||
private Pattern readOnlyAttributesPattern;
|
||||
|
||||
// Attributes, which can't be updated by administrator
|
||||
private Pattern adminReadOnlyAttributesPattern;
|
||||
|
||||
private String[] DEFAULT_READ_ONLY_ATTRIBUTES = { "KERBEROS_PRINCIPAL", "LDAP_ID", "LDAP_ENTRY_DN", "CREATED_TIMESTAMP", "createTimestamp", "modifyTimestamp", "userCertificate", "saml.persistent.name.id.for.*", "ENABLED", "EMAIL_VERIFIED", "disabledReason" };
|
||||
private String[] DEFAULT_ADMIN_READ_ONLY_ATTRIBUTES = { "KERBEROS_PRINCIPAL", "LDAP_ID", "LDAP_ENTRY_DN", "CREATED_TIMESTAMP", "createTimestamp", "modifyTimestamp" };
|
||||
|
||||
@Override
|
||||
public UserProfileProvider create(KeycloakSession session) {
|
||||
provider = new LegacyUserProfileProvider(session, readOnlyAttributesPattern, adminReadOnlyAttributesPattern);
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
this.readOnlyAttributesPattern = getRegexPatternString(config, "read-only-attributes", DEFAULT_READ_ONLY_ATTRIBUTES);
|
||||
this.adminReadOnlyAttributesPattern = getRegexPatternString(config, "admin-read-only-attributes", DEFAULT_ADMIN_READ_ONLY_ATTRIBUTES);
|
||||
}
|
||||
|
||||
private Pattern getRegexPatternString(Config.Scope config, String configKey, String[] builtinReadOnlyAttributes) {
|
||||
String[] readOnlyAttributesCfg = config.getArray(configKey);
|
||||
List<String> readOnlyAttributes = new ArrayList<>(Arrays.asList(builtinReadOnlyAttributes));
|
||||
if (readOnlyAttributesCfg != null) {
|
||||
List<String> configured = Arrays.asList(readOnlyAttributesCfg);
|
||||
logger.infof("Configured %s: %s", configKey, configured);
|
||||
readOnlyAttributes.addAll(configured);
|
||||
}
|
||||
|
||||
String regexStr = readOnlyAttributes.stream()
|
||||
.map(configAttrName -> configAttrName.endsWith("*")
|
||||
? "^" + Pattern.quote(configAttrName.substring(0, configAttrName.length() - 1)) + ".*$"
|
||||
: "^" + Pattern.quote(configAttrName ) + "$")
|
||||
.collect(Collectors.joining("|"));
|
||||
regexStr = "(?i:" + regexStr + ")";
|
||||
|
||||
logger.debugf("Regex used for %s: %s", configKey, regexStr);
|
||||
return Pattern.compile(regexStr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
public static final String PROVIDER_ID = "legacy-user-profile";
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,411 @@
|
|||
/*
|
||||
*
|
||||
* * Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* * and other contributors as indicated by the @author tags.
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile.legacy;
|
||||
|
||||
import static org.keycloak.userprofile.DefaultAttributes.READ_ONLY_ATTRIBUTE_KEY;
|
||||
import static org.keycloak.userprofile.UserProfileContext.*;
|
||||
import static org.keycloak.userprofile.UserProfileContext.ACCOUNT;
|
||||
import static org.keycloak.userprofile.UserProfileContext.ACCOUNT_OLD;
|
||||
import static org.keycloak.userprofile.UserProfileContext.IDP_REVIEW;
|
||||
import static org.keycloak.userprofile.UserProfileContext.REGISTRATION_PROFILE;
|
||||
import static org.keycloak.userprofile.UserProfileContext.UPDATE_PROFILE;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.common.util.ObjectUtil;
|
||||
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.services.validation.Validation;
|
||||
import org.keycloak.userprofile.Attributes;
|
||||
import org.keycloak.userprofile.DefaultAttributes;
|
||||
import org.keycloak.userprofile.DefaultUserProfile;
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.userprofile.UserProfileMetadata;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
import org.keycloak.userprofile.UserProfileProviderFactory;
|
||||
import org.keycloak.userprofile.AttributeValidatorMetadata;
|
||||
import org.keycloak.userprofile.validation.Validator;
|
||||
|
||||
/**
|
||||
* <p>A base class for {@link UserProfileProvider} implementations providing the main hooks for customizations.
|
||||
*
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public abstract class AbstractUserProfileProvider<U extends UserProfileProvider> implements UserProfileProvider, UserProfileProviderFactory<U> {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(DefaultAttributes.class);
|
||||
|
||||
public static Pattern getRegexPatternString(String[] builtinReadOnlyAttributes) {
|
||||
if (builtinReadOnlyAttributes != null) {
|
||||
List<String> readOnlyAttributes = new ArrayList<>(Arrays.asList(builtinReadOnlyAttributes));
|
||||
|
||||
String regexStr = readOnlyAttributes.stream()
|
||||
.map(configAttrName -> configAttrName.endsWith("*")
|
||||
? "^" + Pattern.quote(configAttrName.substring(0, configAttrName.length() - 1)) + ".*$"
|
||||
: "^" + Pattern.quote(configAttrName) + "$")
|
||||
.collect(Collectors.joining("|"));
|
||||
regexStr = "(?i:" + regexStr + ")";
|
||||
|
||||
return Pattern.compile(regexStr);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Validator isReadOnlyAttributeUnchanged(Pattern pattern) {
|
||||
return (context) -> {
|
||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
||||
String key = attribute.getKey();
|
||||
|
||||
if (!pattern.matcher(key).find()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
List<String> values = attribute.getValue();
|
||||
|
||||
if (values == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
UserModel user = context.getUser();
|
||||
|
||||
List<String> existingAttrValues = user == null ? null : user.getAttribute(key);
|
||||
String existingValue = null;
|
||||
|
||||
if (existingAttrValues != null && !existingAttrValues.isEmpty()) {
|
||||
existingValue = existingAttrValues.get(0);
|
||||
}
|
||||
|
||||
if (values.isEmpty() && existingValue != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String value = null;
|
||||
|
||||
if (!values.isEmpty()) {
|
||||
value = values.get(0);
|
||||
}
|
||||
|
||||
boolean result = ObjectUtil.isEqualOrBothNull(value, existingValue);
|
||||
|
||||
if (!result) {
|
||||
logger.warnf("Attempt to edit denied attribute '%s' of user '%s'", pattern, user == null ? "new user" : user.getFirstAttribute(UserModel.USERNAME));
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* There are the declarations for creating the built-in validations for read-only attributes. Regardless of the context where
|
||||
* user profiles are used. They are related to internal attributes with hard conditions on them in terms of management.
|
||||
*/
|
||||
private static String UPDATE_READ_ONLY_ATTRIBUTES_REJECTED = "updateReadOnlyAttributesRejectedMessage";
|
||||
private static String[] DEFAULT_READ_ONLY_ATTRIBUTES = { "KERBEROS_PRINCIPAL", "LDAP_ID", "LDAP_ENTRY_DN", "CREATED_TIMESTAMP", "createTimestamp", "modifyTimestamp", "userCertificate", "saml.persistent.name.id.for.*", "ENABLED", "EMAIL_VERIFIED", "disabledReason" };
|
||||
private static String[] DEFAULT_ADMIN_READ_ONLY_ATTRIBUTES = { "KERBEROS_PRINCIPAL", "LDAP_ID", "LDAP_ENTRY_DN", "CREATED_TIMESTAMP", "createTimestamp", "modifyTimestamp" };
|
||||
private static Pattern readOnlyAttributesPattern = getRegexPatternString(DEFAULT_READ_ONLY_ATTRIBUTES);
|
||||
private static Pattern adminReadOnlyAttributesPattern = getRegexPatternString(DEFAULT_ADMIN_READ_ONLY_ATTRIBUTES);
|
||||
|
||||
protected final Map<UserProfileContext, UserProfileMetadata> contextualMetadataRegistry;
|
||||
protected final KeycloakSession session;
|
||||
|
||||
public AbstractUserProfileProvider() {
|
||||
// for reflection
|
||||
this(null, new HashMap<>());
|
||||
}
|
||||
|
||||
public AbstractUserProfileProvider(KeycloakSession session, Map<UserProfileContext, UserProfileMetadata> contextualMetadataRegistry) {
|
||||
this.session = session;
|
||||
this.contextualMetadataRegistry = contextualMetadataRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserProfile create(UserProfileContext context, UserModel user) {
|
||||
return createUserProfile(context, user.getAttributes(), user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserProfile create(UserProfileContext context, Map<String, ?> attributes, UserModel user) {
|
||||
return createUserProfile(context, attributes, user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserProfile create(UserProfileContext context, Map<String, ?> attributes) {
|
||||
return createUserProfile(context, attributes, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public U create(KeycloakSession session) {
|
||||
return create(session, contextualMetadataRegistry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
Pattern pattern = getRegexPatternString(config.getArray("read-only-attributes"));
|
||||
AttributeValidatorMetadata readOnlyValidator = null;
|
||||
|
||||
if (pattern != null) {
|
||||
readOnlyValidator = Validators.create(Messages.UPDATE_READ_ONLY_ATTRIBUTES_REJECTED, isReadOnlyAttributeUnchanged(pattern));
|
||||
}
|
||||
|
||||
addContextualProfileMetadata(configureUserProfile(createBrokeringProfile(readOnlyValidator)));
|
||||
addContextualProfileMetadata(configureUserProfile(createDefaultProfile(ACCOUNT, readOnlyValidator)));
|
||||
addContextualProfileMetadata(configureUserProfile(createDefaultProfile(ACCOUNT_OLD, readOnlyValidator)));
|
||||
addContextualProfileMetadata(configureUserProfile(createDefaultProfile(REGISTRATION_PROFILE, readOnlyValidator)));
|
||||
addContextualProfileMetadata(configureUserProfile(createDefaultProfile(UPDATE_PROFILE, readOnlyValidator)));
|
||||
addContextualProfileMetadata(configureUserProfile(createRegistrationUserCreationProfile()));
|
||||
addContextualProfileMetadata(configureUserProfile(createUserResourceValidation(config)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getConfiguration() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setConfiguration(String configuration) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses can override this method to create their instances of {@link UserProfileProvider}.
|
||||
*
|
||||
* @param session the session
|
||||
* @param metadataRegistry the profile metadata
|
||||
*
|
||||
* @return the profile provider instance
|
||||
*/
|
||||
protected abstract U create(KeycloakSession session, Map<UserProfileContext, UserProfileMetadata> metadataRegistry);
|
||||
|
||||
/**
|
||||
* Sub-types can override this method to customize how contextual profile metadata is configured at init time.
|
||||
*
|
||||
* @param metadata the profile metadata
|
||||
* @return the metadata
|
||||
*/
|
||||
protected UserProfileMetadata configureUserProfile(UserProfileMetadata metadata) {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sub-types can override this method to customize how contextual profile metadata is configured at runtime.
|
||||
*
|
||||
* @param metadata the profile metadata
|
||||
* @param metadata the current session
|
||||
* @return the metadata
|
||||
*/
|
||||
protected UserProfileMetadata configureUserProfile(UserProfileMetadata metadata, KeycloakSession session) {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Function} for creating new users when the creating them using {@link UserProfile#create()}.
|
||||
*
|
||||
* @return a function for creating new users.
|
||||
*/
|
||||
private Function<Attributes, UserModel> createUserFactory() {
|
||||
return new Function<Attributes, UserModel>() {
|
||||
private UserModel user;
|
||||
|
||||
@Override
|
||||
public UserModel apply(Attributes attributes) {
|
||||
if (user == null) {
|
||||
String userName = attributes.getFirstValue(UserModel.USERNAME);
|
||||
|
||||
// fallback to email in case email is allowed
|
||||
if (userName == null) {
|
||||
userName = attributes.getFirstValue(UserModel.EMAIL);
|
||||
}
|
||||
|
||||
user = session.users().addUser(session.getContext().getRealm(), userName);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private void addContextualProfileMetadata(UserProfileMetadata metadata) {
|
||||
if (contextualMetadataRegistry.putIfAbsent(metadata.getContext(), metadata) != null) {
|
||||
throw new IllegalStateException("Multiple profile metadata found for context " + metadata.getContext());
|
||||
}
|
||||
}
|
||||
|
||||
private UserProfileMetadata createRegistrationUserCreationProfile() {
|
||||
UserProfileMetadata metadata = new UserProfileMetadata(REGISTRATION_USER_CREATION);
|
||||
|
||||
metadata.addAttribute(UserModel.USERNAME, Validators.create(Messages.MISSING_USERNAME, (context) -> {
|
||||
RealmModel realm = context.getSession().getContext().getRealm();
|
||||
|
||||
if (!realm.isRegistrationEmailAsUsername()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Validators.isBlank().validate(context);
|
||||
}), Validators.create(Messages.USERNAME_EXISTS,
|
||||
(context) -> {
|
||||
KeycloakSession session = context.getSession();
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
if (realm.isRegistrationEmailAsUsername()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
||||
List<String> values = attribute.getValue();
|
||||
|
||||
if (values.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String value = values.get(0);
|
||||
|
||||
UserModel existing = session.users().getUserByUsername(realm, value);
|
||||
return existing == null;
|
||||
}));
|
||||
|
||||
metadata.addAttribute(UserModel.EMAIL, Validators.create(Messages.INVALID_EMAIL, (context) -> {
|
||||
RealmModel realm = context.getSession().getContext().getRealm();
|
||||
|
||||
if (!realm.isRegistrationEmailAsUsername()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
||||
List<String> values = attribute.getValue();
|
||||
|
||||
if (values.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String value = values.get(0);
|
||||
|
||||
return Validation.isBlank(value) || Validation.isEmailValid(value);
|
||||
}));
|
||||
|
||||
metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, new AttributeValidatorMetadata(UPDATE_READ_ONLY_ATTRIBUTES_REJECTED, isReadOnlyAttributeUnchanged(readOnlyAttributesPattern)));
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private UserProfileMetadata createDefaultProfile(UserProfileContext context, AttributeValidatorMetadata readOnlyValidator) {
|
||||
UserProfileMetadata metadata = new UserProfileMetadata(context);
|
||||
|
||||
metadata.addAttribute(UserModel.USERNAME, Validators.create(Messages.MISSING_USERNAME, Validators.checkUsernameExists()),
|
||||
Validators.create(Messages.USERNAME_EXISTS, Validators.userNameExists()),
|
||||
Validators.create(Messages.READ_ONLY_USERNAME, Validators.isUserMutable()));
|
||||
|
||||
metadata.addAttribute(UserModel.FIRST_NAME, Validators.create(Messages.MISSING_FIRST_NAME, Validators.isBlank()));
|
||||
|
||||
metadata.addAttribute(UserModel.LAST_NAME, Validators.create(Messages.MISSING_LAST_NAME, Validators.isBlank()));
|
||||
|
||||
metadata.addAttribute(UserModel.EMAIL, Validators.create(Messages.MISSING_EMAIL, Validators.isBlank()),
|
||||
Validators.create(Messages.INVALID_EMAIL, Validators.isEmailValid()),
|
||||
Validators.create(Messages.EMAIL_EXISTS, Validators.isEmailDuplicated()),
|
||||
Validators.create(Messages.USERNAME_EXISTS, Validators.doesEmailExistAsUsername()));
|
||||
|
||||
List<AttributeValidatorMetadata> readonlyValidators = new ArrayList<>();
|
||||
|
||||
readonlyValidators.add(new AttributeValidatorMetadata(UPDATE_READ_ONLY_ATTRIBUTES_REJECTED,
|
||||
isReadOnlyAttributeUnchanged(readOnlyAttributesPattern)));
|
||||
|
||||
if (readOnlyValidator != null) {
|
||||
readonlyValidators.add(readOnlyValidator);
|
||||
}
|
||||
|
||||
metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, readonlyValidators);
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private UserProfileMetadata createBrokeringProfile(AttributeValidatorMetadata readOnlyValidator) {
|
||||
UserProfileMetadata metadata = new UserProfileMetadata(IDP_REVIEW);
|
||||
|
||||
metadata.addAttribute(UserModel.USERNAME, Validators
|
||||
.create(Messages.MISSING_USERNAME, Validators.checkFederatedUsernameExists()));
|
||||
|
||||
metadata.addAttribute(UserModel.FIRST_NAME,
|
||||
Validators.create(Messages.MISSING_FIRST_NAME, Validators.isBlank()));
|
||||
|
||||
metadata.addAttribute(UserModel.LAST_NAME,
|
||||
Validators.create(Messages.MISSING_LAST_NAME, Validators.isBlank()));
|
||||
|
||||
metadata.addAttribute(UserModel.EMAIL, Validators.create(Messages.MISSING_EMAIL, Validators.isBlank()),
|
||||
Validators.create(Messages.INVALID_EMAIL, Validators.isEmailValid()));
|
||||
|
||||
List<AttributeValidatorMetadata> readonlyValidators = new ArrayList<>();
|
||||
|
||||
readonlyValidators.add(new AttributeValidatorMetadata(UPDATE_READ_ONLY_ATTRIBUTES_REJECTED,
|
||||
isReadOnlyAttributeUnchanged(readOnlyAttributesPattern)));
|
||||
|
||||
if (readOnlyValidator != null) {
|
||||
readonlyValidators.add(readOnlyValidator);
|
||||
}
|
||||
|
||||
metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, readonlyValidators);
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private UserProfileMetadata createUserResourceValidation(Config.Scope config) {
|
||||
Pattern p = getRegexPatternString(config.getArray("admin-read-only-attributes"));
|
||||
UserProfileMetadata metadata = new UserProfileMetadata(USER_API);
|
||||
List<AttributeValidatorMetadata> readonlyValidators = new ArrayList<>();
|
||||
|
||||
if (p != null) {
|
||||
readonlyValidators.add(Validators.create(Messages.UPDATE_READ_ONLY_ATTRIBUTES_REJECTED, isReadOnlyAttributeUnchanged(p)));
|
||||
}
|
||||
|
||||
readonlyValidators.add(new AttributeValidatorMetadata(UPDATE_READ_ONLY_ATTRIBUTES_REJECTED,
|
||||
isReadOnlyAttributeUnchanged(adminReadOnlyAttributesPattern)));
|
||||
|
||||
metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, readonlyValidators);
|
||||
|
||||
return metadata;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
*
|
||||
* * Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* * and other contributors as indicated by the @author tags.
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile.legacy;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.userprofile.UserProfileMetadata;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public class DefaultUserProfileProvider extends AbstractUserProfileProvider<DefaultUserProfileProvider> {
|
||||
|
||||
private static final String PROVIDER_ID = "legacy-user-profile";
|
||||
|
||||
public DefaultUserProfileProvider() {
|
||||
// for reflection
|
||||
}
|
||||
|
||||
public DefaultUserProfileProvider(KeycloakSession session, Map<UserProfileContext, UserProfileMetadata> validators) {
|
||||
super(session, validators);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DefaultUserProfileProvider create(KeycloakSession session, Map<UserProfileContext, UserProfileMetadata> metadataRegistry) {
|
||||
return new DefaultUserProfileProvider(session, metadataRegistry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int order() {
|
||||
return 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,278 @@
|
|||
/*
|
||||
*
|
||||
* * Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* * and other contributors as indicated by the @author tags.
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
* * You may obtain a copy of the License at
|
||||
* *
|
||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
||||
* *
|
||||
* * Unless required by applicable law or agreed to in writing, software
|
||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* * See the License for the specific language governing permissions and
|
||||
* * limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile.legacy;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.services.validation.Validation;
|
||||
import org.keycloak.userprofile.AttributeContext;
|
||||
import org.keycloak.userprofile.AttributeMetadata;
|
||||
import org.keycloak.userprofile.AttributeValidatorMetadata;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.userprofile.validation.Validator;
|
||||
|
||||
/**
|
||||
* Functions are supposed to return:
|
||||
* - true if validation success
|
||||
* - false if validation fails
|
||||
*
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public class Validators {
|
||||
|
||||
public static final AttributeValidatorMetadata create(String message, Validator validator) {
|
||||
return new AttributeValidatorMetadata(message, validator);
|
||||
}
|
||||
|
||||
public static final Validator isBlank() {
|
||||
return (context) -> {
|
||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
||||
List<String> values = attribute.getValue();
|
||||
|
||||
if (values.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String value = values.get(0);
|
||||
|
||||
return value == null || !Validation.isBlank(value);
|
||||
};
|
||||
}
|
||||
|
||||
public static final Validator isEmailValid() {
|
||||
return (context) -> {
|
||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
||||
List<String> values = attribute.getValue();
|
||||
|
||||
if (values.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String value = values.get(0);
|
||||
|
||||
return Validation.isBlank(value) || Validation.isEmailValid(value);
|
||||
};
|
||||
}
|
||||
|
||||
public static final Validator userNameExists() {
|
||||
return (context) -> {
|
||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
||||
List<String> values = attribute.getValue();
|
||||
|
||||
if (values.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String value = values.get(0);
|
||||
|
||||
if (Validation.isBlank(value)) return true;
|
||||
|
||||
KeycloakSession session = context.getSession();
|
||||
UserModel existing = session.users().getUserByUsername(session.getContext().getRealm(), value);
|
||||
UserModel user = context.getUser();
|
||||
|
||||
return !(user != null
|
||||
&& !value.equals(user.getFirstAttribute(UserModel.USERNAME))
|
||||
&& (existing != null && !existing.getId().equals(user.getId())));
|
||||
};
|
||||
}
|
||||
|
||||
public static final Validator isUserMutable() {
|
||||
return (context) -> {
|
||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
||||
List<String> values = attribute.getValue();
|
||||
|
||||
if (values.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String value = values.get(0);
|
||||
|
||||
if (Validation.isBlank(value)) return true;
|
||||
|
||||
UserModel user = context.getUser();
|
||||
RealmModel realm = context.getSession().getContext().getRealm();
|
||||
|
||||
return !(!realm.isEditUsernameAllowed()
|
||||
&& user != null
|
||||
&& !value.equals(user.getFirstAttribute(UserModel.USERNAME))
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
public static final Validator checkFederatedUsernameExists() {
|
||||
return (context) -> {
|
||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
||||
List<String> values = attribute.getValue();
|
||||
String value = null;
|
||||
|
||||
if (!values.isEmpty()) {
|
||||
value = values.get(0);
|
||||
}
|
||||
|
||||
RealmModel realm = context.getSession().getContext().getRealm();
|
||||
|
||||
return !(!realm.isRegistrationEmailAsUsername() && Validation.isBlank(value));
|
||||
};
|
||||
}
|
||||
|
||||
public static final Validator checkUsernameExists() {
|
||||
return (context) -> {
|
||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
||||
List<String> values = attribute.getValue();
|
||||
String value = null;
|
||||
|
||||
if (!values.isEmpty()) {
|
||||
value = values.get(0);
|
||||
}
|
||||
|
||||
return !Validation.isBlank(value);
|
||||
};
|
||||
}
|
||||
|
||||
public static final Validator doesEmailExistAsUsername() {
|
||||
return (context) -> {
|
||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
||||
List<String> values = attribute.getValue();
|
||||
|
||||
if (values.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String value = values.get(0);
|
||||
|
||||
if (Validation.isBlank(value)) return true;
|
||||
|
||||
KeycloakSession session = context.getSession();
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
UserModel user = context.getUser();
|
||||
|
||||
if (!realm.isDuplicateEmailsAllowed()) {
|
||||
UserModel userByEmail = session.users().getUserByEmail(realm, value);
|
||||
return !(realm.isRegistrationEmailAsUsername() && userByEmail != null && user != null && !userByEmail.getId().equals(user.getId()));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
public static final Validator isEmailDuplicated() {
|
||||
return (context) -> {
|
||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
||||
List<String> values = attribute.getValue();
|
||||
|
||||
if (values.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String value = values.get(0);
|
||||
|
||||
if (Validation.isBlank(value)) return true;
|
||||
|
||||
KeycloakSession session = context.getSession();
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
if (!realm.isDuplicateEmailsAllowed()) {
|
||||
UserModel userByEmail = session.users().getUserByEmail(realm, value);
|
||||
UserModel user = context.getUser();
|
||||
// check for duplicated email
|
||||
return !(userByEmail != null && (user == null || !userByEmail.getId().equals(user.getId())));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
public static final Validator doesEmailExist(KeycloakSession session) {
|
||||
return (context) -> {
|
||||
if (UserProfileContext.REGISTRATION_USER_CREATION.equals(context.getContext())) {
|
||||
RealmModel realm = context.getSession().getContext().getRealm();
|
||||
|
||||
if (!realm.isRegistrationEmailAsUsername()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
||||
List<String> values = attribute.getValue();
|
||||
String value = values.get(0);
|
||||
|
||||
return !(value != null
|
||||
&& !session.getContext().getRealm().isDuplicateEmailsAllowed()
|
||||
&& session.users().getUserByEmail(session.getContext().getRealm(), value) != null);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate String length based on the configuration if string is not blank.
|
||||
*
|
||||
* @param config can contain "max" and "min" keys with integer values
|
||||
* @return true if string is blank or conforms min and max configurations
|
||||
*/
|
||||
public static final Validator length(final Map<String, Object> config) {
|
||||
return (context) -> {
|
||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
||||
List<String> values = attribute.getValue();
|
||||
|
||||
if (values == null || values.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String value = values.get(0);
|
||||
|
||||
if (Validation.isBlank(value))
|
||||
return true;
|
||||
|
||||
if (config.containsKey("min") && value.length() < (Integer) config.get("min")) {
|
||||
return false;
|
||||
}
|
||||
if (config.containsKey("max") && value.length() > (Integer) config.get("max")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validator for "required" validation based on evaluation of the {@link AttributeMetadata#isRequired(AttributeContext)}.
|
||||
*
|
||||
*/
|
||||
public static final Validator requiredByAttributeMetadata() {
|
||||
return (context) -> {
|
||||
if(!context.getMetadata().isRequired(context)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Map.Entry<String, List<String>> attribute = context.getAttribute();
|
||||
List<String> values = attribute.getValue();
|
||||
|
||||
if (values == null || values.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String value = values.get(0);
|
||||
|
||||
return !Validation.isBlank(value);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 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.profile;
|
||||
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.UserProfileAttributes;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class AbstractUserProfile implements UserProfile {
|
||||
|
||||
private final UserProfileAttributes attributes;
|
||||
|
||||
|
||||
public AbstractUserProfile(Map<String, List<String>> attributes, UserProfileProvider profileProvider) {
|
||||
this.attributes = new UserProfileAttributes(attributes, profileProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserProfileAttributes getAttributes() {
|
||||
return this.attributes;
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 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.profile;
|
||||
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
||||
import org.keycloak.userprofile.validation.UserUpdateEvent;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public class DefaultUserProfileContext implements UserProfileContext {
|
||||
private UserProfile currentUserProfile;
|
||||
private final UserProfile updatedProfile;
|
||||
private final UserProfileProvider profileProvider;
|
||||
private UserUpdateEvent userUpdateEvent;
|
||||
|
||||
DefaultUserProfileContext(UserUpdateEvent userUpdateEvent, UserProfile currentUserProfile,
|
||||
UserProfile updatedProfile,
|
||||
UserProfileProvider profileProvider) {
|
||||
this.userUpdateEvent = userUpdateEvent;
|
||||
this.currentUserProfile = currentUserProfile;
|
||||
this.updatedProfile = updatedProfile;
|
||||
this.profileProvider = profileProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserProfile getCurrentProfile() {
|
||||
return currentUserProfile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserUpdateEvent getUpdateEvent(){
|
||||
return userUpdateEvent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserProfileValidationResult validate() {
|
||||
return profileProvider.validate(this, updatedProfile);
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile.profile;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
|
||||
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.account.UserRepresentation;
|
||||
import org.keycloak.services.resources.AttributeFormDataProcessor;
|
||||
import org.keycloak.userprofile.LegacyUserProfileProviderFactory;
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
import org.keycloak.userprofile.profile.representations.AccountUserRepresentationUserProfile;
|
||||
import org.keycloak.userprofile.profile.representations.IdpUserProfile;
|
||||
import org.keycloak.userprofile.profile.representations.UserModelUserProfile;
|
||||
import org.keycloak.userprofile.profile.representations.UserRepresentationUserProfile;
|
||||
import org.keycloak.userprofile.validation.UserUpdateEvent;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public final class UserProfileContextFactory {
|
||||
|
||||
public static DefaultUserProfileContext forIdpReview(SerializedBrokeredIdentityContext currentUser,
|
||||
MultivaluedMap<String, String> formData, KeycloakSession session) {
|
||||
UserProfileProvider profileProvider = getProfileProvider(session);
|
||||
return new DefaultUserProfileContext(UserUpdateEvent.IdpReview, new IdpUserProfile(currentUser, profileProvider),
|
||||
AttributeFormDataProcessor.toUserProfile(formData), profileProvider);
|
||||
}
|
||||
|
||||
public static DefaultUserProfileContext forUpdateProfile(UserModel currentUser,
|
||||
MultivaluedMap<String, String> formData,
|
||||
KeycloakSession session) {
|
||||
UserProfileProvider profileProvider = getProfileProvider(session);
|
||||
return new DefaultUserProfileContext(UserUpdateEvent.UpdateProfile, new UserModelUserProfile(currentUser, profileProvider),
|
||||
AttributeFormDataProcessor.toUserProfile(formData), profileProvider);
|
||||
}
|
||||
|
||||
public static DefaultUserProfileContext forAccountService(UserModel currentUser,
|
||||
UserRepresentation rep, KeycloakSession session) {
|
||||
UserProfileProvider profileProvider = getProfileProvider(session);
|
||||
return new DefaultUserProfileContext(UserUpdateEvent.Account, new UserModelUserProfile(currentUser, profileProvider),
|
||||
new AccountUserRepresentationUserProfile(rep, profileProvider),
|
||||
profileProvider);
|
||||
}
|
||||
|
||||
public static DefaultUserProfileContext forOldAccount(UserModel currentUser,
|
||||
MultivaluedMap<String, String> formData, KeycloakSession session) {
|
||||
UserProfileProvider profileProvider = getProfileProvider(session);
|
||||
return new DefaultUserProfileContext(UserUpdateEvent.Account, new UserModelUserProfile(currentUser, profileProvider),
|
||||
AttributeFormDataProcessor.toUserProfile(formData),
|
||||
profileProvider);
|
||||
}
|
||||
|
||||
public static DefaultUserProfileContext forRegistrationUserCreation(
|
||||
KeycloakSession session, MultivaluedMap<String, String> formData) {
|
||||
UserProfileProvider profileProvider = getProfileProvider(session);
|
||||
return new DefaultUserProfileContext(UserUpdateEvent.RegistrationUserCreation, null,
|
||||
AttributeFormDataProcessor.toUserProfile(formData), profileProvider);
|
||||
}
|
||||
|
||||
public static DefaultUserProfileContext forRegistrationProfile(KeycloakSession session,
|
||||
MultivaluedMap<String, String> formData) {
|
||||
UserProfileProvider profileProvider = getProfileProvider(session);
|
||||
return new DefaultUserProfileContext(UserUpdateEvent.RegistrationProfile, null,
|
||||
AttributeFormDataProcessor.toUserProfile(formData), profileProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param currentUser if this is null, then we're creating new user. If it is not null, we're updating existing user
|
||||
* @param rep
|
||||
* @return user profile context for the validation of user when called from admin REST API
|
||||
*/
|
||||
public static DefaultUserProfileContext forUserResource(UserModel currentUser,
|
||||
org.keycloak.representations.idm.UserRepresentation rep, KeycloakSession session) {
|
||||
UserProfileProvider profileProvider = getProfileProvider(session);
|
||||
UserProfile currentUserProfile = currentUser == null ? null : new UserModelUserProfile(currentUser, profileProvider);
|
||||
return new DefaultUserProfileContext(UserUpdateEvent.UserResource, currentUserProfile,
|
||||
new UserRepresentationUserProfile(rep, profileProvider), profileProvider);
|
||||
}
|
||||
|
||||
public static DefaultUserProfileContext forProfile(UserUpdateEvent event) {
|
||||
return new DefaultUserProfileContext(event, null, null, null);
|
||||
}
|
||||
|
||||
private static UserProfileProvider getProfileProvider(KeycloakSession session) {
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
return session.getProvider(UserProfileProvider.class, LegacyUserProfileProviderFactory.PROVIDER_ID);
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 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.profile.representations;
|
||||
|
||||
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.account.UserRepresentation;
|
||||
import org.keycloak.userprofile.UserProfileAttributes;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public class AccountUserRepresentationUserProfile extends AttributeUserProfile {
|
||||
|
||||
public AccountUserRepresentationUserProfile(UserRepresentation user, UserProfileProvider profileProvider) {
|
||||
super(flattenUserRepresentation(user), profileProvider);
|
||||
}
|
||||
|
||||
private static UserProfileAttributes flattenUserRepresentation(UserRepresentation user) {
|
||||
Map<String, List<String>> attrs = new HashMap<>();
|
||||
|
||||
if (user.getAttributes() != null) attrs.putAll(user.getAttributes());
|
||||
|
||||
if (user.getUsername() != null)
|
||||
attrs.put(UserModel.USERNAME, Collections.singletonList(user.getUsername()));
|
||||
else
|
||||
attrs.remove(UserModel.USERNAME);
|
||||
|
||||
if (user.getEmail() != null)
|
||||
attrs.put(UserModel.EMAIL, Collections.singletonList(user.getEmail()));
|
||||
else
|
||||
attrs.remove(UserModel.EMAIL);
|
||||
|
||||
if (user.getLastName() != null)
|
||||
attrs.put(UserModel.LAST_NAME, Collections.singletonList(user.getLastName()));
|
||||
|
||||
if (user.getFirstName() != null)
|
||||
attrs.put(UserModel.FIRST_NAME, Collections.singletonList(user.getFirstName()));
|
||||
|
||||
|
||||
return new UserProfileAttributes(attrs);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 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.profile.representations;
|
||||
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
import org.keycloak.userprofile.profile.AbstractUserProfile;
|
||||
|
||||
import javax.ws.rs.NotSupportedException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public class AttributeUserProfile extends AbstractUserProfile {
|
||||
|
||||
public AttributeUserProfile(Map<String, List<String>> attributes, UserProfileProvider profileProvider) {
|
||||
super(attributes, profileProvider);
|
||||
}
|
||||
|
||||
public AttributeUserProfile(Map<String, List<String>> attributes) {
|
||||
super(attributes, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
throw new NotSupportedException("No ID support");
|
||||
}
|
||||
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 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.profile.representations;
|
||||
|
||||
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
import org.keycloak.userprofile.profile.AbstractUserProfile;
|
||||
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public class IdpUserProfile extends AbstractUserProfile {
|
||||
|
||||
private final SerializedBrokeredIdentityContext user;
|
||||
|
||||
public IdpUserProfile(SerializedBrokeredIdentityContext user, UserProfileProvider profileProvider) {
|
||||
super(user.getAttributes(), profileProvider);
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return user.getId();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 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.profile.representations;
|
||||
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
import org.keycloak.userprofile.profile.AbstractUserProfile;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public class UserModelUserProfile extends AbstractUserProfile {
|
||||
|
||||
|
||||
public UserModelUserProfile(UserModel user, UserProfileProvider profileProvider) {
|
||||
super(user.getAttributes(), profileProvider);
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
private final UserModel user;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return user.getId();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 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.profile.representations;
|
||||
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.userprofile.UserProfileAttributes;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public class UserRepresentationUserProfile extends AttributeUserProfile {
|
||||
|
||||
|
||||
public UserRepresentationUserProfile(UserRepresentation user, UserProfileProvider profileProvider) {
|
||||
super(flattenUserRepresentation(user), profileProvider);
|
||||
}
|
||||
|
||||
public UserRepresentationUserProfile(UserRepresentation user) {
|
||||
super(flattenUserRepresentation(user), null);
|
||||
}
|
||||
|
||||
private static UserProfileAttributes flattenUserRepresentation(UserRepresentation user) {
|
||||
Map<String, List<String>> attrs = new HashMap<>();
|
||||
|
||||
if (user.getAttributes() != null) attrs.putAll(user.getAttributes());
|
||||
|
||||
if (user.getUsername() != null)
|
||||
attrs.put(UserModel.USERNAME, Collections.singletonList(user.getUsername()));
|
||||
else
|
||||
attrs.remove(UserModel.USERNAME);
|
||||
|
||||
if (user.getEmail() != null)
|
||||
attrs.put(UserModel.EMAIL, Collections.singletonList(user.getEmail()));
|
||||
else
|
||||
attrs.remove(UserModel.EMAIL);
|
||||
|
||||
if (user.getUsername() != null)
|
||||
attrs.put(UserModel.USERNAME, Collections.singletonList(user.getUsername()));
|
||||
|
||||
if (user.getLastName() != null)
|
||||
attrs.put(UserModel.LAST_NAME, Collections.singletonList(user.getLastName()));
|
||||
|
||||
if (user.getFirstName() != null)
|
||||
attrs.put(UserModel.FIRST_NAME, Collections.singletonList(user.getFirstName()));
|
||||
|
||||
if (user.getEmail() != null)
|
||||
attrs.put(UserModel.EMAIL, Collections.singletonList(user.getEmail()));
|
||||
|
||||
return new UserProfileAttributes(attrs);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,168 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 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.utils;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.userprofile.LegacyUserProfileProviderFactory;
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.UserProfileAttributes;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
import org.keycloak.userprofile.profile.representations.UserRepresentationUserProfile;
|
||||
import org.keycloak.userprofile.validation.UserUpdateEvent;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public class UserUpdateHelper {
|
||||
|
||||
|
||||
public static void updateRegistrationProfile(RealmModel realm, UserModel currentUser, UserProfile updatedUser) {
|
||||
register(UserUpdateEvent.RegistrationProfile, realm, currentUser, updatedUser);
|
||||
}
|
||||
|
||||
public static void updateRegistrationUserCreation(RealmModel realm, UserModel currentUser, UserProfile updatedUser) {
|
||||
register(UserUpdateEvent.RegistrationUserCreation, realm, currentUser, updatedUser);
|
||||
}
|
||||
|
||||
public static void updateIdpReview(RealmModel realm, UserModel userModelDelegate, UserProfile updatedProfile) {
|
||||
update(UserUpdateEvent.IdpReview, realm, userModelDelegate, updatedProfile.getAttributes(), false);
|
||||
}
|
||||
|
||||
public static void updateUserProfile(RealmModel realm, UserModel user, UserProfile updatedProfile) {
|
||||
update(UserUpdateEvent.UpdateProfile, realm, user, updatedProfile.getAttributes(), false);
|
||||
}
|
||||
|
||||
public static void updateAccount(RealmModel realm, UserModel user, UserProfile updatedProfile) {
|
||||
update(UserUpdateEvent.Account, realm, user, updatedProfile);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>This method should be used when account is updated through the old console where the behavior is different
|
||||
* than when using the new Account REST API and console in regards to how user attributes are managed.
|
||||
*
|
||||
* @deprecated Remove this method as soon as the old console is no longer part of the distribution
|
||||
* @param realm
|
||||
* @param user
|
||||
* @param updatedProfile
|
||||
*/
|
||||
@Deprecated
|
||||
public static void updateAccountOldConsole(RealmModel realm, UserModel user, UserProfile updatedProfile) {
|
||||
update(UserUpdateEvent.Account, realm, user, updatedProfile.getAttributes(), false);
|
||||
}
|
||||
|
||||
public static void updateUserResource(KeycloakSession session, UserModel user, UserRepresentation rep, boolean removeExistingAttributes) {
|
||||
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class, LegacyUserProfileProviderFactory.PROVIDER_ID);
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
UserRepresentationUserProfile userProfile = new UserRepresentationUserProfile(rep, profileProvider);
|
||||
update(UserUpdateEvent.UserResource, realm, user, userProfile.getAttributes(), removeExistingAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* will update the user model with the profile values, all missing attributes in the new profile will be removed on the user model
|
||||
* @param userUpdateEvent
|
||||
* @param realm
|
||||
* @param currentUser
|
||||
* @param updatedUser
|
||||
*/
|
||||
private static void update(UserUpdateEvent userUpdateEvent, RealmModel realm, UserModel currentUser, UserProfile updatedUser) {
|
||||
update(userUpdateEvent, realm, currentUser, updatedUser.getAttributes(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* will update the user model with the profile values, attributes which are missing will be ignored
|
||||
* @param userUpdateEvent
|
||||
* @param realm
|
||||
* @param currentUser
|
||||
* @param updatedUser
|
||||
*/
|
||||
private static void register(UserUpdateEvent userUpdateEvent, RealmModel realm, UserModel currentUser, UserProfile updatedUser) {
|
||||
update(userUpdateEvent, realm, currentUser, updatedUser.getAttributes(), false);
|
||||
}
|
||||
|
||||
private static void update(UserUpdateEvent userUpdateEvent, RealmModel realm, UserModel currentUser, UserProfileAttributes updatedUser, boolean removeMissingAttributes) {
|
||||
|
||||
if (updatedUser == null || updatedUser.size() == 0)
|
||||
return;
|
||||
|
||||
filterAttributes(userUpdateEvent, realm, updatedUser);
|
||||
|
||||
updateAttributes(currentUser, updatedUser, removeMissingAttributes);
|
||||
}
|
||||
|
||||
private static void filterAttributes(UserUpdateEvent userUpdateEvent, RealmModel realm, UserProfileAttributes updatedUser) {
|
||||
//The Idp review does not respect "isEditUserNameAllowed" therefore we have to miss the check here
|
||||
if (!userUpdateEvent.equals(UserUpdateEvent.IdpReview)) {
|
||||
//This step has to be done before email is assigned to the username if isRegistrationEmailAsUsername is set
|
||||
//Otherwise email change will not reflect in username changes.
|
||||
if (updatedUser.getFirstAttribute(UserModel.USERNAME) != null && !realm.isEditUsernameAllowed()) {
|
||||
updatedUser.removeAttribute(UserModel.USERNAME);
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedUser.getFirstAttribute(UserModel.EMAIL) != null && updatedUser.getFirstAttribute(UserModel.EMAIL).isEmpty()) {
|
||||
updatedUser.removeAttribute(UserModel.EMAIL);
|
||||
updatedUser.setAttribute(UserModel.EMAIL, Collections.singletonList(null));
|
||||
}
|
||||
|
||||
if (updatedUser.getFirstAttribute(UserModel.EMAIL) != null && realm.isRegistrationEmailAsUsername()) {
|
||||
updatedUser.removeAttribute(UserModel.USERNAME);
|
||||
updatedUser.setAttribute(UserModel.USERNAME, Collections.singletonList(updatedUser.getFirstAttribute(UserModel.EMAIL)));
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateAttributes(UserModel currentUser, UserProfileAttributes attributes, boolean removeMissingAttributes) {
|
||||
for (Map.Entry<String, List<String>> attr : attributes.entrySet()) {
|
||||
List<String> currentValue = currentUser.getAttributeStream(attr.getKey()).collect(Collectors.toList());
|
||||
//In case of username we need to provide lower case values
|
||||
List<String> updatedValue = attr.getKey().equals(UserModel.USERNAME) ? AttributeToLower(attr.getValue()) : attr.getValue();
|
||||
if (currentValue.size() != updatedValue.size() || !currentValue.containsAll(updatedValue)) {
|
||||
currentUser.setAttribute(attr.getKey(), updatedValue);
|
||||
}
|
||||
}
|
||||
if (removeMissingAttributes) {
|
||||
Set<String> attrsToRemove = new HashSet<>(currentUser.getAttributes().keySet());
|
||||
attrsToRemove.removeAll(attributes.keySet());
|
||||
|
||||
for (String attr : attrsToRemove) {
|
||||
if (attributes.isReadOnlyAttribute(attr)) {
|
||||
continue;
|
||||
}
|
||||
currentUser.removeAttribute(attr);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> AttributeToLower(List<String> attr) {
|
||||
if (attr.size() == 1 && attr.get(0) != null)
|
||||
return Collections.singletonList(KeycloakModelUtils.toLowerCaseSafe(attr.get(0)));
|
||||
return attr;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile.validation;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public class AttributeValidator {
|
||||
String attributeKey;
|
||||
List<Validator> validators;
|
||||
|
||||
public AttributeValidator(String attributeKey, List<Validator> validators) {
|
||||
this.validators = validators;
|
||||
this.attributeKey = attributeKey;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile.validation;
|
||||
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public class AttributeValidatorBuilder {
|
||||
ValidationChainBuilder validationChainBuilder;
|
||||
String attributeKey;
|
||||
List<Validator> validations = new ArrayList<>();
|
||||
|
||||
public AttributeValidatorBuilder(ValidationChainBuilder validationChainBuilder) {
|
||||
this.validationChainBuilder = validationChainBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is for validating first value of the specified attribute. It is sufficient for all the single-valued attributes
|
||||
*
|
||||
* @param messageKey Key of the error message to be displayed when validation fails
|
||||
* @param validationFunction Function, which does the actual validation logic. The "String" argument is the new value of the particular attribute.
|
||||
* @return this
|
||||
*/
|
||||
public AttributeValidatorBuilder addSingleAttributeValueValidationFunction(String messageKey, BiFunction<String, UserProfileContext, Boolean> validationFunction) {
|
||||
BiFunction<List<String>, UserProfileContext, Boolean> wrappedValidationFunction = (attrValues, context) -> {
|
||||
String singleValue = attrValues == null ? null : attrValues.get(0);
|
||||
return validationFunction.apply(singleValue, context);
|
||||
};
|
||||
this.validations.add(new Validator(messageKey, wrappedValidationFunction));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AttributeValidatorBuilder addValidationFunction(String messageKey, BiFunction<List<String>, UserProfileContext, Boolean> validationFunction) {
|
||||
this.validations.add(new Validator(messageKey, validationFunction));
|
||||
return this;
|
||||
}
|
||||
|
||||
public AttributeValidatorBuilder forAttribute(String attributeKey) {
|
||||
this.attributeKey = attributeKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ValidationChainBuilder build() {
|
||||
this.validationChainBuilder.addValidatorConfig(new AttributeValidator(attributeKey, this.validations));
|
||||
return this.validationChainBuilder;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,125 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile.validation;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.ObjectUtil;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.services.validation.Validation;
|
||||
import org.keycloak.userprofile.LegacyUserProfileProvider;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* Functions are supposed to return:
|
||||
* - true if validation success
|
||||
* - false if validation fails
|
||||
*
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public class StaticValidators {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(StaticValidators.class);
|
||||
|
||||
public static BiFunction<String, UserProfileContext, Boolean> isBlank() {
|
||||
return (value, context) ->
|
||||
value==null || !Validation.isBlank(value);
|
||||
}
|
||||
|
||||
public static BiFunction<String, UserProfileContext, Boolean> isEmailValid() {
|
||||
return (value, context) ->
|
||||
Validation.isBlank(value) || Validation.isEmailValid(value);
|
||||
}
|
||||
|
||||
public static BiFunction<String, UserProfileContext, Boolean> userNameExists(KeycloakSession session) {
|
||||
return (value, context) -> {
|
||||
if (Validation.isBlank(value)) return true;
|
||||
return !(context.getCurrentProfile() != null
|
||||
&& !value.equals(context.getCurrentProfile().getAttributes().getFirstAttribute(UserModel.USERNAME))
|
||||
&& session.users().getUserByUsername(session.getContext().getRealm(), value) != null);
|
||||
};
|
||||
}
|
||||
|
||||
public static BiFunction<String, UserProfileContext, Boolean> isUserMutable(RealmModel realm) {
|
||||
return (value, context) -> {
|
||||
if (Validation.isBlank(value)) return true;
|
||||
return !(!realm.isEditUsernameAllowed()
|
||||
&& context.getCurrentProfile() != null
|
||||
&& !value.equals(context.getCurrentProfile().getAttributes().getFirstAttribute(UserModel.USERNAME))
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
public static BiFunction<String, UserProfileContext, Boolean> checkUsernameExists(boolean externalCondition) {
|
||||
return (value, context) ->
|
||||
!(externalCondition && Validation.isBlank(value));
|
||||
}
|
||||
|
||||
|
||||
public static BiFunction<String, UserProfileContext, Boolean> doesEmailExistAsUsername(KeycloakSession session) {
|
||||
return (value, context) -> {
|
||||
if (Validation.isBlank(value)) return true;
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
if (!realm.isDuplicateEmailsAllowed()) {
|
||||
UserModel userByEmail = session.users().getUserByEmail(realm, value);
|
||||
return !(realm.isRegistrationEmailAsUsername() && userByEmail != null && context.getCurrentProfile() != null && !userByEmail.getId().equals(context.getCurrentProfile().getId()));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
public static BiFunction<String, UserProfileContext, Boolean> isEmailDuplicated(KeycloakSession session) {
|
||||
return (value, context) -> {
|
||||
if (Validation.isBlank(value)) return true;
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
if (!realm.isDuplicateEmailsAllowed()) {
|
||||
UserModel userByEmail = session.users().getUserByEmail(realm, value);
|
||||
// check for duplicated email
|
||||
return !(userByEmail != null && (context.getCurrentProfile() == null || !userByEmail.getId().equals(context.getCurrentProfile().getId())));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
public static BiFunction<String, UserProfileContext, Boolean> doesEmailExist(KeycloakSession session) {
|
||||
return (value, context) ->
|
||||
!(value != null
|
||||
&& !session.getContext().getRealm().isDuplicateEmailsAllowed()
|
||||
&& session.users().getUserByEmail(session.getContext().getRealm(), value) != null);
|
||||
}
|
||||
|
||||
public static BiFunction<List<String>, UserProfileContext, Boolean> isReadOnlyAttributeUnchanged(String attributeName) {
|
||||
return (newAttrValues, context) -> {
|
||||
if (newAttrValues == null) {
|
||||
return true;
|
||||
}
|
||||
List<String> existingAttrValues = context.getCurrentProfile() == null ? null : context.getCurrentProfile().getAttributes().getAttribute(attributeName);
|
||||
boolean result = ObjectUtil.isEqualOrBothNull(newAttrValues, existingAttrValues);
|
||||
|
||||
if (!result) {
|
||||
logger.warnf("Attempt to edit denied attribute '%s' of user '%s'", attributeName, context.getCurrentProfile() == null ? "new user" : context.getCurrentProfile().getAttributes().getFirstAttribute(UserModel.USERNAME));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile.validation;
|
||||
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public class ValidationChain {
|
||||
List<AttributeValidator> attributeValidators;
|
||||
|
||||
public ValidationChain(List<AttributeValidator> attributeValidators) {
|
||||
this.attributeValidators = attributeValidators;
|
||||
}
|
||||
|
||||
public List<AttributeValidationResult> validate(UserProfileContext updateContext, UserProfile updatedProfile) {
|
||||
List<AttributeValidationResult> overallResults = new ArrayList<>();
|
||||
for (AttributeValidator attribute : attributeValidators) {
|
||||
List<ValidationResult> validationResults = new ArrayList<>();
|
||||
|
||||
String attributeKey = attribute.attributeKey;
|
||||
List<String> attributeValues = updatedProfile.getAttributes().getAttribute(attributeKey);
|
||||
|
||||
List<String> existingAttrValues = updateContext.getCurrentProfile() == null ? null : updateContext.getCurrentProfile().getAttributes().getAttribute(attributeKey);
|
||||
boolean attributeChanged = !Objects.equals(attributeValues, existingAttrValues);
|
||||
for (Validator validator : attribute.validators) {
|
||||
validationResults.add(new ValidationResult(validator.function.apply(attributeValues, updateContext), validator.errorType));
|
||||
}
|
||||
|
||||
overallResults.add(new AttributeValidationResult(attributeKey, attributeChanged, validationResults));
|
||||
}
|
||||
|
||||
return overallResults;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile.validation;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public class ValidationChainBuilder {
|
||||
|
||||
Map<String, AttributeValidator> attributeConfigs = new HashMap<>();
|
||||
|
||||
public static ValidationChainBuilder builder() {
|
||||
return new ValidationChainBuilder();
|
||||
}
|
||||
|
||||
public AttributeValidatorBuilder addAttributeValidator() {
|
||||
return new AttributeValidatorBuilder(this);
|
||||
}
|
||||
|
||||
public ValidationChain build() {
|
||||
return new ValidationChain(this.attributeConfigs.values().stream().collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
public void addValidatorConfig(AttributeValidator validator) {
|
||||
if (attributeConfigs.containsKey(validator.attributeKey)) {
|
||||
attributeConfigs.get(validator.attributeKey).validators.addAll(validator.validators);
|
||||
} else {
|
||||
attributeConfigs.put(validator.attributeKey, validator);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile.validation;
|
||||
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||
*/
|
||||
public class Validator {
|
||||
String errorType;
|
||||
BiFunction<List<String>, UserProfileContext, Boolean> function;
|
||||
|
||||
public Validator(String errorType, BiFunction<List<String>, UserProfileContext, Boolean> function) {
|
||||
this.function = function;
|
||||
this.errorType = errorType;
|
||||
}
|
||||
|
||||
}
|
|
@ -15,4 +15,4 @@
|
|||
# limitations under the License.
|
||||
#
|
||||
|
||||
org.keycloak.userprofile.LegacyUserProfileProviderFactory
|
||||
org.keycloak.userprofile.legacy.DefaultUserProfileProvider
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.userprofile.validation;
|
||||
|
||||
import static org.keycloak.userprofile.profile.UserProfileContextFactory.forProfile;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.userprofile.profile.DefaultUserProfileContext;
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.profile.representations.UserRepresentationUserProfile;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ValidationChainTest {
|
||||
|
||||
ValidationChainBuilder builder;
|
||||
ValidationChain testchain;
|
||||
UserProfile user;
|
||||
DefaultUserProfileContext updateContext;
|
||||
UserRepresentation rep = new UserRepresentation();
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
builder = ValidationChainBuilder.builder()
|
||||
.addAttributeValidator().forAttribute("FAKE_FIELD")
|
||||
.addSingleAttributeValueValidationFunction("FAKE_FIELD_ERRORKEY", (value, updateUserProfileContext) -> !value.equals("content")).build()
|
||||
.addAttributeValidator().forAttribute("firstName")
|
||||
.addSingleAttributeValueValidationFunction("FIRST_NAME_FIELD_ERRORKEY", (value, updateUserProfileContext) -> true).build();
|
||||
|
||||
//default user content
|
||||
rep.singleAttribute(UserModel.FIRST_NAME, "firstName");
|
||||
rep.singleAttribute(UserModel.LAST_NAME, "lastName");
|
||||
rep.singleAttribute(UserModel.EMAIL, "email");
|
||||
rep.singleAttribute("FAKE_FIELD", "content");
|
||||
rep.singleAttribute("NULLABLE_FIELD", null);
|
||||
|
||||
updateContext = forProfile(UserUpdateEvent.RegistrationProfile);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validate() {
|
||||
testchain = builder.build();
|
||||
UserProfileValidationResult results = new UserProfileValidationResult(testchain.validate(updateContext, new UserRepresentationUserProfile(rep)), null);
|
||||
Assert.assertEquals(true, results.hasFailureOfErrorType("FAKE_FIELD_ERRORKEY"));
|
||||
Assert.assertEquals(false, results.hasFailureOfErrorType("FIRST_NAME_FIELD_ERRORKEY"));
|
||||
Assert.assertEquals(true, results.getValidationResults().stream().filter(o -> o.getField().equals("firstName")).collect(Collectors.toList()).get(0).isValid());
|
||||
Assert.assertEquals(2, results.getValidationResults().size());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mergedConfig() {
|
||||
testchain = builder.addAttributeValidator().forAttribute("FAKE_FIELD")
|
||||
.addSingleAttributeValueValidationFunction("FAKE_FIELD_ERRORKEY_1", (value, updateUserProfileContext) -> false).build()
|
||||
.addAttributeValidator().forAttribute("FAKE_FIELD")
|
||||
.addSingleAttributeValueValidationFunction("FAKE_FIELD_ERRORKEY_2", (value, updateUserProfileContext) -> false).build().build();
|
||||
|
||||
UserProfileValidationResult results = new UserProfileValidationResult(testchain.validate(updateContext, new UserRepresentationUserProfile(rep)), null);
|
||||
Assert.assertEquals(true, results.hasFailureOfErrorType("FAKE_FIELD_ERRORKEY_1"));
|
||||
Assert.assertEquals(true, results.hasFailureOfErrorType("FAKE_FIELD_ERRORKEY_2"));
|
||||
Assert.assertEquals(true, results.getValidationResults().stream().filter(o -> o.getField().equals("firstName")).collect(Collectors.toList()).get(0).isValid());
|
||||
Assert.assertEquals(true, results.hasAttributeChanged("firstName"));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyChain() {
|
||||
UserProfileValidationResult results = new UserProfileValidationResult(ValidationChainBuilder.builder().build().validate(updateContext,new UserRepresentationUserProfile(rep) ), null);
|
||||
Assert.assertEquals(Collections.emptyList(), results.getValidationResults());
|
||||
}
|
||||
}
|
|
@ -116,12 +116,12 @@ public class UserMapStorage implements UserLookupProvider.Streams, UserStoragePr
|
|||
user = new AbstractUserAdapterFederatedStorage.Streams(session, realm, model) {
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return username;
|
||||
return username.toLowerCase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUsername(String innerUsername) {
|
||||
if (! Objects.equals(innerUsername, username)) {
|
||||
if (! Objects.equals(innerUsername, username.toLowerCase())) {
|
||||
throw new RuntimeException("Unsupported");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
*
|
||||
* * 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.config;
|
||||
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class DeclarativeUserProfileModel extends ComponentModel {
|
||||
|
||||
public DeclarativeUserProfileModel() {
|
||||
setProviderId(DeclarativeUserProfileProvider.ID);
|
||||
setProviderType(UserProfileProvider.class.getName());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,409 @@
|
|||
/*
|
||||
*
|
||||
* * 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.config;
|
||||
|
||||
import static org.keycloak.common.util.ObjectUtil.isBlank;
|
||||
import static org.keycloak.testsuite.user.profile.config.UPConfigUtils.readConfig;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
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.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.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.ProviderConfigProperty;
|
||||
import org.keycloak.userprofile.AttributeContext;
|
||||
import org.keycloak.userprofile.AttributeMetadata;
|
||||
import org.keycloak.userprofile.AttributeValidatorMetadata;
|
||||
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.legacy.Validators;
|
||||
|
||||
/**
|
||||
* {@link UserProfileProvider} loading configuration from the changeable JSON
|
||||
* file stored in component config. Parsed configuration is cached.
|
||||
*
|
||||
* @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 static final String ID = "declarative-userprofile-provider";
|
||||
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 String defaultRawConfig;
|
||||
|
||||
public DeclarativeUserProfileProvider() {
|
||||
// for reflection
|
||||
}
|
||||
|
||||
public DeclarativeUserProfileProvider(KeycloakSession session,
|
||||
Map<UserProfileContext, UserProfileMetadata> metadataRegistry) {
|
||||
super(session, metadataRegistry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DeclarativeUserProfileProvider create(KeycloakSession session,
|
||||
Map<UserProfileContext, UserProfileMetadata> metadataRegistry) {
|
||||
return new DeclarativeUserProfileProvider(session, metadataRegistry);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UserProfileMetadata configureUserProfile(UserProfileMetadata metadata, KeycloakSession session) {
|
||||
ComponentModel model = getComponentModelOrCreate(session);
|
||||
Map<UserProfileContext, UserProfileMetadata> metadataMap = model.getNote(PARSED_CONFIG_COMPONENT_KEY);
|
||||
|
||||
// not cached, create a note with cache
|
||||
if (metadataMap == null) {
|
||||
metadataMap = new HashMap<>();
|
||||
model.setNote(PARSED_CONFIG_COMPONENT_KEY, metadataMap);
|
||||
}
|
||||
|
||||
return metadataMap.computeIfAbsent(metadata.getContext(),
|
||||
(context) -> decorateUserProfileForCache(metadata, model));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model)
|
||||
throws ComponentValidationException {
|
||||
String upConfigJson = getConfigJsonFromComponentModel(model);
|
||||
|
||||
if (!isBlank(upConfigJson)) {
|
||||
try {
|
||||
UPConfig upc = readConfig(new ByteArrayInputStream(upConfigJson.getBytes("UTF-8")));
|
||||
List<String> errors = UPConfigUtils.validate(upc);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
throw new ComponentValidationException(
|
||||
"UserProfile configuration is invalid: " + errors.toString());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ComponentValidationException(
|
||||
"UserProfile configuration is invalid due to JSON parsing error: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// delete cache so new config is parsed and applied next time it is required
|
||||
// throught #configureUserProfile(metadata, session)
|
||||
if (model != null) {
|
||||
model.removeNote(PARSED_CONFIG_COMPONENT_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getConfiguration() {
|
||||
String cfg = getConfigJsonFromComponentModel(getComponentModel());
|
||||
|
||||
if (isBlank(cfg)) {
|
||||
return defaultRawConfig;
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setConfiguration(String configuration) {
|
||||
ComponentModel component = getComponentModel();
|
||||
|
||||
removeConfigJsonFromComponentModel(component);
|
||||
|
||||
if (!isBlank(configuration)) {
|
||||
// store new parts
|
||||
List<String> parts = UPConfigUtils.getChunks(configuration, 3800);
|
||||
MultivaluedHashMap<String, String> config = component.getConfig();
|
||||
|
||||
config.putSingle(UP_PIECES_COUNT_COMPONENT_CONFIG_KEY, "" + parts.size());
|
||||
|
||||
int i = 0;
|
||||
|
||||
for (String part : parts) {
|
||||
config.putSingle(UP_PIECE_COMPONENT_CONFIG_KEY_BASE + (i++), part);
|
||||
}
|
||||
}
|
||||
|
||||
session.getContext().getRealm().updateComponent(component);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
//TODO: We should avoid blocking operations during startup. Need to review this.
|
||||
try (InputStream is = getClass().getResourceAsStream(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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public ComponentModel getComponentModel() {
|
||||
return getComponentModelOrCreate(session);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorate basic metadata provided from {@link AbstractUserProfileProvider}
|
||||
* based on 'per realm' configuration. This method is called for each
|
||||
* {@link UserProfileContext} in each realm, and metadata are cached then and
|
||||
* this method is called again only if configuration changes.
|
||||
*
|
||||
* @param metadata base to be decorated based on configuration loaded from
|
||||
* component model
|
||||
* @param model component model to get "per realm" configuration from
|
||||
* @return decorated metadata
|
||||
*/
|
||||
private UserProfileMetadata decorateUserProfileForCache(UserProfileMetadata metadata, ComponentModel model) {
|
||||
UserProfileContext context = metadata.getContext();
|
||||
UPConfig parsedConfig = getParsedConfig(model);
|
||||
|
||||
if (parsedConfig == null) {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
// need to clone otherwise changes to profile config are going to be reflected
|
||||
// in the default config
|
||||
UserProfileMetadata decoratedMetadata = metadata.clone();
|
||||
|
||||
for (UPAttribute attrConfig : parsedConfig.getAttributes()) {
|
||||
String attributeName = attrConfig.getName();
|
||||
List<AttributeValidatorMetadata> validators = new ArrayList<>();
|
||||
Map<String, Map<String, Object>> validationsConfig = attrConfig.getValidations();
|
||||
|
||||
if (validationsConfig != null) {
|
||||
for (Map.Entry<String, Map<String, Object>> vc : validationsConfig.entrySet()) {
|
||||
validators.add(createConfiguredValidator(attrConfig, vc.getKey(), vc.getValue()));
|
||||
}
|
||||
}
|
||||
|
||||
UPAttributeRequired rc = attrConfig.getRequired();
|
||||
Predicate<AttributeContext> required = AttributeMetadata.ALWAYS_FALSE;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
Predicate<AttributeContext> readOnly = 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);
|
||||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
return decoratedMetadata;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parsed config file configured in model. Default one used if not
|
||||
* configured.
|
||||
*
|
||||
* @param model to take config from
|
||||
* @return parsed configuration
|
||||
*/
|
||||
private UPConfig getParsedConfig(ComponentModel model) {
|
||||
String rawConfig = getConfigJsonFromComponentModel(model);
|
||||
|
||||
if (!isBlank(rawConfig)) {
|
||||
try {
|
||||
return readConfig(new ByteArrayInputStream(rawConfig.getBytes("UTF-8")));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("UserProfile config for realm " + session.getContext().getRealm().getName()
|
||||
+ " is invalid:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
* @param session to be used, and take realm from
|
||||
* @return componenet
|
||||
*/
|
||||
private ComponentModel getComponentModelOrCreate(KeycloakSession session) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
return realm.getComponentsStream(realm.getId(), UserProfileProvider.class.getName()).findAny()
|
||||
.orElseGet(() -> realm.addComponentModel(new DeclarativeUserProfileModel()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create validator for 'required' validation.
|
||||
*
|
||||
* @return validator
|
||||
*/
|
||||
private AttributeValidatorMetadata createRequiredValidator(UPAttribute attrConfig) {
|
||||
String msg = "missing" + UPConfigUtils.capitalizeFirstLetter(attrConfig.getName()) + "Message";
|
||||
return Validators.create(msg, Validators.requiredByAttributeMetadata());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create validator for validation configured in the user profile config.
|
||||
*
|
||||
* @param attrConfig to create validator for
|
||||
* @return validator
|
||||
*/
|
||||
private AttributeValidatorMetadata createConfiguredValidator(UPAttribute attrConfig,
|
||||
String validator, Map<String, Object> validatorConfig) {
|
||||
// TODO UserProfile - integrate Validation SPI
|
||||
if ("length".equals(validator))
|
||||
return Validators.create("badLenght" + UPConfigUtils.capitalizeFirstLetter(attrConfig.getName()) + "Message",
|
||||
Validators.length(validatorConfig));
|
||||
else if ("emailFormat".equals(validator))
|
||||
return Validators.create("invalidEmailMessage", Validators.isEmailValid());
|
||||
else
|
||||
throw new RuntimeException("Unsupported UserProfile validator " + validator);
|
||||
}
|
||||
|
||||
private String getConfigJsonFromComponentModel(ComponentModel model) {
|
||||
if (model == null)
|
||||
return null;
|
||||
|
||||
int count = model.get(UP_PIECES_COUNT_COMPONENT_CONFIG_KEY, 0);
|
||||
if (count < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < count; i++) {
|
||||
String v = model.get(UP_PIECE_COMPONENT_CONFIG_KEY_BASE + i);
|
||||
if (v != null)
|
||||
sb.append(v);
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private void removeConfigJsonFromComponentModel(ComponentModel model) {
|
||||
if (model == null)
|
||||
return;
|
||||
|
||||
int count = model.get(UP_PIECES_COUNT_COMPONENT_CONFIG_KEY, 0);
|
||||
if (count < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
model.getConfig().remove(UP_PIECE_COMPONENT_CONFIG_KEY_BASE + i);
|
||||
}
|
||||
model.getConfig().remove(UP_PIECES_COUNT_COMPONENT_CONFIG_KEY);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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.config;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Configuration of the Attribute.
|
||||
*
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*
|
||||
*/
|
||||
public class UPAttribute {
|
||||
|
||||
private String name;
|
||||
/** key in the Map is name of the validator, value is its configuration */
|
||||
private Map<String, Map<String, Object>> validations;
|
||||
private Map<String, Object> annotations;
|
||||
/** null means it is not required */
|
||||
private UPAttributeRequired required;
|
||||
/** null means everyone can view and edit the attribute */
|
||||
private UPAttributePermissions permissions;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name != null ? name.trim() : null;
|
||||
}
|
||||
|
||||
public Map<String, Map<String, Object>> getValidations() {
|
||||
return validations;
|
||||
}
|
||||
|
||||
public void setValidations(Map<String, Map<String, Object>> validations) {
|
||||
this.validations = validations;
|
||||
}
|
||||
|
||||
public Map<String, Object> getAnnotations() {
|
||||
return annotations;
|
||||
}
|
||||
|
||||
public void setAnnotations(Map<String, Object> annotations) {
|
||||
this.annotations = annotations;
|
||||
}
|
||||
|
||||
public UPAttributeRequired getRequired() {
|
||||
return required;
|
||||
}
|
||||
|
||||
public void setRequired(UPAttributeRequired required) {
|
||||
this.required = required;
|
||||
}
|
||||
|
||||
public UPAttributePermissions getPermissions() {
|
||||
return permissions;
|
||||
}
|
||||
|
||||
public void setPermissions(UPAttributePermissions permissions) {
|
||||
this.permissions = permissions;
|
||||
}
|
||||
|
||||
public void addValidation(String validator, Map<String, Object> config) {
|
||||
if (validations == null) {
|
||||
validations = new HashMap<>();
|
||||
}
|
||||
validations.put(validator, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UPAttribute [name=" + name + ", permissions=" + permissions + ", required=" + required + ", validations=" + validations + ", annotations="
|
||||
+ annotations + "]";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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.config;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Configuration of permissions for the attribute
|
||||
*
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*
|
||||
*/
|
||||
public class UPAttributePermissions {
|
||||
|
||||
private List<String> view;
|
||||
private List<String> edit;
|
||||
|
||||
public List<String> getView() {
|
||||
return view;
|
||||
}
|
||||
|
||||
public void setView(List<String> view) {
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
public List<String> getEdit() {
|
||||
return edit;
|
||||
}
|
||||
|
||||
public void setEdit(List<String> edit) {
|
||||
this.edit = edit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UPAttributePermissions [view=" + view + ", edit=" + edit + "]";
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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.config;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
/**
|
||||
* Config of the rules when attribute is required.
|
||||
*
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*
|
||||
*/
|
||||
public class UPAttributeRequired {
|
||||
|
||||
private List<String> roles;
|
||||
private List<String> scopes;
|
||||
|
||||
/**
|
||||
* Check if this config means that the attribute is ALWAYS required.
|
||||
*
|
||||
* @return true if the attribute is always required
|
||||
*/
|
||||
@JsonIgnore
|
||||
public boolean isAlways() {
|
||||
return (roles == null || roles.isEmpty()) && (scopes == null || scopes.isEmpty());
|
||||
}
|
||||
|
||||
public List<String> getRoles() {
|
||||
return roles;
|
||||
}
|
||||
|
||||
public void setRoles(List<String> roles) {
|
||||
this.roles = roles;
|
||||
}
|
||||
|
||||
public List<String> getScopes() {
|
||||
return scopes;
|
||||
}
|
||||
|
||||
public void setScopes(List<String> scopes) {
|
||||
this.scopes = scopes;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UPAttributeRequired [isAlways=" + isAlways() + ", roles=" + roles + ", scopes=" + scopes + "]";
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Configuration of the User Profile for one realm.
|
||||
*
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*
|
||||
*/
|
||||
public class UPConfig {
|
||||
|
||||
private List<UPAttribute> attributes;
|
||||
|
||||
public List<UPAttribute> getAttributes() {
|
||||
return attributes;
|
||||
}
|
||||
|
||||
public void setAttributes(List<UPAttribute> attributes) {
|
||||
this.attributes = attributes;
|
||||
}
|
||||
|
||||
public UPConfig addAttribute(UPAttribute attribute) {
|
||||
if (attributes == null) {
|
||||
attributes = new ArrayList<>();
|
||||
}
|
||||
|
||||
attributes.add(attribute);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UPConfig [attributes=" + attributes + "]";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,223 @@
|
|||
/*
|
||||
* 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.config;
|
||||
|
||||
import static org.keycloak.common.util.ObjectUtil.isBlank;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
/**
|
||||
* Utility methods to work with User Profile Configurations
|
||||
*
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*
|
||||
*/
|
||||
public class UPConfigUtils {
|
||||
|
||||
public static final String ROLE_USER = "user";
|
||||
public static final String ROLE_ADMIN = "admin";
|
||||
|
||||
private static final Set<String> PSEUDOROLES = new HashSet<>();
|
||||
|
||||
static {
|
||||
PSEUDOROLES.add(ROLE_ADMIN);
|
||||
PSEUDOROLES.add(ROLE_USER);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load configuration from JSON file.
|
||||
* <p>
|
||||
* Configuration is not validated, use {@link #validate(UPConfig)} to validate it and get list of errors.
|
||||
*
|
||||
* @param is JSON file to be loaded
|
||||
* @return object representation of the configuration
|
||||
* @throws IOException if JSON configuration can't be loaded (eg due to JSON format errors etc)
|
||||
*/
|
||||
public static UPConfig readConfig(InputStream is) throws IOException {
|
||||
return JsonSerialization.readValue(is, UPConfig.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate object representation of the configuration. Validations:
|
||||
* <ul>
|
||||
* <li>defaultProfile is defined and exists in profiles
|
||||
* <li>parent exists for type
|
||||
* <li>type exists for attribute
|
||||
* <li>validator (from Validator SPI) exists for validation and it's config is correct
|
||||
* </ul>
|
||||
*
|
||||
* @param config to validate
|
||||
* @return list of errors, empty if no error found
|
||||
*/
|
||||
public static List<String> validate(UPConfig config) {
|
||||
List<String> errors = new ArrayList<>();
|
||||
|
||||
if (config.getAttributes() != null) {
|
||||
Set<String> attNamesCache = new HashSet<>();
|
||||
config.getAttributes().forEach((attribute) -> validate(attribute, errors, attNamesCache));
|
||||
} else {
|
||||
errors.add("UserProfile configuration without 'attributes' section is not allowed");
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate attribute configuration
|
||||
*
|
||||
* @param attributeConfig config to be validated
|
||||
* @param errors to add error message in if something is invalid
|
||||
*/
|
||||
private static void validate(UPAttribute attributeConfig, List<String> errors, Set<String> attNamesCache) {
|
||||
String attributeName = attributeConfig.getName();
|
||||
if (isBlank(attributeName)) {
|
||||
errors.add("Attribute configuration without 'name' is not allowed");
|
||||
} else {
|
||||
if (attNamesCache.contains(attributeName)) {
|
||||
errors.add("Duplicit attribute configuration with 'name':'" + attributeName + "'");
|
||||
} else {
|
||||
attNamesCache.add(attributeName);
|
||||
if(!isValidAttributeName(attributeName)) {
|
||||
errors.add("Invalid attribute name (only letters, numbers and '.' '_' '-' special characters allowed): " + attributeName + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (attributeConfig.getValidations() != null) {
|
||||
attributeConfig.getValidations().forEach((validator, validatorConfig) -> validateValidationConfig(validator, validatorConfig, attributeName, errors));
|
||||
}
|
||||
if (attributeConfig.getPermissions() != null) {
|
||||
if (attributeConfig.getPermissions().getView() != null) {
|
||||
validateRoles(attributeConfig.getPermissions().getView(), "permissions.view", errors, attributeName);
|
||||
}
|
||||
if (attributeConfig.getPermissions().getEdit() != null) {
|
||||
validateRoles(attributeConfig.getPermissions().getEdit(), "permissions.edit", errors, attributeName);
|
||||
}
|
||||
}
|
||||
if (attributeConfig.getRequired() != null) {
|
||||
validateRoles(attributeConfig.getRequired().getRoles(), "required.roles", errors, attributeName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param attributeName to validate
|
||||
* @return
|
||||
*/
|
||||
static boolean isValidAttributeName(String attributeName) {
|
||||
return Pattern.matches("[a-zA-Z0-9\\._\\-]+", attributeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate list of configured roles - must contain only supported {@link #PSEUDOROLES} for now.
|
||||
*
|
||||
* @param roles to validate
|
||||
* @param fieldName we are validating for use in error messages
|
||||
* @param errors to ass error message into
|
||||
* @param attributeName we are validating for use in erorr messages
|
||||
*/
|
||||
private static void validateRoles(List<String> roles, String fieldName, List<String> errors, String attributeName) {
|
||||
if (roles != null) {
|
||||
for (String role : roles) {
|
||||
if (!PSEUDOROLES.contains(role)) {
|
||||
errors.add("'" + fieldName + "' configuration for attribute '" + attributeName + "' contains unsupported role '" + role + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that validation configuration is correct
|
||||
*
|
||||
* @param validatorConfig config to be checked
|
||||
* @param errors to add error message in if something is invalid
|
||||
*/
|
||||
private static void validateValidationConfig(String validator, Map<String, Object> validatorConfig, String attributeName, List<String> errors) {
|
||||
|
||||
if (isBlank(validator)) {
|
||||
errors.add("Validation without 'validator' is defined for attribute '" + attributeName + "'");
|
||||
} else {
|
||||
// TODO UserProfile - Validation SPI integration - check that the validator exists using Validation SPI
|
||||
// TODO UserProfile - Validation SPI integration - check that the validation configuration is correct for given validator using Validation SPI
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Break string to substrings of given length
|
||||
*
|
||||
* @param src to break
|
||||
* @param partLength
|
||||
* @return list of string parts, never null (but can be empty if src is null)
|
||||
*/
|
||||
public static List<String> getChunks(String src, int partLength) {
|
||||
List<String> ret = new ArrayList<>();
|
||||
if (src != null) {
|
||||
int pieces = (src.length() / partLength) + 1;
|
||||
for (int i = 0; i < pieces; i++) {
|
||||
if ((i + 1) < pieces)
|
||||
ret.add(src.substring(i * partLength, (i + 1) * partLength));
|
||||
else if (i == 0 || (i * partLength) < src.length())
|
||||
ret.add(src.substring(i * partLength));
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if context CAN BE part of the AuthenticationFlow.
|
||||
*
|
||||
* @param context to check
|
||||
* @return true if context CAN BE part of the auth flow
|
||||
*/
|
||||
public static boolean canBeAuthFlowContext(UserProfileContext context) {
|
||||
return context != UserProfileContext.USER_API && context != UserProfileContext.ACCOUNT
|
||||
&& context != UserProfileContext.ACCOUNT_OLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if roles configuration contains role given current context.
|
||||
*
|
||||
* @param context to be checked
|
||||
* @param roles to be inspected
|
||||
* @return true if roles list contains role representing checked context
|
||||
*/
|
||||
public static boolean isRoleForContext(UserProfileContext context, List<String> roles) {
|
||||
if (roles == null)
|
||||
return false;
|
||||
if (context == UserProfileContext.USER_API)
|
||||
return roles.contains(ROLE_ADMIN);
|
||||
else
|
||||
return roles.contains(ROLE_USER);
|
||||
}
|
||||
|
||||
public static String capitalizeFirstLetter(String str) {
|
||||
if (str == null || str.isEmpty())
|
||||
return str;
|
||||
return str.substring(0, 1).toUpperCase() + str.substring(1);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
#
|
||||
# /*
|
||||
# * 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
|
|
@ -21,6 +21,10 @@
|
|||
</resources>
|
||||
<dependencies>
|
||||
<module name="com.fasterxml.jackson.core.jackson-core"/>
|
||||
<module name="com.fasterxml.jackson.core.jackson-annotations"/>
|
||||
<module name="com.fasterxml.jackson.core.jackson-databind"/>
|
||||
<module name="com.fasterxml.jackson.datatype.jackson-datatype-jdk8"/>
|
||||
<module name="com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider"/>
|
||||
<module name="javax.api"/>
|
||||
<module name="javax.mail.api"/>
|
||||
<module name="javax.ws.rs.api"/>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"name": "username"
|
||||
},
|
||||
{
|
||||
"name": "email"
|
||||
},
|
||||
{
|
||||
"name": "firstName",
|
||||
"required": {}
|
||||
},
|
||||
{
|
||||
"name": "lastName",
|
||||
"required": {}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -120,8 +120,8 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
|||
user = updateAndGet(user);
|
||||
|
||||
assertEquals(user.getLastName(), "Bob");
|
||||
assertEquals(user.getFirstName(), originalFirstName);
|
||||
assertEquals(user.getEmail(), originalEmail);
|
||||
assertNull(user.getFirstName());
|
||||
assertNull(user.getEmail());
|
||||
|
||||
} finally {
|
||||
RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
|
||||
|
|
|
@ -93,7 +93,7 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest {
|
|||
assertEquals("", registerPage.getPassword());
|
||||
assertEquals("", registerPage.getPasswordConfirm());
|
||||
|
||||
events.expectRegister("roleRichUser", "registerExistingUser@email")
|
||||
events.expectRegister("rolerichuser", "registerExistingUser@email")
|
||||
.removeDetail(Details.EMAIL)
|
||||
.user((String) null).error("username_in_use").assertEvent();
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest {
|
|||
assertEquals("", registerPage.getPassword());
|
||||
assertEquals("", registerPage.getPasswordConfirm());
|
||||
|
||||
events.expectRegister("registerExistingUser", "registerExistingUser@email")
|
||||
events.expectRegister("registerexistinguser", "registerExistingUser@email")
|
||||
.removeDetail(Details.EMAIL)
|
||||
.user((String) null).error("email_in_use").assertEvent();
|
||||
}
|
||||
|
@ -281,7 +281,7 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest {
|
|||
registerPage.register("firstName", "lastName", null, "registerUserMissingEmail", "password", "password");
|
||||
registerPage.assertCurrent();
|
||||
assertEquals("Please specify email.", registerPage.getInputAccountErrors().getEmailError());
|
||||
events.expectRegister("registerUserMissingEmail", null)
|
||||
events.expectRegister("registerusermissingemail", null)
|
||||
.removeDetail("email")
|
||||
.error("invalid_registration").assertEvent();
|
||||
}
|
||||
|
@ -296,7 +296,7 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest {
|
|||
registerPage.assertCurrent();
|
||||
assertEquals("registerUserInvalidEmailemail", registerPage.getEmail());
|
||||
assertEquals("Invalid email address.", registerPage.getInputAccountErrors().getEmailError());
|
||||
events.expectRegister("registerUserInvalidEmail", "registerUserInvalidEmailemail")
|
||||
events.expectRegister("registeruserinvalidemail", "registerUserInvalidEmailemail")
|
||||
.error("invalid_registration").assertEvent();
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,241 @@
|
|||
/*
|
||||
*
|
||||
* * 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 java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
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.UserProfileProvider;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public abstract class AbstractUserProfileTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
protected static void configureAuthenticationSession(KeycloakSession session) {
|
||||
configureSessionRealm(session);
|
||||
Set<String> scopes = new HashSet<>();
|
||||
|
||||
scopes.add("customer");
|
||||
|
||||
configureAuthenticationSession(session, "client-a", scopes);
|
||||
}
|
||||
|
||||
protected static void configureAuthenticationSession(KeycloakSession session, String clientId, Set<String> requestedScopes) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
protected static AuthenticationSessionModel createAuthenticationSession(ClientModel client, Set<String> scopes) {
|
||||
return new AuthenticationSessionModel() {
|
||||
@Override
|
||||
public String getTabId() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RootAuthenticationSessionModel getParentSession() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, ExecutionStatus> getExecutionStatus() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExecutionStatus(String authenticator, ExecutionStatus status) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearExecutionStatus() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserModel getAuthenticatedUser() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthenticatedUser(UserModel user) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getRequiredActions() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addRequiredAction(String action) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeRequiredAction(String action) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addRequiredAction(UserModel.RequiredAction action) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeRequiredAction(UserModel.RequiredAction action) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserSessionNote(String name, String value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getUserSessionNotes() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearUserSessionNotes() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthNote(String name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthNote(String name, String value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAuthNote(String name) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAuthNotes() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientNote(String name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientNote(String name, String value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeClientNote(String name) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getClientNotes() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearClientNotes() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getClientScopes() {
|
||||
return scopes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientScopes(Set<String> clientScopes) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRedirectUri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRedirectUri(String uri) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public RealmModel getRealm() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientModel getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAction() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAction(String action) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProtocol() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProtocol(String method) {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,579 @@
|
|||
/*
|
||||
*
|
||||
* * 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;
|
||||
|
||||
/**
|
||||
* @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("length", 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("badLenghtUsernameMessage"));
|
||||
}
|
||||
|
||||
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("max", 4);
|
||||
attribute.addValidation("length", validatorConfig);
|
||||
config.addAttribute(attribute);
|
||||
|
||||
attribute = new UPAttribute();
|
||||
attribute.setName(UserModel.LAST_NAME);
|
||||
attribute.addValidation("length", 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() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testCustomAttribute);
|
||||
}
|
||||
|
||||
private static void testCustomAttribute(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("min", 4);
|
||||
|
||||
attribute.addValidation("length", 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 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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,416 @@
|
|||
/*
|
||||
*
|
||||
* * 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.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
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 java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
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.models.ClientModel;
|
||||
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.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
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.testsuite.util.ClientScopeBuilder;
|
||||
import org.keycloak.testsuite.util.KeycloakModelUtils;
|
||||
import org.keycloak.userprofile.Attributes;
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
import org.keycloak.userprofile.ValidationException;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class UserProfileTest extends AbstractUserProfileTest {
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
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);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIdempotentProfile() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testIdempotentProfile);
|
||||
}
|
||||
|
||||
private static void testIdempotentProfile(KeycloakSession session) {
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
|
||||
attributes.put(UserModel.USERNAME, "profiled-user");
|
||||
|
||||
// once created, profile attributes can not be changed
|
||||
assertTrue(profile.getAttributes().contains(UserModel.USERNAME));
|
||||
assertNull(profile.getAttributes().getFirstValue(UserModel.USERNAME));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomAttributeInAnyContext() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testCustomAttributeInAnyContext);
|
||||
}
|
||||
|
||||
private static void testCustomAttributeInAnyContext(KeycloakSession session) {
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
attributes.put(UserModel.USERNAME, "profiled-user");
|
||||
|
||||
UserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
|
||||
provider.setConfiguration("{\"attributes\": [{\"name\": \"address\", \"required\": {}}]}");
|
||||
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
|
||||
try {
|
||||
profile.validate();
|
||||
Assert.fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
// address is mandatory
|
||||
assertTrue(ve.isAttributeOnError("address"));
|
||||
}
|
||||
|
||||
assertThat(profile.getAttributes().nameSet(),
|
||||
containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, UserModel.FIRST_NAME, UserModel.LAST_NAME, "address"));
|
||||
|
||||
attributes.put("address", "myaddress");
|
||||
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
|
||||
profile.validate();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolveProfile() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testResolveProfile);
|
||||
}
|
||||
|
||||
private static void testResolveProfile(KeycloakSession session) {
|
||||
configureAuthenticationSession(session);
|
||||
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
|
||||
attributes.put(UserModel.USERNAME, "profiled-user");
|
||||
|
||||
UserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
|
||||
provider.setConfiguration("{\"attributes\": [{\"name\": \"business.address\", \"required\": {\"scopes\": [\"customer\"]}}]}");
|
||||
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
|
||||
profile.getAttributes();
|
||||
|
||||
try {
|
||||
profile.validate();
|
||||
Assert.fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
// address is mandatory
|
||||
assertTrue(ve.isAttributeOnError("business.address"));
|
||||
}
|
||||
|
||||
attributes.put("business.address", "valid-address");
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||
profile.validate();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidation() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileTest::failValidationWhenEmptyAttributes);
|
||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testAttributeValidation);
|
||||
}
|
||||
|
||||
private static void failValidationWhenEmptyAttributes(KeycloakSession session) {
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||
UserProfile profile;
|
||||
|
||||
try {
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
Assert.fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
// username is mandatory
|
||||
assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
|
||||
}
|
||||
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
||||
try {
|
||||
attributes.clear();
|
||||
attributes.put(UserModel.EMAIL, "profile-user@keycloak.org");
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
Assert.fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
// username is mandatory
|
||||
assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
|
||||
}
|
||||
|
||||
try {
|
||||
realm.setRegistrationEmailAsUsername(true);
|
||||
attributes.clear();
|
||||
attributes.put(UserModel.EMAIL, "profile-user@keycloak.org");
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
profile.validate();
|
||||
} catch (ValidationException ve) {
|
||||
Assert.fail("Should be OK email as username");
|
||||
} finally {
|
||||
// we should probably avoid this kind of logic and make the test reset the realm to original state
|
||||
realm.setRegistrationEmailAsUsername(false);
|
||||
}
|
||||
|
||||
attributes.clear();
|
||||
attributes.put(UserModel.USERNAME, "profile-user");
|
||||
provider.create(UserProfileContext.UPDATE_PROFILE, attributes).validate();
|
||||
}
|
||||
|
||||
private static void testAttributeValidation(KeycloakSession session) {
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||
|
||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
List<String> errors = new ArrayList<>();
|
||||
|
||||
assertFalse(profile.getAttributes().validate(UserModel.USERNAME, (Consumer<String>) errors::add));
|
||||
assertTrue(errors.contains(Messages.MISSING_USERNAME));
|
||||
|
||||
errors.clear();
|
||||
attributes.clear();
|
||||
attributes.put(UserModel.EMAIL, "invalid");
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||
assertFalse(profile.getAttributes().validate(UserModel.EMAIL, (Consumer<String>) errors::add));
|
||||
assertTrue(errors.contains(Messages.INVALID_EMAIL));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateComplianceWithUserProfile() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testValidateComplianceWithUserProfile);
|
||||
}
|
||||
|
||||
private static void testValidateComplianceWithUserProfile(KeycloakSession session) throws IOException {
|
||||
RealmModel realm = configureSessionRealm(session);
|
||||
UserModel user = session.users().addUser(realm, "profiled-user");
|
||||
UserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
|
||||
UPConfig config = new UPConfig();
|
||||
UPAttribute attribute = new UPAttribute();
|
||||
|
||||
attribute.setName("address");
|
||||
|
||||
UPAttributeRequired requirements = new UPAttributeRequired();
|
||||
|
||||
attribute.setRequired(requirements);
|
||||
|
||||
config.addAttribute(attribute);
|
||||
|
||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||
|
||||
UserProfile profile = provider.create(UserProfileContext.ACCOUNT, user);
|
||||
|
||||
try {
|
||||
profile.validate();
|
||||
Assert.fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
// username is mandatory
|
||||
assertTrue(ve.isAttributeOnError("address"));
|
||||
}
|
||||
|
||||
user.setAttribute("address", Arrays.asList("fixed-address"));
|
||||
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, user);
|
||||
|
||||
profile.validate();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetProfileAttributes() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testGetProfileAttributes);
|
||||
}
|
||||
|
||||
private static void testGetProfileAttributes(KeycloakSession session) {
|
||||
RealmModel realm = configureSessionRealm(session);
|
||||
UserModel user = session.users().addUser(realm, org.keycloak.models.utils.KeycloakModelUtils.generateId());
|
||||
UserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
|
||||
provider.setConfiguration("{\"attributes\": [{\"name\": \"address\", \"required\": {}}]}");
|
||||
|
||||
UserProfile profile = provider.create(UserProfileContext.ACCOUNT, user);
|
||||
Attributes attributes = profile.getAttributes();
|
||||
|
||||
assertThat(attributes.nameSet(),
|
||||
containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, UserModel.FIRST_NAME, UserModel.LAST_NAME, "address"));
|
||||
|
||||
try {
|
||||
profile.validate();
|
||||
Assert.fail("Should fail validation");
|
||||
} catch (ValidationException ve) {
|
||||
// username is mandatory
|
||||
assertTrue(ve.isAttributeOnError("address"));
|
||||
}
|
||||
|
||||
assertNotNull(attributes.getFirstValue(UserModel.USERNAME));
|
||||
assertNull(attributes.getFirstValue(UserModel.EMAIL));
|
||||
assertNull(attributes.getFirstValue(UserModel.FIRST_NAME));
|
||||
assertNull(attributes.getFirstValue(UserModel.LAST_NAME));
|
||||
assertNull(attributes.getFirstValue("address"));
|
||||
|
||||
user.setAttribute("address", Arrays.asList("fixed-address"));
|
||||
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, user);
|
||||
attributes = profile.getAttributes();
|
||||
|
||||
profile.validate();
|
||||
|
||||
assertNotNull(attributes.getFirstValue("address"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateAndUpdateUser() {
|
||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testCreateAndUpdateUser);
|
||||
}
|
||||
|
||||
private static void testCreateAndUpdateUser(KeycloakSession session) {
|
||||
UserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
String userName = org.keycloak.models.utils.KeycloakModelUtils.generateId();
|
||||
|
||||
attributes.put(UserModel.USERNAME, userName);
|
||||
attributes.put("address", "fixed-address");
|
||||
|
||||
UserProfile profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||
UserModel user = profile.create();
|
||||
|
||||
assertEquals(userName, user.getUsername());
|
||||
assertEquals("fixed-address", user.getFirstAttribute("address"));
|
||||
|
||||
attributes.put(UserModel.FIRST_NAME, "Alice");
|
||||
attributes.put(UserModel.LAST_NAME, "In Chains");
|
||||
attributes.put(UserModel.EMAIL, "alice@keycloak.org");
|
||||
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes, user);
|
||||
Set<String> attributesUpdated = new HashSet<>();
|
||||
|
||||
profile.update((attributeName, userModel) -> assertTrue(attributesUpdated.add(attributeName)));
|
||||
|
||||
assertThat(attributesUpdated, containsInAnyOrder(UserModel.FIRST_NAME, UserModel.LAST_NAME, UserModel.EMAIL));
|
||||
|
||||
configureAuthenticationSession(session);
|
||||
|
||||
attributes.put("business.address", "fixed-business-address");
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes, user);
|
||||
|
||||
attributesUpdated.clear();
|
||||
profile.update((attributeName, userModel) -> assertTrue(attributesUpdated.add(attributeName)));
|
||||
|
||||
assertThat(attributesUpdated, containsInAnyOrder("business.address"));
|
||||
|
||||
assertEquals("fixed-business-address", user.getFirstAttribute("business.address"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadonlyUpdates() {
|
||||
getTestingClient().server().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());
|
||||
attributes.put("address", Arrays.asList("fixed-address"));
|
||||
attributes.put("department", Arrays.asList("sales"));
|
||||
|
||||
UserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||
|
||||
provider.setConfiguration("{\"attributes\": [{\"name\": \"department\", \"permissions\": {\"edit\": [\"admin\"]}}]}");
|
||||
|
||||
UserProfile profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||
UserModel user = profile.create();
|
||||
|
||||
assertThat(profile.getAttributes().nameSet(),
|
||||
containsInAnyOrder(UserModel.USERNAME, UserModel.EMAIL, UserModel.FIRST_NAME, UserModel.LAST_NAME, "address", "department"));
|
||||
|
||||
assertNull(user.getFirstAttribute("department"));
|
||||
|
||||
profile = provider.create(UserProfileContext.USER_API, attributes, user);
|
||||
|
||||
Set<String> attributesUpdated = new HashSet<>();
|
||||
|
||||
profile.update((attributeName, userModel) -> assertTrue(attributesUpdated.add(attributeName)));
|
||||
|
||||
assertThat(attributesUpdated, containsInAnyOrder("department"));
|
||||
|
||||
assertEquals("sales", user.getFirstAttribute("department"));
|
||||
|
||||
attributes.put("department", "cannot-change");
|
||||
|
||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes, user);
|
||||
|
||||
profile.update();
|
||||
|
||||
assertEquals("sales", user.getFirstAttribute("department"));
|
||||
|
||||
assertTrue(profile.getAttributes().isReadOnly("department"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,250 @@
|
|||
/*
|
||||
* 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.config;
|
||||
|
||||
import static org.keycloak.testsuite.user.profile.config.UPConfigUtils.readConfig;
|
||||
import static org.keycloak.testsuite.user.profile.config.UPConfigUtils.validate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
|
||||
/**
|
||||
* Unit test for {@link UPConfigParser} functionality
|
||||
*
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*
|
||||
*/
|
||||
public class UPConfigParserTest {
|
||||
|
||||
@Test
|
||||
public void attributeNameIsValid() {
|
||||
// few invalid cases
|
||||
Assert.assertFalse(UPConfigUtils.isValidAttributeName(""));
|
||||
Assert.assertFalse(UPConfigUtils.isValidAttributeName(" "));
|
||||
Assert.assertFalse(UPConfigUtils.isValidAttributeName("a b"));
|
||||
Assert.assertFalse(UPConfigUtils.isValidAttributeName("a*b"));
|
||||
Assert.assertFalse(UPConfigUtils.isValidAttributeName("a%b"));
|
||||
Assert.assertFalse(UPConfigUtils.isValidAttributeName("a$b"));
|
||||
|
||||
// few valid cases
|
||||
Assert.assertTrue(UPConfigUtils.isValidAttributeName("a-b"));
|
||||
Assert.assertTrue(UPConfigUtils.isValidAttributeName("a.b"));
|
||||
Assert.assertTrue(UPConfigUtils.isValidAttributeName("a_b"));
|
||||
Assert.assertTrue(UPConfigUtils.isValidAttributeName("a3B"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadConfigurationFromJsonFile() throws IOException {
|
||||
UPConfig config = readConfig(getValidConfigFileIS());
|
||||
|
||||
// only basic assertion to check config is loaded, more detailed tests follow
|
||||
Assert.assertEquals(5, config.getAttributes().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseConfigurationFile_OK() throws IOException {
|
||||
UPConfig config = loadValidConfig();
|
||||
|
||||
Assert.assertNotNull(config);
|
||||
|
||||
// assert *** attributes ***
|
||||
Assert.assertEquals(5, config.getAttributes().size());
|
||||
UPAttribute att = config.getAttributes().get(1);
|
||||
Assert.assertNotNull(att);
|
||||
Assert.assertEquals("email", att.getName());
|
||||
// validation
|
||||
Assert.assertEquals(3, att.getValidations().size());
|
||||
Assert.assertEquals(1, att.getValidations().get("length").size());
|
||||
Assert.assertEquals(255, att.getValidations().get("length").get("max"));
|
||||
// annotations
|
||||
Assert.assertEquals("userEmailFormFieldHint", att.getAnnotations().get("formHintKey"));
|
||||
|
||||
att = config.getAttributes().get(4);
|
||||
// required
|
||||
Assert.assertNotNull(att.getRequired());
|
||||
Assert.assertFalse(att.getRequired().isAlways());
|
||||
Assert.assertNotNull(att.getRequired().getScopes());
|
||||
Assert.assertNotNull(att.getRequired().getRoles());
|
||||
Assert.assertEquals(2, att.getRequired().getRoles().size());
|
||||
|
||||
// permissions
|
||||
att = config.getAttributes().get(3);
|
||||
Assert.assertTrue(att.getRequired().isAlways());
|
||||
|
||||
// permissions
|
||||
Assert.assertNotNull(att.getPermissions());
|
||||
Assert.assertNotNull(att.getPermissions().getEdit());
|
||||
Assert.assertEquals(1, att.getPermissions().getEdit().size());
|
||||
Assert.assertTrue(att.getPermissions().getEdit().contains("admin"));
|
||||
Assert.assertNotNull(att.getPermissions().getView());
|
||||
Assert.assertEquals(2, att.getPermissions().getView().size());
|
||||
Assert.assertTrue(att.getPermissions().getView().contains("admin"));
|
||||
Assert.assertTrue(att.getPermissions().getView().contains("user"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse valid JSON config from the test file for tests.
|
||||
*
|
||||
* @return valid config
|
||||
* @throws IOException
|
||||
*/
|
||||
private UPConfig loadValidConfig() throws IOException {
|
||||
return readConfig(getValidConfigFileIS());
|
||||
}
|
||||
|
||||
private InputStream getValidConfigFileIS() {
|
||||
return getClass().getResourceAsStream("test-OK.json");
|
||||
}
|
||||
|
||||
@Test(expected = JsonMappingException.class)
|
||||
public void parseConfigurationFile_invalidJsonFormat() throws IOException {
|
||||
readConfig(getClass().getResourceAsStream("test-invalidJsonFormat.json"));
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void parseConfigurationFile_invalidType() throws IOException {
|
||||
readConfig(getClass().getResourceAsStream("test-invalidType.json"));
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void parseConfigurationFile_unknownField() throws IOException {
|
||||
readConfig(getClass().getResourceAsStream("test-unknownField.json"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateConfiguration_OK() throws IOException {
|
||||
List<String> errors = validate(loadValidConfig());
|
||||
Assert.assertTrue(errors.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateConfiguration_attributeNameErrors() throws IOException {
|
||||
UPConfig config = loadValidConfig();
|
||||
|
||||
UPAttribute attConfig = config.getAttributes().get(1);
|
||||
|
||||
attConfig.setName(null);
|
||||
List<String> errors = validate(config);
|
||||
Assert.assertEquals(1, errors.size());
|
||||
|
||||
attConfig.setName(" ");
|
||||
errors = validate(config);
|
||||
Assert.assertEquals(1, errors.size());
|
||||
|
||||
// duplicate attribute name
|
||||
attConfig.setName("firstName");
|
||||
errors = validate(config);
|
||||
Assert.assertEquals(1, errors.size());
|
||||
|
||||
// attribute name format error - unallowed character
|
||||
attConfig.setName("ema il");
|
||||
errors = validate(config);
|
||||
Assert.assertEquals(1, errors.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateConfiguration_attributePermissionsErrors() throws IOException {
|
||||
UPConfig config = loadValidConfig();
|
||||
|
||||
UPAttribute attConfig = config.getAttributes().get(1);
|
||||
|
||||
// no permissions configures at all
|
||||
attConfig.setPermissions(null);
|
||||
List<String> errors = validate(config);
|
||||
Assert.assertEquals(0, errors.size());
|
||||
|
||||
// no permissions structure fields configured
|
||||
UPAttributePermissions permsConfig = new UPAttributePermissions();
|
||||
attConfig.setPermissions(permsConfig);
|
||||
errors = validate(config);
|
||||
Assert.assertTrue(errors.isEmpty());
|
||||
|
||||
// valid if both are present, even empty
|
||||
permsConfig.setEdit(Collections.emptyList());
|
||||
permsConfig.setView(Collections.emptyList());
|
||||
attConfig.setPermissions(permsConfig);
|
||||
errors = validate(config);
|
||||
Assert.assertEquals(0, errors.size());
|
||||
|
||||
List<String> withInvRole = new ArrayList<>();
|
||||
withInvRole.add("invalid");
|
||||
|
||||
// invalid role used for view
|
||||
permsConfig.setView(withInvRole);
|
||||
errors = validate(config);
|
||||
Assert.assertEquals(1, errors.size());
|
||||
|
||||
// invalid role used for edit also
|
||||
permsConfig.setEdit(withInvRole);
|
||||
errors = validate(config);
|
||||
Assert.assertEquals(2, errors.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateConfiguration_attributeRequirementsErrors() throws IOException {
|
||||
UPConfig config = loadValidConfig();
|
||||
|
||||
UPAttribute attConfig = config.getAttributes().get(1);
|
||||
|
||||
// it is OK without requirements configures at all
|
||||
attConfig.setRequired(null);
|
||||
List<String> errors = validate(config);
|
||||
Assert.assertEquals(0, errors.size());
|
||||
|
||||
// it is OK with empty config as it means ALWAYS required
|
||||
UPAttributeRequired reqConfig = new UPAttributeRequired();
|
||||
attConfig.setRequired(reqConfig);
|
||||
errors = validate(config);
|
||||
Assert.assertEquals(0, errors.size());
|
||||
Assert.assertTrue(reqConfig.isAlways());
|
||||
|
||||
List<String> withInvRole = new ArrayList<>();
|
||||
withInvRole.add("invalid");
|
||||
|
||||
// invalid role used
|
||||
reqConfig.setRoles(withInvRole);;
|
||||
errors = validate(config);
|
||||
Assert.assertEquals(1, errors.size());
|
||||
Assert.assertFalse(reqConfig.isAlways());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validateConfiguration_attributeValidationsErrors() throws IOException {
|
||||
UPConfig config = loadValidConfig();
|
||||
|
||||
Map<String, Map<String, Object>> validationConfig = config.getAttributes().get(1).getValidations();
|
||||
|
||||
validationConfig.put(" ",null);
|
||||
List<String> errors = validate(config);
|
||||
Assert.assertEquals(1, errors.size());
|
||||
|
||||
// TODO Validation SPI integration - test validation of the validator existence and validator config
|
||||
// validationConfig.setValidator("unknownValidator");
|
||||
// errors = UPConfigUtils.validateConfiguration(config);
|
||||
// Assert.assertEquals(1, errors.size());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* 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.config;
|
||||
|
||||
import static org.keycloak.testsuite.user.profile.config.UPConfigUtils.ROLE_ADMIN;
|
||||
import static org.keycloak.testsuite.user.profile.config.UPConfigUtils.ROLE_USER;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
|
||||
/**
|
||||
* Unit test for {@link UPConfigUtils}
|
||||
*
|
||||
* @author Vlastimil Elias <velias@redhat.com>
|
||||
*
|
||||
*/
|
||||
public class UPConfigUtilsTest {
|
||||
|
||||
@Test
|
||||
public void canBeAuthFlowContext() {
|
||||
Assert.assertFalse(UPConfigUtils.canBeAuthFlowContext(UserProfileContext.ACCOUNT));
|
||||
Assert.assertFalse(UPConfigUtils.canBeAuthFlowContext(UserProfileContext.ACCOUNT_OLD));
|
||||
Assert.assertFalse(UPConfigUtils.canBeAuthFlowContext(UserProfileContext.USER_API));
|
||||
|
||||
Assert.assertTrue(UPConfigUtils.canBeAuthFlowContext(UserProfileContext.IDP_REVIEW));
|
||||
Assert.assertTrue(UPConfigUtils.canBeAuthFlowContext(UserProfileContext.REGISTRATION_PROFILE));
|
||||
Assert.assertTrue(UPConfigUtils.canBeAuthFlowContext(UserProfileContext.REGISTRATION_USER_CREATION));
|
||||
Assert.assertTrue(UPConfigUtils.canBeAuthFlowContext(UserProfileContext.UPDATE_PROFILE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isRoleForContext() {
|
||||
|
||||
Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT, null));
|
||||
|
||||
List<String> roles = new ArrayList<>();
|
||||
roles.add(ROLE_ADMIN);
|
||||
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.USER_API, roles));
|
||||
Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT, roles));
|
||||
Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT_OLD, roles));
|
||||
Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.UPDATE_PROFILE, roles));
|
||||
|
||||
roles = new ArrayList<>();
|
||||
roles.add(ROLE_USER);
|
||||
Assert.assertFalse(UPConfigUtils.isRoleForContext(UserProfileContext.USER_API, roles));
|
||||
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT, roles));
|
||||
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT_OLD, roles));
|
||||
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.IDP_REVIEW, roles));
|
||||
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.REGISTRATION_PROFILE, roles));
|
||||
|
||||
// both in roles
|
||||
roles.add(ROLE_ADMIN);
|
||||
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.USER_API, roles));
|
||||
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT, roles));
|
||||
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.ACCOUNT_OLD, roles));
|
||||
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.IDP_REVIEW, roles));
|
||||
Assert.assertTrue(UPConfigUtils.isRoleForContext(UserProfileContext.REGISTRATION_PROFILE, roles));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void breakString() {
|
||||
List<String> ret = UPConfigUtils.getChunks(null, 2);
|
||||
Assert.assertEquals(0, ret.size());
|
||||
|
||||
ret = UPConfigUtils.getChunks("", 2);
|
||||
assertListContent(ret, "");
|
||||
|
||||
ret = UPConfigUtils.getChunks("1234567", 3);
|
||||
assertListContent(ret, "123", "456", "7");
|
||||
|
||||
ret = UPConfigUtils.getChunks("12345678", 3);
|
||||
assertListContent(ret, "123", "456", "78");
|
||||
|
||||
ret = UPConfigUtils.getChunks("123456789", 3);
|
||||
assertListContent(ret, "123", "456", "789");
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert list exactly contains all expected parts in given order
|
||||
*/
|
||||
private void assertListContent(List<String> actual, String... expectedParts) {
|
||||
int i = 0;
|
||||
Assert.assertEquals(expectedParts.length, actual.size());
|
||||
for (String ep : expectedParts) {
|
||||
Assert.assertEquals(ep, actual.get(i++));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void capitalizeFirstLetter() {
|
||||
Assert.assertNull(UPConfigUtils.capitalizeFirstLetter(null));
|
||||
Assert.assertEquals("",UPConfigUtils.capitalizeFirstLetter(""));
|
||||
Assert.assertEquals("A",UPConfigUtils.capitalizeFirstLetter("a"));
|
||||
Assert.assertEquals("AbcDefGh",UPConfigUtils.capitalizeFirstLetter("abcDefGh"));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"name":"username",
|
||||
"validations": {
|
||||
"length" : { "min": 3, "max": 80 }
|
||||
}
|
||||
},{
|
||||
"name":"email ",
|
||||
"validations": {
|
||||
"length" : { "max": 255 },
|
||||
"emailFormat": {},
|
||||
"emailDomainDenyList": {}
|
||||
},
|
||||
"required": {
|
||||
"roles" : ["user", "admin"]
|
||||
},
|
||||
"annotations": {
|
||||
"formHintKey" : "userEmailFormFieldHint",
|
||||
"anotherKey" : 10,
|
||||
"yetAnotherKey" : "some value"
|
||||
}
|
||||
},{
|
||||
"name":"firstName",
|
||||
"validations": {
|
||||
"length": { "max": 255 }
|
||||
},
|
||||
"permissions": {
|
||||
"view": ["admin", "user"],
|
||||
"edit": ["admin", "user"]
|
||||
},
|
||||
"required": {}
|
||||
}, {
|
||||
"name":"lastName",
|
||||
"validations": {
|
||||
"length": { "max": 255 }
|
||||
},
|
||||
"required": {},
|
||||
"permissions": {
|
||||
"view": ["admin", "user"],
|
||||
"edit": ["admin"]
|
||||
}
|
||||
},{
|
||||
"name":"phone",
|
||||
"validations": {
|
||||
"phoneNumberFormatInternational":{}
|
||||
},
|
||||
"required": {
|
||||
"scopes" : ["phone-1", "phone-2"],
|
||||
"roles" : ["user", "admin"]
|
||||
},
|
||||
"permissions": {
|
||||
"view": ["admin", "user"],
|
||||
"edit": ["admin"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"name":"n1"
|
||||
"name2" : ""
|
||||
},
|
||||
{
|
||||
"name":"n2"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"attributes": {}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"attributes": [
|
||||
],
|
||||
"unknown" : {}
|
||||
}
|
Loading…
Reference in a new issue