[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.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
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;
|
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 org.keycloak.json.StringListMapDeserializer;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -287,4 +288,28 @@ public class UserRepresentation {
|
||||||
public void setAccess(Map<String, Boolean> access) {
|
public void setAccess(Map<String, Boolean> access) {
|
||||||
this.access = 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
|
@Override
|
||||||
public String getFirstName() {
|
public String getFirstName() {
|
||||||
|
if (updated != null) return updated.getFirstName();
|
||||||
return getFirstAttribute(FIRST_NAME);
|
return getFirstAttribute(FIRST_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,6 +72,7 @@ public class UserAdapter implements CachedUserModel.Streams {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getLastName() {
|
public String getLastName() {
|
||||||
|
if (updated != null) return updated.getLastName();
|
||||||
return getFirstAttribute(LAST_NAME);
|
return getFirstAttribute(LAST_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,6 +83,7 @@ public class UserAdapter implements CachedUserModel.Streams {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getEmail() {
|
public String getEmail() {
|
||||||
|
if (updated != null) return updated.getEmail();
|
||||||
return getFirstAttribute(EMAIL);
|
return getFirstAttribute(EMAIL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,6 +135,7 @@ public class UserAdapter implements CachedUserModel.Streams {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUsername() {
|
public String getUsername() {
|
||||||
|
if (updated != null) return updated.getUsername();
|
||||||
return getFirstAttribute(UserModel.USERNAME);
|
return getFirstAttribute(UserModel.USERNAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.keycloak.models.jpa;
|
package org.keycloak.models.jpa;
|
||||||
|
|
||||||
import org.keycloak.common.util.MultivaluedHashMap;
|
import org.keycloak.common.util.MultivaluedHashMap;
|
||||||
|
import org.keycloak.common.util.ObjectUtil;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.GroupModel;
|
import org.keycloak.models.GroupModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -316,6 +317,9 @@ public class UserAdapter implements UserModel.Streams, JpaModel<UserEntity> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setEmail(String email) {
|
public void setEmail(String email) {
|
||||||
|
if (ObjectUtil.isBlank(email)) {
|
||||||
|
email = null;
|
||||||
|
}
|
||||||
email = KeycloakModelUtils.toLowerCaseSafe(email);
|
email = KeycloakModelUtils.toLowerCaseSafe(email);
|
||||||
user.setEmail(email, realm.isDuplicateEmailsAllowed());
|
user.setEmail(email, realm.isDuplicateEmailsAllowed());
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.keycloak.models.map.user;
|
package org.keycloak.models.map.user;
|
||||||
|
|
||||||
import org.keycloak.common.util.MultivaluedHashMap;
|
import org.keycloak.common.util.MultivaluedHashMap;
|
||||||
|
import org.keycloak.common.util.ObjectUtil;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.GroupModel;
|
import org.keycloak.models.GroupModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -205,7 +206,14 @@ public abstract class MapUserAdapter<K> extends AbstractUserModel<MapUserEntity<
|
||||||
@Override
|
@Override
|
||||||
public void setEmail(String email) {
|
public void setEmail(String email) {
|
||||||
email = KeycloakModelUtils.toLowerCaseSafe(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();
|
boolean duplicatesAllowed = realm.isDuplicateEmailsAllowed();
|
||||||
|
|
||||||
if (!duplicatesAllowed && email != null && checkEmailUniqueness(realm, email)) {
|
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;
|
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
|
* <p>An interface providing as an entry point for managing users.
|
||||||
* login when user doesn't yet exists in Keycloak DB)
|
|
||||||
*
|
*
|
||||||
|
* <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>
|
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||||
*/
|
*/
|
||||||
public interface UserProfile {
|
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");
|
* * Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||||
* you may not use this file except in compliance with the License.
|
* * and other contributors as indicated by the @author tags.
|
||||||
* You may obtain a copy of the License at
|
* *
|
||||||
|
* * 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;
|
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>
|
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||||
*/
|
*/
|
||||||
public interface UserProfileContext {
|
public enum UserProfileContext {
|
||||||
|
|
||||||
UserUpdateEvent getUpdateEvent();
|
UPDATE_PROFILE,
|
||||||
UserProfile getCurrentProfile();
|
USER_API,
|
||||||
UserProfileValidationResult validate();
|
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;
|
package org.keycloak.userprofile;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.provider.Provider;
|
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>
|
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
||||||
*/
|
*/
|
||||||
public interface UserProfileProvider extends Provider {
|
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>
|
* @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);
|
notes.put(key, object);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void removeNote(String key) {
|
||||||
|
notes.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
public String getProviderId() {
|
public String getProviderId() {
|
||||||
return providerId;
|
return providerId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ public abstract class UserModelDefaultMethods implements UserModel {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setEmail(String email) {
|
public void setEmail(String email) {
|
||||||
email = email == null ? null : email.toLowerCase();
|
email = email == null || email.trim().isEmpty() ? null : email.toLowerCase();
|
||||||
setSingleAttribute(EMAIL, email);
|
setSingleAttribute(EMAIL, email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,6 @@
|
||||||
|
|
||||||
package org.keycloak.authentication.authenticators.broker;
|
package org.keycloak.authentication.authenticators.broker;
|
||||||
|
|
||||||
import static org.keycloak.userprofile.profile.UserProfileContextFactory.forIdpReview;
|
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
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.models.utils.UserModelDelegate;
|
||||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||||
import org.keycloak.services.validation.Validation;
|
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.UserProfile;
|
||||||
import org.keycloak.userprofile.utils.UserUpdateHelper;
|
import org.keycloak.userprofile.UserProfileProvider;
|
||||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
|
||||||
|
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
@ -102,23 +101,13 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
|
||||||
EventBuilder event = context.getEvent();
|
EventBuilder event = context.getEvent();
|
||||||
event.event(EventType.UPDATE_PROFILE);
|
event.event(EventType.UPDATE_PROFILE);
|
||||||
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
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);
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return userCtx.getId();
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
UserProfile updatedProfile = result.getProfile();
|
|
||||||
|
|
||||||
UserUpdateHelper.updateIdpReview(context.getRealm(), new UserModelDelegate(null) {
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, List<String>> getAttributes() {
|
public Map<String, List<String>> getAttributes() {
|
||||||
return userCtx.getAttributes();
|
return userCtx.getAttributes();
|
||||||
|
@ -138,19 +127,50 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
|
||||||
public void removeAttribute(String name) {
|
public void removeAttribute(String name) {
|
||||||
userCtx.getAttributes().remove(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);
|
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());
|
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 = profile.getAttributes().getFirstValue(UserModel.EMAIL);
|
||||||
String newEmail = updatedProfile.getAttributes().getFirstAttribute(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);
|
event.detail(Details.UPDATED_EMAIL, newEmail);
|
||||||
|
|
||||||
// Ensure page is always shown when user later returns to it - for example with form "back" button
|
// 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;
|
package org.keycloak.authentication.forms;
|
||||||
|
|
||||||
import static org.keycloak.userprofile.profile.UserProfileContextFactory.forRegistrationProfile;
|
|
||||||
|
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.authentication.FormAction;
|
import org.keycloak.authentication.FormAction;
|
||||||
import org.keycloak.authentication.FormActionFactory;
|
import org.keycloak.authentication.FormActionFactory;
|
||||||
|
@ -34,12 +32,11 @@ import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.utils.FormMessage;
|
import org.keycloak.models.utils.FormMessage;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.resources.AttributeFormDataProcessor;
|
|
||||||
import org.keycloak.services.validation.Validation;
|
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.UserProfile;
|
||||||
import org.keycloak.userprofile.profile.representations.AttributeUserProfile;
|
import org.keycloak.userprofile.UserProfileProvider;
|
||||||
import org.keycloak.userprofile.utils.UserUpdateHelper;
|
|
||||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
|
||||||
|
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -67,33 +64,37 @@ public class RegistrationProfile implements FormAction, FormActionFactory {
|
||||||
|
|
||||||
context.getEvent().detail(Details.REGISTER_METHOD, "form");
|
context.getEvent().detail(Details.REGISTER_METHOD, "form");
|
||||||
|
|
||||||
UserProfileValidationResult result = forRegistrationProfile(context.getSession(), formData).validate();
|
UserProfileProvider profileProvider = context.getSession().getProvider(UserProfileProvider.class);
|
||||||
List<FormMessage> errors = Validation.getFormErrorsFromValidation(result);
|
UserProfile profile = profileProvider.create(UserProfileContext.REGISTRATION_PROFILE, formData);
|
||||||
|
|
||||||
if (errors.size() > 0) {
|
try {
|
||||||
if (result.hasFailureOfErrorType(Messages.EMAIL_EXISTS, Messages.INVALID_EMAIL)) {
|
profile.validate();
|
||||||
UserProfile updatedProfile = result.getProfile();
|
} catch (ValidationException pve) {
|
||||||
context.getEvent().detail(Details.EMAIL, updatedProfile.getAttributes().getFirstAttribute(UserModel.EMAIL));
|
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);
|
context.error(Errors.EMAIL_IN_USE);
|
||||||
formData.remove("email");
|
formData.remove("email");
|
||||||
} else
|
} else
|
||||||
context.error(Errors.INVALID_REGISTRATION);
|
context.error(Errors.INVALID_REGISTRATION);
|
||||||
context.validationError(formData, errors);
|
|
||||||
return;
|
|
||||||
|
|
||||||
} else {
|
context.validationError(formData, errors);
|
||||||
context.success();
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void success(FormContext context) {
|
public void success(FormContext context) {
|
||||||
UserModel user = context.getUser();
|
UserModel user = context.getUser();
|
||||||
AttributeUserProfile updatedProfile = AttributeFormDataProcessor.toUserProfile(context.getHttpRequest().getDecodedFormParameters());
|
UserProfileProvider provider = context.getSession().getProvider(UserProfileProvider.class);
|
||||||
UserUpdateHelper.updateRegistrationProfile(context.getRealm(), user, updatedProfile);
|
provider.create(UserProfileContext.REGISTRATION_PROFILE, context.getHttpRequest().getDecodedFormParameters(), user).update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -17,8 +17,6 @@
|
||||||
|
|
||||||
package org.keycloak.authentication.forms;
|
package org.keycloak.authentication.forms;
|
||||||
|
|
||||||
import static org.keycloak.userprofile.profile.UserProfileContextFactory.forRegistrationUserCreation;
|
|
||||||
|
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.authentication.FormAction;
|
import org.keycloak.authentication.FormAction;
|
||||||
import org.keycloak.authentication.FormActionFactory;
|
import org.keycloak.authentication.FormActionFactory;
|
||||||
|
@ -37,12 +35,11 @@ import org.keycloak.models.utils.FormMessage;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.resources.AttributeFormDataProcessor;
|
|
||||||
import org.keycloak.services.validation.Validation;
|
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.UserProfile;
|
||||||
import org.keycloak.userprofile.profile.representations.AttributeUserProfile;
|
import org.keycloak.userprofile.UserProfileProvider;
|
||||||
import org.keycloak.userprofile.utils.UserUpdateHelper;
|
|
||||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
|
||||||
|
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -70,33 +67,37 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
|
||||||
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||||
context.getEvent().detail(Details.REGISTER_METHOD, "form");
|
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();
|
String username = profile.getAttributes().getFirstValue(UserModel.USERNAME);
|
||||||
UserProfile newProfile = result.getProfile();
|
String firstName = profile.getAttributes().getFirstValue(UserModel.FIRST_NAME);
|
||||||
String email = newProfile.getAttributes().getFirstAttribute(UserModel.EMAIL);
|
String lastName = profile.getAttributes().getFirstValue(UserModel.LAST_NAME);
|
||||||
|
|
||||||
String username = newProfile.getAttributes().getFirstAttribute(UserModel.USERNAME);
|
|
||||||
String firstName = newProfile.getAttributes().getFirstAttribute(UserModel.FIRST_NAME);
|
|
||||||
String lastName = newProfile.getAttributes().getFirstAttribute(UserModel.LAST_NAME);
|
|
||||||
context.getEvent().detail(Details.EMAIL, email);
|
context.getEvent().detail(Details.EMAIL, email);
|
||||||
|
|
||||||
context.getEvent().detail(Details.USERNAME, username);
|
context.getEvent().detail(Details.USERNAME, username);
|
||||||
context.getEvent().detail(Details.FIRST_NAME, firstName);
|
context.getEvent().detail(Details.FIRST_NAME, firstName);
|
||||||
context.getEvent().detail(Details.LAST_NAME, lastName);
|
context.getEvent().detail(Details.LAST_NAME, lastName);
|
||||||
|
|
||||||
List<FormMessage> errors = Validation.getFormErrorsFromValidation(result);
|
|
||||||
if (context.getRealm().isRegistrationEmailAsUsername()) {
|
if (context.getRealm().isRegistrationEmailAsUsername()) {
|
||||||
context.getEvent().detail(Details.USERNAME, email);
|
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);
|
context.error(Errors.EMAIL_IN_USE);
|
||||||
formData.remove(RegistrationPage.FIELD_EMAIL);
|
formData.remove(RegistrationPage.FIELD_EMAIL);
|
||||||
} else if (result.hasFailureOfErrorType(Messages.MISSING_EMAIL, Messages.MISSING_USERNAME, Messages.INVALID_EMAIL)) {
|
} else if (pve.hasError(Messages.MISSING_EMAIL, Messages.MISSING_USERNAME, Messages.INVALID_EMAIL)) {
|
||||||
if (result.hasFailureOfErrorType(Messages.INVALID_EMAIL))
|
if (pve.hasError(Messages.INVALID_EMAIL))
|
||||||
formData.remove(Validation.FIELD_EMAIL);
|
formData.remove(Validation.FIELD_EMAIL);
|
||||||
context.error(Errors.INVALID_REGISTRATION);
|
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);
|
context.error(Errors.USERNAME_IN_USE);
|
||||||
formData.remove(Validation.FIELD_USERNAME);
|
formData.remove(Validation.FIELD_USERNAME);
|
||||||
}
|
}
|
||||||
|
@ -114,24 +115,31 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void success(FormContext context) {
|
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()) {
|
if (context.getRealm().isRegistrationEmailAsUsername()) {
|
||||||
username = email;
|
username = email;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.getEvent().detail(Details.USERNAME, username)
|
context.getEvent().detail(Details.USERNAME, username)
|
||||||
.detail(Details.REGISTER_METHOD, "form")
|
.detail(Details.REGISTER_METHOD, "form")
|
||||||
.detail(Details.EMAIL, email);
|
.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);
|
user.setEnabled(true);
|
||||||
UserUpdateHelper.updateRegistrationUserCreation(context.getRealm(), user, updatedProfile);
|
|
||||||
|
context.setUser(user);
|
||||||
|
|
||||||
context.getAuthenticationSession().setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username);
|
context.getAuthenticationSession().setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username);
|
||||||
|
|
||||||
context.setUser(user);
|
|
||||||
context.getEvent().user(user);
|
context.getEvent().user(user);
|
||||||
context.getEvent().success();
|
context.getEvent().success();
|
||||||
context.newEvent().event(EventType.LOGIN);
|
context.newEvent().event(EventType.LOGIN);
|
||||||
|
|
|
@ -17,8 +17,6 @@
|
||||||
|
|
||||||
package org.keycloak.authentication.requiredactions;
|
package org.keycloak.authentication.requiredactions;
|
||||||
|
|
||||||
import static org.keycloak.userprofile.profile.UserProfileContextFactory.forUpdateProfile;
|
|
||||||
|
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.authentication.DisplayTypeRequiredActionFactory;
|
import org.keycloak.authentication.DisplayTypeRequiredActionFactory;
|
||||||
|
@ -34,9 +32,10 @@ import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.utils.FormMessage;
|
import org.keycloak.models.utils.FormMessage;
|
||||||
import org.keycloak.services.validation.Validation;
|
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.UserProfile;
|
||||||
import org.keycloak.userprofile.utils.UserUpdateHelper;
|
import org.keycloak.userprofile.UserProfileProvider;
|
||||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
|
||||||
|
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
@ -73,36 +72,34 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
|
||||||
String oldFirstName = user.getFirstName();
|
String oldFirstName = user.getFirstName();
|
||||||
String oldLastName = user.getLastName();
|
String oldLastName = user.getLastName();
|
||||||
String oldEmail = user.getEmail();
|
String oldEmail = user.getEmail();
|
||||||
UserProfileValidationResult result = forUpdateProfile(user, formData, context.getSession()).validate();
|
UserProfileProvider provider = context.getSession().getProvider(UserProfileProvider.class);
|
||||||
final UserProfile updatedProfile = result.getProfile();
|
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, formData, user);
|
||||||
List<FormMessage> errors = Validation.getFormErrorsFromValidation(result);
|
|
||||||
|
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()
|
Response challenge = context.form()
|
||||||
.setErrors(errors)
|
.setErrors(errors)
|
||||||
.setFormData(formData)
|
.setFormData(formData)
|
||||||
.createResponse(UserModel.RequiredAction.UPDATE_PROFILE);
|
.createResponse(UserModel.RequiredAction.UPDATE_PROFILE);
|
||||||
context.challenge(challenge);
|
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;
|
package org.keycloak.services.resources.account;
|
||||||
|
|
||||||
import static org.keycloak.userprofile.profile.UserProfileContextFactory.forOldAccount;
|
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.authorization.AuthorizationProvider;
|
import org.keycloak.authorization.AuthorizationProvider;
|
||||||
import org.keycloak.authorization.model.PermissionTicket;
|
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.AuthenticationManager;
|
||||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||||
import org.keycloak.services.managers.UserConsentManager;
|
import org.keycloak.services.managers.UserConsentManager;
|
||||||
import org.keycloak.services.managers.UserSessionManager;
|
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.resources.AbstractSecuredLocalService;
|
import org.keycloak.services.resources.AbstractSecuredLocalService;
|
||||||
import org.keycloak.services.resources.RealmsResource;
|
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.services.validation.Validation;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.storage.ReadOnlyException;
|
import org.keycloak.storage.ReadOnlyException;
|
||||||
|
import org.keycloak.userprofile.UserProfileContext;
|
||||||
|
import org.keycloak.userprofile.ValidationException;
|
||||||
import org.keycloak.userprofile.UserProfile;
|
import org.keycloak.userprofile.UserProfile;
|
||||||
import org.keycloak.userprofile.utils.UserUpdateHelper;
|
import org.keycloak.userprofile.UserProfileProvider;
|
||||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
import org.keycloak.utils.CredentialHelper;
|
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());
|
event.event(EventType.UPDATE_PROFILE).client(auth.getClient()).user(auth.getUser());
|
||||||
|
|
||||||
UserProfileValidationResult result = forOldAccount(user, formData, session).validate();
|
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
|
||||||
List<FormMessage> errors = Validation.getFormErrorsFromValidation(result);
|
UserProfile profile = profileProvider.create(UserProfileContext.ACCOUNT_OLD, formData, user);
|
||||||
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
setReferrerOnPage();
|
|
||||||
Response.Status status = Status.OK;
|
|
||||||
|
|
||||||
if (result.hasFailureOfErrorType(Messages.READ_ONLY_USERNAME)) {
|
|
||||||
status = Response.Status.BAD_REQUEST;
|
|
||||||
} else if (result.hasFailureOfErrorType(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 {
|
try {
|
||||||
// backward compatibility with old account console where attributes are not removed if missing
|
// backward compatibility with old account console where attributes are not removed if missing
|
||||||
UserUpdateHelper.updateAccountOldConsole(realm, user, updatedProfile);
|
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 (pve.hasError(Messages.READ_ONLY_USERNAME)) {
|
||||||
|
status = Response.Status.BAD_REQUEST;
|
||||||
|
} else if (pve.hasError(Messages.EMAIL_EXISTS, Messages.USERNAME_EXISTS)) {
|
||||||
|
status = Response.Status.CONFLICT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return account.setErrors(status, errors).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT);
|
||||||
|
}
|
||||||
} catch (ReadOnlyException e) {
|
} catch (ReadOnlyException e) {
|
||||||
setReferrerOnPage();
|
setReferrerOnPage();
|
||||||
return account.setError(Response.Status.BAD_REQUEST, Messages.READ_ONLY_USER).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT);
|
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();
|
event.success();
|
||||||
setReferrerOnPage();
|
setReferrerOnPage();
|
||||||
return account.setSuccess(Messages.ACCOUNT_UPDATED).createResponse(AccountPages.ACCOUNT);
|
return account.setSuccess(Messages.ACCOUNT_UPDATED).createResponse(AccountPages.ACCOUNT);
|
||||||
|
|
|
@ -16,8 +16,6 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.services.resources.account;
|
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.annotations.cache.NoCache;
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.keycloak.common.ClientConnection;
|
import org.keycloak.common.ClientConnection;
|
||||||
|
@ -42,14 +40,15 @@ import org.keycloak.representations.account.UserRepresentation;
|
||||||
import org.keycloak.services.ErrorResponse;
|
import org.keycloak.services.ErrorResponse;
|
||||||
import org.keycloak.services.managers.Auth;
|
import org.keycloak.services.managers.Auth;
|
||||||
import org.keycloak.services.managers.UserConsentManager;
|
import org.keycloak.services.managers.UserConsentManager;
|
||||||
import org.keycloak.services.managers.UserSessionManager;
|
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.resources.account.resources.ResourcesService;
|
import org.keycloak.services.resources.account.resources.ResourcesService;
|
||||||
import org.keycloak.services.util.ResolveRelative;
|
import org.keycloak.services.util.ResolveRelative;
|
||||||
import org.keycloak.storage.ReadOnlyException;
|
import org.keycloak.storage.ReadOnlyException;
|
||||||
import org.keycloak.theme.Theme;
|
import org.keycloak.theme.Theme;
|
||||||
import org.keycloak.userprofile.utils.UserUpdateHelper;
|
import org.keycloak.userprofile.UserProfileContext;
|
||||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
import org.keycloak.userprofile.ValidationException;
|
||||||
|
import org.keycloak.userprofile.UserProfile;
|
||||||
|
import org.keycloak.userprofile.UserProfileProvider;
|
||||||
|
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.DELETE;
|
import javax.ws.rs.DELETE;
|
||||||
|
@ -158,25 +157,26 @@ public class AccountRestService {
|
||||||
|
|
||||||
event.event(EventType.UPDATE_PROFILE).client(auth.getClient()).user(auth.getUser());
|
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 {
|
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();
|
event.success();
|
||||||
|
|
||||||
return Response.noContent().build();
|
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) {
|
} catch (ReadOnlyException e) {
|
||||||
return ErrorResponse.error(Messages.READ_ONLY_USER, Response.Status.BAD_REQUEST);
|
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.resources.admin.permissions.AdminPermissionEvaluator;
|
||||||
import org.keycloak.services.validation.Validation;
|
import org.keycloak.services.validation.Validation;
|
||||||
import org.keycloak.storage.ReadOnlyException;
|
import org.keycloak.storage.ReadOnlyException;
|
||||||
import org.keycloak.userprofile.utils.UserUpdateHelper;
|
import org.keycloak.userprofile.ValidationException;
|
||||||
import org.keycloak.userprofile.validation.AttributeValidationResult;
|
import org.keycloak.userprofile.UserProfile;
|
||||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
import org.keycloak.userprofile.UserProfileProvider;
|
||||||
import org.keycloak.userprofile.validation.ValidationResult;
|
|
||||||
import org.keycloak.utils.ProfileHelper;
|
import org.keycloak.utils.ProfileHelper;
|
||||||
|
|
||||||
import javax.ws.rs.BadRequestException;
|
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_ID;
|
||||||
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
|
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
|
* Base resource for managing users
|
||||||
|
@ -170,11 +169,14 @@ public class UserResource {
|
||||||
wasPermanentlyLockedOut = session.getProvider(BruteForceProtector.class).isPermanentlyLockedOut(session, realm, user);
|
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) {
|
if (response != null) {
|
||||||
return response;
|
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);
|
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
|
// 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) {
|
public static Response validateUserProfile(UserProfile profile) {
|
||||||
UserProfileValidationResult result = forUserResource(user, rep, session).validate();
|
try {
|
||||||
if (!result.getErrors().isEmpty()) {
|
profile.validate();
|
||||||
for (AttributeValidationResult attrValidation : result.getErrors()) {
|
} catch (ValidationException pve) {
|
||||||
StringBuilder s = new StringBuilder("Failed to update attribute " + attrValidation.getField() + ": ");
|
for (ValidationException.Error error : pve.getErrors()) {
|
||||||
for (ValidationResult valResult : attrValidation.getFailedValidations()) {
|
StringBuilder s = new StringBuilder("Failed to update attribute " + error.getAttribute() + ": ");
|
||||||
s.append(valResult.getErrorType() + ", ");
|
|
||||||
}
|
s.append(error.getMessage()).append(", ");
|
||||||
|
|
||||||
logger.warn(s);
|
logger.warn(s);
|
||||||
}
|
}
|
||||||
return ErrorResponse.error("Could not update user! See server log for more details", Response.Status.BAD_REQUEST);
|
return ErrorResponse.error("Could not update user! See server log for more details", Response.Status.BAD_REQUEST);
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void updateUserFromRep(UserModel user, UserRepresentation rep, KeycloakSession session, boolean isUpdateExistingUser) {
|
public static void updateUserFromRep(UserProfile profile, UserModel user, UserRepresentation rep, KeycloakSession session, boolean isUpdateExistingUser) {
|
||||||
boolean removeMissingRequiredActions = isUpdateExistingUser;
|
boolean removeMissingRequiredActions = isUpdateExistingUser;
|
||||||
UserUpdateHelper.updateUserResource(session, user, rep, rep.getAttributes() != null);
|
|
||||||
|
|
||||||
if (rep.isEnabled() != null) user.setEnabled(rep.isEnabled());
|
if (rep.isEnabled() != null) user.setEnabled(rep.isEnabled());
|
||||||
if (rep.isEmailVerified() != null) user.setEmailVerified(rep.isEmailVerified());
|
if (rep.isEmailVerified() != null) user.setEmailVerified(rep.isEmailVerified());
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.services.resources.admin;
|
package org.keycloak.services.resources.admin;
|
||||||
|
|
||||||
|
import static org.keycloak.userprofile.UserProfileContext.USER_API;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||||
|
@ -39,6 +41,8 @@ import org.keycloak.services.ErrorResponse;
|
||||||
import org.keycloak.services.ForbiddenException;
|
import org.keycloak.services.ForbiddenException;
|
||||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||||
import org.keycloak.services.resources.admin.permissions.UserPermissionEvaluator;
|
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.Consumes;
|
||||||
import javax.ws.rs.GET;
|
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 {
|
try {
|
||||||
Response response = UserResource.validateUserProfile(null, rep, session);
|
Response response = UserResource.validateUserProfile(profile);
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
return response;
|
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.createFederatedIdentities(rep, session, realm, user);
|
||||||
RepresentationToModel.createGroups(rep, realm, user);
|
RepresentationToModel.createGroups(rep, realm, user);
|
||||||
|
|
||||||
|
|
|
@ -26,21 +26,17 @@ import org.keycloak.policy.PasswordPolicyManagerProvider;
|
||||||
import org.keycloak.policy.PolicyError;
|
import org.keycloak.policy.PolicyError;
|
||||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.userprofile.validation.AttributeValidationResult;
|
import org.keycloak.userprofile.ValidationException;
|
||||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
|
||||||
|
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class Validation {
|
public class Validation {
|
||||||
|
|
||||||
public static final String FIELD_PASSWORD_CONFIRM = "password-confirm";
|
public static final String FIELD_PASSWORD_CONFIRM = "password-confirm";
|
||||||
public static final String FIELD_EMAIL = "email";
|
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_PASSWORD = "password";
|
||||||
public static final String FIELD_USERNAME = "username";
|
public static final String FIELD_USERNAME = "username";
|
||||||
public static final String FIELD_OTP_CODE = "totp";
|
public static final String FIELD_OTP_CODE = "totp";
|
||||||
|
@ -49,76 +45,10 @@ public class Validation {
|
||||||
// Actually allow same emails like angular. See ValidationTest.testEmailValidation()
|
// Actually allow same emails like angular. See ValidationTest.testEmailValidation()
|
||||||
private static final Pattern EMAIL_PATTERN = Pattern.compile("[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*");
|
private 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){
|
private static void addError(List<FormMessage> errors, String field, String message){
|
||||||
errors.add(new FormMessage(field, 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.
|
* Validate if user object contains all mandatory fields.
|
||||||
*
|
*
|
||||||
|
@ -155,12 +85,12 @@ public class Validation {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static List<FormMessage> getFormErrorsFromValidation(UserProfileValidationResult results) {
|
public static List<FormMessage> getFormErrorsFromValidation(List<ValidationException.Error> errors) {
|
||||||
List<FormMessage> errors = new ArrayList<>();
|
List<FormMessage> messages = new ArrayList<>();
|
||||||
for (AttributeValidationResult result : results.getErrors()) {
|
for (ValidationException.Error error : errors) {
|
||||||
result.getFailedValidations().forEach(o -> addError(errors, result.getField(), o.getErrorType()));
|
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.
|
# 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) {
|
user = new AbstractUserAdapterFederatedStorage.Streams(session, realm, model) {
|
||||||
@Override
|
@Override
|
||||||
public String getUsername() {
|
public String getUsername() {
|
||||||
return username;
|
return username.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setUsername(String innerUsername) {
|
public void setUsername(String innerUsername) {
|
||||||
if (! Objects.equals(innerUsername, username)) {
|
if (! Objects.equals(innerUsername, username.toLowerCase())) {
|
||||||
throw new RuntimeException("Unsupported");
|
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>
|
</resources>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<module name="com.fasterxml.jackson.core.jackson-core"/>
|
<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.api"/>
|
||||||
<module name="javax.mail.api"/>
|
<module name="javax.mail.api"/>
|
||||||
<module name="javax.ws.rs.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);
|
user = updateAndGet(user);
|
||||||
|
|
||||||
assertEquals(user.getLastName(), "Bob");
|
assertEquals(user.getLastName(), "Bob");
|
||||||
assertEquals(user.getFirstName(), originalFirstName);
|
assertNull(user.getFirstName());
|
||||||
assertEquals(user.getEmail(), originalEmail);
|
assertNull(user.getEmail());
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
|
RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
|
||||||
|
|
|
@ -93,7 +93,7 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest {
|
||||||
assertEquals("", registerPage.getPassword());
|
assertEquals("", registerPage.getPassword());
|
||||||
assertEquals("", registerPage.getPasswordConfirm());
|
assertEquals("", registerPage.getPasswordConfirm());
|
||||||
|
|
||||||
events.expectRegister("roleRichUser", "registerExistingUser@email")
|
events.expectRegister("rolerichuser", "registerExistingUser@email")
|
||||||
.removeDetail(Details.EMAIL)
|
.removeDetail(Details.EMAIL)
|
||||||
.user((String) null).error("username_in_use").assertEvent();
|
.user((String) null).error("username_in_use").assertEvent();
|
||||||
}
|
}
|
||||||
|
@ -117,7 +117,7 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest {
|
||||||
assertEquals("", registerPage.getPassword());
|
assertEquals("", registerPage.getPassword());
|
||||||
assertEquals("", registerPage.getPasswordConfirm());
|
assertEquals("", registerPage.getPasswordConfirm());
|
||||||
|
|
||||||
events.expectRegister("registerExistingUser", "registerExistingUser@email")
|
events.expectRegister("registerexistinguser", "registerExistingUser@email")
|
||||||
.removeDetail(Details.EMAIL)
|
.removeDetail(Details.EMAIL)
|
||||||
.user((String) null).error("email_in_use").assertEvent();
|
.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.register("firstName", "lastName", null, "registerUserMissingEmail", "password", "password");
|
||||||
registerPage.assertCurrent();
|
registerPage.assertCurrent();
|
||||||
assertEquals("Please specify email.", registerPage.getInputAccountErrors().getEmailError());
|
assertEquals("Please specify email.", registerPage.getInputAccountErrors().getEmailError());
|
||||||
events.expectRegister("registerUserMissingEmail", null)
|
events.expectRegister("registerusermissingemail", null)
|
||||||
.removeDetail("email")
|
.removeDetail("email")
|
||||||
.error("invalid_registration").assertEvent();
|
.error("invalid_registration").assertEvent();
|
||||||
}
|
}
|
||||||
|
@ -296,7 +296,7 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest {
|
||||||
registerPage.assertCurrent();
|
registerPage.assertCurrent();
|
||||||
assertEquals("registerUserInvalidEmailemail", registerPage.getEmail());
|
assertEquals("registerUserInvalidEmailemail", registerPage.getEmail());
|
||||||
assertEquals("Invalid email address.", registerPage.getInputAccountErrors().getEmailError());
|
assertEquals("Invalid email address.", registerPage.getInputAccountErrors().getEmailError());
|
||||||
events.expectRegister("registerUserInvalidEmail", "registerUserInvalidEmailemail")
|
events.expectRegister("registeruserinvalidemail", "registerUserInvalidEmailemail")
|
||||||
.error("invalid_registration").assertEvent();
|
.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