Refactoring user profile interfaces and consolidating user representation for both admin and account context
Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
1545b32a64
commit
fa79b686b6
30 changed files with 760 additions and 609 deletions
|
@ -17,128 +17,11 @@
|
|||
|
||||
package org.keycloak.representations.account;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import org.keycloak.json.StringListMapDeserializer;
|
||||
import org.keycloak.representations.idm.UserProfileMetadata;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.keycloak.representations.idm.AbstractUserRepresentation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
*/
|
||||
public class UserRepresentation {
|
||||
public class UserRepresentation extends AbstractUserRepresentation {
|
||||
|
||||
private String id;
|
||||
private String username;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
private String email;
|
||||
private boolean emailVerified;
|
||||
private UserProfileMetadata userProfileMetadata;
|
||||
|
||||
@JsonDeserialize(using = StringListMapDeserializer.class)
|
||||
private Map<String, List<String>> attributes;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public boolean isEmailVerified() {
|
||||
return emailVerified;
|
||||
}
|
||||
|
||||
public void setEmailVerified(boolean emailVerified) {
|
||||
this.emailVerified = emailVerified;
|
||||
}
|
||||
|
||||
public Map<String, List<String>> getAttributes() {
|
||||
return attributes;
|
||||
}
|
||||
|
||||
public void setAttributes(Map<String, List<String>> attributes) {
|
||||
this.attributes = attributes;
|
||||
}
|
||||
|
||||
public void singleAttribute(String name, String value) {
|
||||
if (this.attributes == null) this.attributes=new HashMap<>();
|
||||
attributes.put(name, (value == null ? new ArrayList<String>() : Arrays.asList(value)));
|
||||
}
|
||||
|
||||
public String firstAttribute(String key) {
|
||||
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;
|
||||
}
|
||||
|
||||
public UserProfileMetadata getUserProfileMetadata() {
|
||||
return userProfileMetadata;
|
||||
}
|
||||
|
||||
public void setUserProfileMetadata(UserProfileMetadata userProfileMetadata) {
|
||||
this.userProfileMetadata = userProfileMetadata;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* Copyright 2023 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.representations.idm;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import org.keycloak.json.StringListMapDeserializer;
|
||||
|
||||
public abstract class AbstractUserRepresentation {
|
||||
|
||||
public static String USERNAME = "username";
|
||||
public static String FIRST_NAME = "firstName";
|
||||
public static String LAST_NAME = "lastName";
|
||||
public static String EMAIL = "email";
|
||||
public static String LOCALE = "locale";
|
||||
|
||||
protected String id;
|
||||
protected String username;
|
||||
protected String firstName;
|
||||
protected String lastName;
|
||||
protected String email;
|
||||
protected Boolean emailVerified;
|
||||
@JsonDeserialize(using = StringListMapDeserializer.class)
|
||||
protected Map<String, List<String>> attributes;
|
||||
private UserProfileMetadata userProfileMetadata;
|
||||
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public Boolean isEmailVerified() {
|
||||
return emailVerified;
|
||||
}
|
||||
|
||||
public void setEmailVerified(Boolean emailVerified) {
|
||||
this.emailVerified = emailVerified;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the attributes set to this user except the root attributes.
|
||||
*
|
||||
* @return the user attributes.
|
||||
*/
|
||||
public Map<String, List<String>> getAttributes() {
|
||||
return attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the user attributes including the root attributes.
|
||||
*
|
||||
* @return all the user attributes.
|
||||
*/
|
||||
@JsonIgnore
|
||||
public Map<String, List<String>> getRawAttributes() {
|
||||
Map<String, List<String>> attrs = new HashMap<>(Optional.ofNullable(attributes).orElse(new HashMap<>()));
|
||||
|
||||
if (username != null)
|
||||
attrs.put(USERNAME, Collections.singletonList(getUsername()));
|
||||
else
|
||||
attrs.remove(USERNAME);
|
||||
|
||||
if (email != null)
|
||||
attrs.put(EMAIL, Collections.singletonList(getEmail()));
|
||||
else
|
||||
attrs.remove(EMAIL);
|
||||
|
||||
if (lastName != null)
|
||||
attrs.put(LAST_NAME, Collections.singletonList(getLastName()));
|
||||
|
||||
if (firstName != null)
|
||||
attrs.put(FIRST_NAME, Collections.singletonList(getFirstName()));
|
||||
|
||||
return attrs;
|
||||
}
|
||||
|
||||
public void setAttributes(Map<String, List<String>> attributes) {
|
||||
this.attributes = attributes;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <R extends AbstractUserRepresentation> R singleAttribute(String name, String value) {
|
||||
if (this.attributes == null) this.attributes=new HashMap<>();
|
||||
attributes.put(name, (value == null ? Collections.emptyList() : Arrays.asList(value)));
|
||||
return (R) this;
|
||||
}
|
||||
|
||||
public String firstAttribute(String key) {
|
||||
return this.attributes == null ? null : this.attributes.get(key) == null ? null : this.attributes.get(key).isEmpty()? null : this.attributes.get(key).get(0);
|
||||
}
|
||||
|
||||
public void setUserProfileMetadata(UserProfileMetadata userProfileMetadata) {
|
||||
this.userProfileMetadata = userProfileMetadata;
|
||||
}
|
||||
|
||||
public UserProfileMetadata getUserProfileMetadata() {
|
||||
return userProfileMetadata;
|
||||
}
|
||||
}
|
|
@ -17,14 +17,7 @@
|
|||
|
||||
package org.keycloak.representations.idm;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import org.keycloak.json.StringListMapDeserializer;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
|
@ -32,24 +25,16 @@ import java.util.Set;
|
|||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class UserRepresentation {
|
||||
public class UserRepresentation extends AbstractUserRepresentation{
|
||||
|
||||
protected String self; // link
|
||||
protected String id;
|
||||
protected String origin;
|
||||
protected Long createdTimestamp;
|
||||
protected String username;
|
||||
protected Boolean enabled;
|
||||
protected Boolean totp;
|
||||
protected Boolean emailVerified;
|
||||
protected String firstName;
|
||||
protected String lastName;
|
||||
protected String email;
|
||||
protected String federationLink;
|
||||
protected String serviceAccountClientId; // For rep, it points to clientId (not DB ID)
|
||||
|
||||
@JsonDeserialize(using = StringListMapDeserializer.class)
|
||||
protected Map<String, List<String>> attributes;
|
||||
protected List<CredentialRepresentation> credentials;
|
||||
protected Set<String> disableableCredentialTypes;
|
||||
protected List<String> requiredActions;
|
||||
|
@ -66,7 +51,6 @@ public class UserRepresentation {
|
|||
|
||||
protected List<String> groups;
|
||||
private Map<String, Boolean> access;
|
||||
private UserProfileMetadata userProfileMetadata;
|
||||
|
||||
public String getSelf() {
|
||||
return self;
|
||||
|
@ -76,14 +60,6 @@ public class UserRepresentation {
|
|||
this.self = self;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Long getCreatedTimestamp() {
|
||||
return createdTimestamp;
|
||||
}
|
||||
|
@ -92,38 +68,6 @@ public class UserRepresentation {
|
|||
this.createdTimestamp = createdTimestamp;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public Boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
@ -142,32 +86,6 @@ public class UserRepresentation {
|
|||
this.totp = totp;
|
||||
}
|
||||
|
||||
public Boolean isEmailVerified() {
|
||||
return emailVerified;
|
||||
}
|
||||
|
||||
public void setEmailVerified(Boolean emailVerified) {
|
||||
this.emailVerified = emailVerified;
|
||||
}
|
||||
|
||||
public Map<String, List<String>> getAttributes() {
|
||||
return attributes;
|
||||
}
|
||||
|
||||
public void setAttributes(Map<String, List<String>> attributes) {
|
||||
this.attributes = attributes;
|
||||
}
|
||||
|
||||
public UserRepresentation singleAttribute(String name, String value) {
|
||||
if (this.attributes == null) this.attributes=new HashMap<>();
|
||||
attributes.put(name, (value == null ? new ArrayList<String>() : Arrays.asList(value)));
|
||||
return this;
|
||||
}
|
||||
|
||||
public String firstAttribute(String key) {
|
||||
return this.attributes == null ? null : this.attributes.get(key) == null ? null : this.attributes.get(key).isEmpty()? null : this.attributes.get(key).get(0);
|
||||
}
|
||||
|
||||
public List<CredentialRepresentation> getCredentials() {
|
||||
return credentials;
|
||||
}
|
||||
|
@ -289,36 +207,4 @@ public class UserRepresentation {
|
|||
public void setAccess(Map<String, Boolean> access) {
|
||||
this.access = access;
|
||||
}
|
||||
|
||||
public Map<String, List<String>> toAttributes() {
|
||||
Map<String, List<String>> attrs = new HashMap<>();
|
||||
|
||||
if (getAttributes() != null) attrs.putAll(getAttributes());
|
||||
|
||||
if (getUsername() != null)
|
||||
attrs.put("username", Collections.singletonList(getUsername()));
|
||||
else
|
||||
attrs.remove("username");
|
||||
|
||||
if (getEmail() != null)
|
||||
attrs.put("email", Collections.singletonList(getEmail()));
|
||||
else
|
||||
attrs.remove("email");
|
||||
|
||||
if (getLastName() != null)
|
||||
attrs.put("lastName", Collections.singletonList(getLastName()));
|
||||
|
||||
if (getFirstName() != null)
|
||||
attrs.put("firstName", Collections.singletonList(getFirstName()));
|
||||
|
||||
return attrs;
|
||||
}
|
||||
|
||||
public void setUserProfileMetadata(UserProfileMetadata userProfileMetadata) {
|
||||
this.userProfileMetadata = userProfileMetadata;
|
||||
}
|
||||
|
||||
public UserProfileMetadata getUserProfileMetadata() {
|
||||
return userProfileMetadata;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,8 +32,20 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
|
|||
public class UPConfig {
|
||||
|
||||
public enum UnmanagedAttributePolicy {
|
||||
|
||||
/**
|
||||
* Unmanaged attributes are enabled and available from any context.
|
||||
*/
|
||||
ENABLED,
|
||||
|
||||
/**
|
||||
* Unmanaged attributes are only available as read-only and only through the management interfaces.
|
||||
*/
|
||||
ADMIN_VIEW,
|
||||
|
||||
/**
|
||||
* Unmanaged attributes are only available as read-write and only through the management interfaces.
|
||||
*/
|
||||
ADMIN_EDIT
|
||||
}
|
||||
|
||||
|
|
|
@ -29,3 +29,17 @@ Proceed to https://www.keycloak.org/server/features[Enabling and disabling featu
|
|||
The Keycloak CR now includes an `startOptimized` field, which may be used to override the default assumption about whether to use the `--optimized` flag for the start command.
|
||||
As a result, you can use the CR to configure build time options also when a custom Keycloak image is used.
|
||||
|
||||
= Breaking changes to the User Profile SPI
|
||||
|
||||
In this release, there are changes to the User Profile SPI that might impact existing implementations based on this SPI. For more details, check the
|
||||
link:{upgradingguide_link}[{upgradingguide_name}].
|
||||
|
||||
= Changes to the user representation in both Admin API and Account contexts
|
||||
|
||||
In this release, we are encapsulating the root user attributes (such as `username`, `email`, `firstName`, `lastName`, and `locale`) by moving them to a base/abstract class in order to align how these attributes
|
||||
are marshalled and unmarshalled when using both Admin and Account REST APIs.
|
||||
|
||||
This strategy provides consistency in how attributes are managed by clients and makes sure they conform to the user profile
|
||||
configuration set to a realm.
|
||||
|
||||
For more details, see link:{upgradingguide_link}[{upgradingguide_name}].
|
||||
|
|
|
@ -18,3 +18,28 @@ import AuthZ from 'keycloak-js/authz';
|
|||
The `spi-truststore-file-*` options and the truststore related options `https-trust-store-*` are deprecated, please use the new default location for truststore material, `conf/truststores`, or specify your desired paths via the `truststore-paths` option. For details refer to the relevant https://www.keycloak.org/server/keycloak-truststore[guide].
|
||||
|
||||
The `tls-hostname-verifier` property should be used instead of the `spi-truststore-file-hostname-verification-policy` property.
|
||||
|
||||
= Breaking changes to the User Profile SPI
|
||||
|
||||
If you are using the User Profile SPI in your extension, you might be impacted by the API changes introduced in this release.
|
||||
|
||||
The `org.keycloak.userprofile.Attributes` interface includes the following changes:
|
||||
|
||||
* Method `getValues` was renamed to `get` to make it more aligned with the same operation from a regular Java `Map`
|
||||
* Method `isRootAttribute` was moved to the utility class `org.keycloak.userprofile.UserProfileUtil.isRootAttribute`
|
||||
* Method `getFirstValue` was renamed to `getFirst` to make it less verbose
|
||||
* Method `getReadable(boolean)` was removed and now all attributes (including root attributes) are returned whenever they have read rights.
|
||||
|
||||
= Changes to the user representation in both Admin API and Account contexts
|
||||
|
||||
Both `org.keycloak.representations.idm.UserRepresentation` and `org.keycloak.representations.account.UserRepresentation` representation classes have changed
|
||||
so that the root user attributes (such as `username`, `email`, `firstName`, `lastName`, and `locale`) have a consistent representation when fetching or sending
|
||||
the representation payload to the Admin and Account APIS, respectively.
|
||||
|
||||
The `username`, `email`, `firstName`, `lastName`, and `locale` attributes were moved to a new `org.keycloak.representations.idm.AbstractUserRepresentation` base class.
|
||||
|
||||
Also the `getAttributes` method is targeted for representing only custom attributes, so you should not expect any root attribute in the map returned by this method. This method is
|
||||
mainly targeted for clients when updating or fetching any custom attribute for a give user.
|
||||
|
||||
In order to resolve all the attributes including the root attributes, a new `getRawAttributes` method was added so that the resulting map also includes the root attributes. However,
|
||||
this method is not available from the representation payload and it is targeted to be used by the server when managing user profiles.
|
|
@ -1,11 +1,15 @@
|
|||
package org.keycloak.admin.ui.rest;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static org.keycloak.userprofile.UserProfileContext.USER_API;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
|
@ -18,6 +22,7 @@ import org.keycloak.models.UserModel;
|
|||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
import org.keycloak.utils.StringUtil;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
|
@ -42,7 +47,7 @@ public class UserResource {
|
|||
|
||||
if (provider.isEnabled(realm)) {
|
||||
UserProfile profile = provider.create(USER_API, user);
|
||||
Map<String, List<String>> managedAttributes = profile.getAttributes().getReadable(false);
|
||||
Map<String, List<String>> managedAttributes = profile.getAttributes().getReadable();
|
||||
Map<String, List<String>> attributes = new HashMap<>(user.getAttributes());
|
||||
UPConfig upConfig = provider.getConfiguration();
|
||||
|
||||
|
@ -56,10 +61,10 @@ public class UserResource {
|
|||
|
||||
attributes.remove(UserModel.USERNAME);
|
||||
attributes.remove(UserModel.EMAIL);
|
||||
attributes.remove(UserModel.FIRST_NAME);
|
||||
attributes.remove(UserModel.LAST_NAME);
|
||||
|
||||
return attributes;
|
||||
return attributes.entrySet().stream()
|
||||
.filter(entry -> ofNullable(entry.getValue()).orElse(emptyList()).stream().anyMatch(StringUtil::isNotBlank))
|
||||
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
|
||||
}
|
||||
|
||||
return Collections.emptyMap();
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
|
@ -29,6 +31,8 @@ import java.util.Optional;
|
|||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.CollectionUtil;
|
||||
import org.keycloak.models.Constants;
|
||||
|
@ -38,6 +42,7 @@ import org.keycloak.models.UserModel;
|
|||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy;
|
||||
import org.keycloak.utils.StringUtil;
|
||||
import org.keycloak.validate.ValidationContext;
|
||||
import org.keycloak.validate.ValidationError;
|
||||
|
||||
|
@ -68,7 +73,7 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
|||
private final Map<String, AttributeMetadata> metadataByAttribute;
|
||||
private final UPConfig upConfig;
|
||||
protected final UserModel user;
|
||||
private Map<String, List<String>> unmanagedAttributes = new HashMap<>();
|
||||
private final Map<String, List<String>> unmanagedAttributes = new HashMap<>();
|
||||
|
||||
public DefaultAttributes(UserProfileContext context, Map<String, ?> attributes, UserModel user,
|
||||
UserProfileMetadata profileMetadata,
|
||||
|
@ -82,28 +87,28 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadOnly(String attributeName) {
|
||||
if (!isManagedAttribute(attributeName)) {
|
||||
public boolean isReadOnly(String name) {
|
||||
if (!isManagedAttribute(name)) {
|
||||
return !isAllowEditUnmanagedAttribute();
|
||||
}
|
||||
|
||||
if (UserModel.USERNAME.equals(attributeName)) {
|
||||
if (UserModel.USERNAME.equals(name)) {
|
||||
if (isServiceAccountUser()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (UserModel.EMAIL.equals(attributeName)) {
|
||||
if (UserModel.EMAIL.equals(name)) {
|
||||
if (isServiceAccountUser()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isReadOnlyFromMetadata(attributeName) || isReadOnlyInternalAttribute(attributeName)) {
|
||||
if (isReadOnlyFromMetadata(name) || isReadOnlyInternalAttribute(name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return getMetadata(attributeName) == null;
|
||||
return getMetadata(name) == null;
|
||||
}
|
||||
|
||||
private boolean isAllowEditUnmanagedAttribute() {
|
||||
|
@ -156,9 +161,9 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
|||
List<AttributeMetadata> metadatas = new ArrayList<>();
|
||||
|
||||
metadatas.addAll(Optional.ofNullable(this.metadataByAttribute.get(attribute.getKey()))
|
||||
.map(Collections::singletonList).orElse(Collections.emptyList()));
|
||||
.map(Collections::singletonList).orElse(emptyList()));
|
||||
metadatas.addAll(Optional.ofNullable(this.metadataByAttribute.get(READ_ONLY_ATTRIBUTE_KEY))
|
||||
.map(Collections::singletonList).orElse(Collections.emptyList()));
|
||||
.map(Collections::singletonList).orElse(emptyList()));
|
||||
|
||||
Boolean result = null;
|
||||
|
||||
|
@ -172,12 +177,15 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
|||
continue;
|
||||
}
|
||||
|
||||
if (user != null && metadata.isReadOnly(attributeContext)
|
||||
&& CollectionUtil.collectionEquals(user.getAttributeStream(name).collect(Collectors.toList()), attribute.getValue())) {
|
||||
// allow update if the value was already wrong in the user and is read-only in this context
|
||||
logger.warnf("User '%s' attribute '%s' has previous validation errors %s but is read-only in context %s.",
|
||||
user.getUsername(), name, vc.getErrors(), attributeContext.getContext());
|
||||
continue;
|
||||
if (user != null && metadata.isReadOnly(attributeContext)) {
|
||||
List<String> value = user.getAttributeStream(name).filter(StringUtil::isNotBlank).collect(Collectors.toList());
|
||||
List<String> newValue = attribute.getValue().stream().filter(StringUtil::isNotBlank).collect(Collectors.toList());
|
||||
if (CollectionUtil.collectionEquals(value, newValue)) {
|
||||
// allow update if the value was already wrong in the user and is read-only in this context
|
||||
logger.debugf("User '%s' attribute '%s' has previous validation errors %s but is read-only in context %s.",
|
||||
user.getUsername(), name, vc.getErrors(), attributeContext.getContext());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (result == null) {
|
||||
|
@ -198,7 +206,7 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
|||
}
|
||||
|
||||
@Override
|
||||
public List<String> getValues(String name) {
|
||||
public List<String> get(String name) {
|
||||
return getOrDefault(name, EMPTY_VALUE);
|
||||
}
|
||||
|
||||
|
@ -236,21 +244,7 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
|||
@Override
|
||||
public AttributeMetadata getMetadata(String name) {
|
||||
if (unmanagedAttributes.containsKey(name)) {
|
||||
return new AttributeMetadata(name, Integer.MAX_VALUE) {
|
||||
final UnmanagedAttributePolicy unmanagedAttributePolicy = upConfig.getUnmanagedAttributePolicy();
|
||||
|
||||
@Override
|
||||
public boolean canView(AttributeContext context) {
|
||||
return canEdit(context)
|
||||
|| (UnmanagedAttributePolicy.ADMIN_VIEW.equals(unmanagedAttributePolicy) && UserProfileContext.USER_API.equals(context.getContext()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canEdit(AttributeContext context) {
|
||||
return UnmanagedAttributePolicy.ENABLED.equals(unmanagedAttributePolicy)
|
||||
|| (UnmanagedAttributePolicy.ADMIN_EDIT.equals(unmanagedAttributePolicy) && UserProfileContext.USER_API.equals(context.getContext()));
|
||||
}
|
||||
};
|
||||
return createUnmanagedAttributeMetadata(name);
|
||||
}
|
||||
|
||||
return Optional.ofNullable(metadataByAttribute.get(name))
|
||||
|
@ -265,9 +259,14 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
|||
for (String name : nameSet()) {
|
||||
AttributeMetadata metadata = getMetadata(name);
|
||||
|
||||
if (metadata == null
|
||||
|| !metadata.canView(createAttributeContext(metadata))
|
||||
|| !metadata.isSelected(createAttributeContext(metadata))) {
|
||||
if (metadata == null) {
|
||||
attributes.remove(name);
|
||||
continue;
|
||||
}
|
||||
|
||||
AttributeContext attributeContext = createAttributeContext(metadata);
|
||||
|
||||
if (!metadata.canView(attributeContext) || !metadata.isSelected(attributeContext)) {
|
||||
attributes.remove(name);
|
||||
}
|
||||
}
|
||||
|
@ -277,7 +276,7 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
|||
|
||||
@Override
|
||||
public Map<String, List<String>> toMap() {
|
||||
return this;
|
||||
return Collections.unmodifiableMap(this);
|
||||
}
|
||||
|
||||
protected boolean isServiceAccountUser() {
|
||||
|
@ -342,7 +341,7 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
|||
|
||||
if (!isSupportedAttribute(key)) {
|
||||
if (!isManagedAttribute(key) && isAllowUnmanagedAttribute()) {
|
||||
unmanagedAttributes.put(key, (List<String>) entry.getValue());
|
||||
unmanagedAttributes.put(key, normalizeAttributeValues(key, entry.getValue()));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
@ -351,18 +350,7 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
|||
key = key.substring(Constants.USER_ATTRIBUTES_PREFIX.length());
|
||||
}
|
||||
|
||||
Object value = entry.getValue();
|
||||
List<String> values;
|
||||
|
||||
if (value instanceof String) {
|
||||
values = Collections.singletonList((String) value);
|
||||
} else {
|
||||
values = (List<String>) value;
|
||||
}
|
||||
|
||||
if (UserModel.USERNAME.equals(key) || UserModel.EMAIL.equals(key)) {
|
||||
values = values.stream().map(KeycloakModelUtils::toLowerCaseSafe).collect(Collectors.toList());
|
||||
}
|
||||
List<String> values = normalizeAttributeValues(key, entry.getValue());
|
||||
|
||||
newAttributes.put(key, Collections.unmodifiableList(values));
|
||||
}
|
||||
|
@ -378,28 +366,24 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
|||
AttributeMetadata metadata = metadataByAttribute.get(attributeName);
|
||||
|
||||
if (user != null && isIncludeAttributeIfNotProvided(metadata)) {
|
||||
values = user.getAttributes().getOrDefault(attributeName, EMPTY_VALUE);
|
||||
values = normalizeAttributeValues(attributeName, user.getAttributes().getOrDefault(attributeName, EMPTY_VALUE));
|
||||
}
|
||||
|
||||
newAttributes.put(attributeName, values);
|
||||
}
|
||||
|
||||
if (user != null) {
|
||||
List<String> username = newAttributes.getOrDefault(UserModel.USERNAME, Collections.emptyList());
|
||||
List<String> username = newAttributes.getOrDefault(UserModel.USERNAME, emptyList());
|
||||
|
||||
if (username.isEmpty() && isReadOnly(UserModel.USERNAME)) {
|
||||
setUserName(newAttributes, Collections.singletonList(user.getUsername()));
|
||||
}
|
||||
}
|
||||
|
||||
List<String> email = newAttributes.getOrDefault(UserModel.EMAIL, Collections.emptyList());
|
||||
List<String> email = newAttributes.getOrDefault(UserModel.EMAIL, emptyList());
|
||||
|
||||
if (!email.isEmpty() && realm.isRegistrationEmailAsUsername()) {
|
||||
List<String> lowerCaseEmailList = email.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
setUserName(newAttributes, lowerCaseEmailList);
|
||||
setUserName(newAttributes, email);
|
||||
|
||||
if (user != null && isReadOnly(UserModel.EMAIL)) {
|
||||
newAttributes.put(UserModel.EMAIL, Collections.singletonList(user.getEmail()));
|
||||
|
@ -414,6 +398,24 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
|||
return newAttributes;
|
||||
}
|
||||
|
||||
private List<String> normalizeAttributeValues(String name, Object value) {
|
||||
List<String> values;
|
||||
|
||||
if (value instanceof String) {
|
||||
values = Collections.singletonList((String) value);
|
||||
} else {
|
||||
values = (List<String>) value;
|
||||
}
|
||||
|
||||
Stream<String> valuesStream = Optional.ofNullable(values).orElse(EMPTY_VALUE).stream().filter(Objects::nonNull);
|
||||
|
||||
if (UserModel.USERNAME.equals(name) || UserModel.EMAIL.equals(name)) {
|
||||
valuesStream = valuesStream.map(KeycloakModelUtils::toLowerCaseSafe);
|
||||
}
|
||||
|
||||
return valuesStream.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private boolean isAllowUnmanagedAttribute() {
|
||||
UnmanagedAttributePolicy unmanagedAttributePolicy = upConfig.getUnmanagedAttributePolicy();
|
||||
|
||||
|
@ -466,12 +468,7 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
|||
return true;
|
||||
}
|
||||
|
||||
if (isReadOnlyInternalAttribute(name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// checks whether the attribute is a core attribute
|
||||
return isRootAttribute(name);
|
||||
return isReadOnlyInternalAttribute(name);
|
||||
}
|
||||
|
||||
private boolean isManagedAttribute(String name) {
|
||||
|
@ -511,4 +508,22 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
|||
public Map<String, List<String>> getUnmanagedAttributes() {
|
||||
return unmanagedAttributes;
|
||||
}
|
||||
|
||||
private AttributeMetadata createUnmanagedAttributeMetadata(String name) {
|
||||
return new AttributeMetadata(name, Integer.MAX_VALUE) {
|
||||
final UnmanagedAttributePolicy unmanagedAttributePolicy = upConfig.getUnmanagedAttributePolicy();
|
||||
|
||||
@Override
|
||||
public boolean canView(AttributeContext context) {
|
||||
return canEdit(context)
|
||||
|| (UnmanagedAttributePolicy.ADMIN_VIEW.equals(unmanagedAttributePolicy) && UserProfileContext.USER_API.equals(context.getContext()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canEdit(AttributeContext context) {
|
||||
return UnmanagedAttributePolicy.ENABLED.equals(unmanagedAttributePolicy)
|
||||
|| (UnmanagedAttributePolicy.ADMIN_EDIT.equals(unmanagedAttributePolicy) && UserProfileContext.USER_API.equals(context.getContext()));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,10 @@
|
|||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import static org.keycloak.userprofile.UserProfileUtil.createUserProfileMetadata;
|
||||
import static org.keycloak.userprofile.UserProfileUtil.isRootAttribute;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
@ -31,7 +35,10 @@ import java.util.stream.Collectors;
|
|||
import org.keycloak.common.util.CollectionUtil;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
import org.keycloak.representations.idm.AbstractUserRepresentation;
|
||||
import org.keycloak.storage.ReadOnlyException;
|
||||
import org.keycloak.utils.StringUtil;
|
||||
|
||||
|
@ -45,10 +52,11 @@ import org.keycloak.utils.StringUtil;
|
|||
*/
|
||||
public final class DefaultUserProfile implements UserProfile {
|
||||
|
||||
protected final UserProfileMetadata metadata;
|
||||
private final UserProfileMetadata metadata;
|
||||
private final Function<Attributes, UserModel> userSupplier;
|
||||
private final Attributes attributes;
|
||||
private final KeycloakSession session;
|
||||
private final boolean isUserProfileEnabled;
|
||||
private boolean validated;
|
||||
private UserModel user;
|
||||
|
||||
|
@ -59,6 +67,8 @@ public final class DefaultUserProfile implements UserProfile {
|
|||
this.attributes = attributes;
|
||||
this.user = user;
|
||||
this.session = session;
|
||||
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||
isUserProfileEnabled = provider.isEnabled(session.getContext().getRealm());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -144,16 +154,27 @@ public final class DefaultUserProfile implements UserProfile {
|
|||
|
||||
attrsToRemove.removeAll(attributes.nameSet());
|
||||
|
||||
for (String attr : attrsToRemove) {
|
||||
if (attributes.isReadOnly(attr)) {
|
||||
for (String name : attrsToRemove) {
|
||||
if (attributes.isReadOnly(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<String> currentValue = user.getAttributeStream(attr).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
user.removeAttribute(attr);
|
||||
List<String> currentValue = user.getAttributeStream(name).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
|
||||
if (isRootAttribute(name)) {
|
||||
if (UserModel.FIRST_NAME.equals(name)) {
|
||||
user.setFirstName(null);
|
||||
} else if (UserModel.LAST_NAME.equals(name)) {
|
||||
user.setLastName(null);
|
||||
} else if (UserModel.LOCALE.equals(name)) {
|
||||
user.removeAttribute(name);
|
||||
}
|
||||
} else {
|
||||
user.removeAttribute(name);
|
||||
}
|
||||
|
||||
for (AttributeChangeListener listener : changeListener) {
|
||||
listener.onChange(attr, user, currentValue);
|
||||
listener.onChange(name, user, currentValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -168,11 +189,88 @@ public final class DefaultUserProfile implements UserProfile {
|
|||
}
|
||||
|
||||
private boolean isCustomAttribute(String name) {
|
||||
return !getAttributes().isRootAttribute(name);
|
||||
return !isRootAttribute(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Attributes getAttributes() {
|
||||
return attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R extends AbstractUserRepresentation> R toRepresentation() {
|
||||
if (user == null) {
|
||||
throw new IllegalStateException("Can not create the representation because the user is not yet created");
|
||||
}
|
||||
|
||||
R rep = createUserRepresentation();
|
||||
Map<String, List<String>> readable = attributes.getReadable();
|
||||
Map<String, List<String>> attributesRep = new HashMap<>(readable);
|
||||
|
||||
// all the attributes here have read access and might be available in the representation
|
||||
for (String name : readable.keySet()) {
|
||||
List<String> values = attributesRep.getOrDefault(name, Collections.emptyList())
|
||||
.stream().filter(StringUtil::isNotBlank)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (values.isEmpty()) {
|
||||
// make sure empty attributes are not in the representation
|
||||
attributesRep.remove(name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isRootAttribute(name)) {
|
||||
if (UserModel.LOCALE.equals(name)) {
|
||||
// local is a special root attribute as it does not have a field in the user representation
|
||||
// it should be available as a regular attribute if set
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean isUnmanagedAttribute = isUserProfileEnabled && metadata.getAttribute(name).isEmpty();
|
||||
String value = isUnmanagedAttribute ? null : values.stream().findFirst().orElse(null);
|
||||
|
||||
if (UserModel.USERNAME.equals(name)) {
|
||||
rep.setUsername(value);
|
||||
} else if (UserModel.EMAIL.equals(name)) {
|
||||
rep.setEmail(value);
|
||||
rep.setEmailVerified(user.isEmailVerified());
|
||||
} else if (UserModel.FIRST_NAME.equals(name)) {
|
||||
rep.setFirstName(value);
|
||||
} else if (UserModel.LAST_NAME.equals(name)) {
|
||||
rep.setLastName(value);
|
||||
}
|
||||
|
||||
// we don't have root attributes as a regular attribute in the representation as they have their own fields
|
||||
attributesRep.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
rep.setId(user.getId());
|
||||
rep.setAttributes(attributesRep.isEmpty() ? null : attributesRep);
|
||||
rep.setUserProfileMetadata(createUserProfileMetadata(session, this));
|
||||
|
||||
return rep;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <R extends AbstractUserRepresentation> R createUserRepresentation() {
|
||||
UserProfileContext context = metadata.getContext();
|
||||
R rep;
|
||||
|
||||
if (UserProfileContext.USER_API.equals(context)) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
rep = (R) ModelToRepresentation.toRepresentation(session, realm, user);
|
||||
} else {
|
||||
// by default, we build the simplest representation without exposing much information about users
|
||||
rep = (R) new org.keycloak.representations.account.UserRepresentation();
|
||||
}
|
||||
|
||||
// reset the root attribute values so that they are calculated based on the user profile configuration
|
||||
rep.setUsername(null);
|
||||
rep.setEmail(null);
|
||||
rep.setFirstName(null);
|
||||
rep.setLastName(null);
|
||||
|
||||
return rep;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,22 +17,38 @@
|
|||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.idm.AbstractUserRepresentation;
|
||||
|
||||
/**
|
||||
* <p>An interface providing as an entry point for managing users.
|
||||
* <p>An interface that serves an entry point for managing users and their attributes.
|
||||
*
|
||||
* <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.
|
||||
* <p>A {@code UserProfile} provides methods for creating, and updating users as well as for accessing their attributes.
|
||||
* All its operations are based the {@link UserProfileContext}. By taking the context into account, the state and behavior of
|
||||
* {@link UserProfile} instances depend on the context they are associated with where creating, updating, validating, and
|
||||
* accessing the attribute set of a user is based on the configuration (see {@link org.keycloak.representations.userprofile.config.UPConfig})
|
||||
* and the constraints associated with a given context.
|
||||
*
|
||||
* <p>The {@link UserProfileContext} represents the different areas in Keycloak where users, and their attributes are managed.
|
||||
* 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}:
|
||||
*
|
||||
* <p>A {@code UserProfile} instance can be obtained through the {@link UserProfileProvider}.
|
||||
* <pre> {@code
|
||||
* // resolve an existing user
|
||||
* UserModel user = getExistingUser();
|
||||
* // obtain the user profile provider
|
||||
* UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||
* // create a instance for managing the user profile through the USER_API context
|
||||
* UserProfile profile = provider.create(USER_API, user);
|
||||
* }</pre>
|
||||
*
|
||||
* <p>The {@link UserProfileProvider} provides different methods for creating {@link UserProfile} instances, each one
|
||||
* target for a specific scenario such as creating a new user, updating an existing one, or only for accessing the attributes
|
||||
* for an existing user as shown in the above example.
|
||||
*
|
||||
* @see UserProfileContext
|
||||
* @see UserProfileProvider
|
||||
|
@ -69,20 +85,23 @@ public interface UserProfile {
|
|||
void update(boolean removeAttributes, AttributeChangeListener... changeListener) throws ValidationException;
|
||||
|
||||
/**
|
||||
* <p>The same as {@link #update(boolean, BiConsumer[])} but forcing the removal of attributes.
|
||||
* <p>The same as {@link #update(boolean, AttributeChangeListener...)}} 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(AttributeChangeListener... changeListener) throws ValidationException, RuntimeException {
|
||||
default void update(AttributeChangeListener... changeListener) throws ValidationException {
|
||||
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}.
|
||||
* the same from the {@link UserModel} as they are based on the configurations set in the {@link org.keycloak.representations.userprofile.config.UPConfig} and
|
||||
* the context this instance is based on.
|
||||
*
|
||||
* @return the attributes associated with this instance.
|
||||
*/
|
||||
Attributes getAttributes();
|
||||
|
||||
<R extends AbstractUserRepresentation> R toRepresentation();
|
||||
}
|
||||
|
|
|
@ -20,11 +20,23 @@
|
|||
package org.keycloak.userprofile;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ConfiguredProvider;
|
||||
import org.keycloak.representations.idm.UserProfileAttributeGroupMetadata;
|
||||
import org.keycloak.representations.idm.UserProfileAttributeMetadata;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.representations.userprofile.config.UPGroup;
|
||||
import org.keycloak.validate.Validators;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -82,4 +94,70 @@ public class UserProfileUtil {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the attribute with the given {@code name} is a root attribute.
|
||||
*
|
||||
* @param name the attribute name
|
||||
* @return
|
||||
*/
|
||||
public static boolean isRootAttribute(String name) {
|
||||
return UserModel.USERNAME.equals(name)
|
||||
|| UserModel.EMAIL.equals(name)
|
||||
|| UserModel.FIRST_NAME.equals(name)
|
||||
|| UserModel.LAST_NAME.equals(name)
|
||||
|| UserModel.LOCALE.equals(name);
|
||||
}
|
||||
|
||||
public static org.keycloak.representations.idm.UserProfileMetadata createUserProfileMetadata(KeycloakSession session, UserProfile profile) {
|
||||
Attributes profileAttributes = profile.getAttributes();
|
||||
Map<String, List<String>> am = profileAttributes.getReadable();
|
||||
|
||||
if(am == null)
|
||||
return null;
|
||||
Map<String, List<String>> unmanagedAttributes = profileAttributes.getUnmanagedAttributes();
|
||||
|
||||
List<UserProfileAttributeMetadata> attributes = am.keySet().stream()
|
||||
.map(profileAttributes::getMetadata)
|
||||
.filter(Objects::nonNull)
|
||||
.filter(attributeMetadata -> !unmanagedAttributes.containsKey(attributeMetadata.getName()))
|
||||
.sorted(Comparator.comparingInt(AttributeMetadata::getGuiOrder))
|
||||
.map(sam -> toRestMetadata(sam, session, profile))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||
UPConfig config = provider.getConfiguration();
|
||||
|
||||
List<UserProfileAttributeGroupMetadata> groups = config.getGroups().stream().map(new Function<UPGroup, UserProfileAttributeGroupMetadata>() {
|
||||
@Override
|
||||
public UserProfileAttributeGroupMetadata apply(UPGroup upGroup) {
|
||||
return new UserProfileAttributeGroupMetadata(upGroup.getName(), upGroup.getDisplayHeader(), upGroup.getDisplayDescription(), upGroup.getAnnotations());
|
||||
}
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
return new org.keycloak.representations.idm.UserProfileMetadata(attributes, groups);
|
||||
}
|
||||
|
||||
private static UserProfileAttributeMetadata toRestMetadata(AttributeMetadata am, KeycloakSession session, UserProfile profile) {
|
||||
String group = null;
|
||||
|
||||
if (am.getAttributeGroupMetadata() != null) {
|
||||
group = am.getAttributeGroupMetadata().getName();
|
||||
}
|
||||
|
||||
return new UserProfileAttributeMetadata(am.getName(),
|
||||
am.getAttributeDisplayName(),
|
||||
profile.getAttributes().isRequired(am.getName()),
|
||||
profile.getAttributes().isReadOnly(am.getName()),
|
||||
group,
|
||||
am.getAnnotations(),
|
||||
toValidatorMetadata(am, session));
|
||||
}
|
||||
|
||||
private static Map<String, Map<String, Object>> toValidatorMetadata(AttributeMetadata am, KeycloakSession session){
|
||||
// we return only validators which are instance of ConfiguredProvider. Others are expected as internal.
|
||||
return am.getValidators() == null ? null : am.getValidators().stream()
|
||||
.filter(avm -> (Validators.validator(session, avm.getValidatorId()) instanceof ConfiguredProvider))
|
||||
.collect(Collectors.toMap(AttributeValidatorMetadata::getValidatorId, AttributeValidatorMetadata::getValidatorConfig));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,20 +19,38 @@
|
|||
|
||||
package org.keycloak.userprofile;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.validate.ValidationError;
|
||||
|
||||
/**
|
||||
* <p>This interface wraps the attributes associated with a user profile. Different operations are provided to access and
|
||||
* manage these attributes.
|
||||
*
|
||||
* <p>Attributes are classified as:</p>
|
||||
* <ul>
|
||||
* <li>Managed
|
||||
* <li>Unmanaged
|
||||
* </ul>
|
||||
*
|
||||
* <p>A <i>managed</i> attribute is any attribute defined in the user profile configuration. Therefore, they are known by
|
||||
* the server and can be managed accordingly.
|
||||
*
|
||||
* <p>A <i>unmanaged</i> attributes is any attribute <b>not</b> defined in the user profile configuration. Therefore, the server
|
||||
* does not know about them and they cannot use capabilities provided by the server. However, they can still be managed by
|
||||
* administrators by setting any of the {@link org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy}.
|
||||
*
|
||||
* <p>Any attribute available from this interface has a corresponding {@link AttributeMetadata}</p>. The metadata describes
|
||||
* the settings for a given attribute so that the server can communicate to a caller the constraints
|
||||
* (see {@link org.keycloak.representations.userprofile.config.UPConfig} and the availability of the attribute in
|
||||
* a given {@link UserProfileContext}.
|
||||
*
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public interface Attributes {
|
||||
|
@ -49,8 +67,8 @@ public interface Attributes {
|
|||
*
|
||||
* @return the first value
|
||||
*/
|
||||
default String getFirstValue(String name) {
|
||||
List<String> values = getValues(name);
|
||||
default String getFirst(String name) {
|
||||
List<String> values = ofNullable(get(name)).orElse(List.of());
|
||||
|
||||
if (values.isEmpty()) {
|
||||
return null;
|
||||
|
@ -66,16 +84,16 @@ public interface Attributes {
|
|||
*
|
||||
* @return the attribute values
|
||||
*/
|
||||
List<String> getValues(String name);
|
||||
List<String> get(String name);
|
||||
|
||||
/**
|
||||
* Checks whether an attribute is read-only.
|
||||
*
|
||||
* @param key
|
||||
* @param name the attribute name
|
||||
*
|
||||
* @return
|
||||
* @return {@code true} if the attribute is read-only. Otherwise, {@code false}
|
||||
*/
|
||||
boolean isReadOnly(String key);
|
||||
boolean isReadOnly(String name);
|
||||
|
||||
/**
|
||||
* Validates the attribute with the given {@code name}.
|
||||
|
@ -105,7 +123,7 @@ public interface Attributes {
|
|||
Set<String> nameSet();
|
||||
|
||||
/**
|
||||
* Returns all attributes that can be written.
|
||||
* Returns all the attributes with read-write permissions in a particular {@link UserProfileContext}.
|
||||
*
|
||||
* @return the attributes
|
||||
*/
|
||||
|
@ -131,52 +149,23 @@ public interface Attributes {
|
|||
boolean isRequired(String name);
|
||||
|
||||
/**
|
||||
* Similar to {{@link #getReadable(boolean)}} but with the possibility to add or remove
|
||||
* the root attributes.
|
||||
* Returns only the attributes that have read permissions in a particular {@link UserProfileContext}.
|
||||
*
|
||||
* @param includeBuiltin if the root attributes should be included.
|
||||
* @return the attributes with read/write permission.
|
||||
*/
|
||||
default Map<String, List<String>> getReadable(boolean includeBuiltin) {
|
||||
return getReadable().entrySet().stream().filter(entry -> {
|
||||
if (includeBuiltin) {
|
||||
return true;
|
||||
}
|
||||
if (isRootAttribute(entry.getKey())) {
|
||||
if (UserModel.LOCALE.equals(entry.getKey()) && !entry.getValue().isEmpty()) {
|
||||
// locale is different form of built-in attribute in the sense it is related to a
|
||||
// specific feature (i18n) and does not have a top-level attribute in the user representation
|
||||
// the locale should be available from the attribute map if not empty
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns only the attributes that have read/write permissions.
|
||||
*
|
||||
* @return the attributes with read/write permission.
|
||||
* @return the attributes with read permission.
|
||||
*/
|
||||
Map<String, List<String>> getReadable();
|
||||
|
||||
/**
|
||||
* Returns whether the attribute with the given {@code name} is a root attribute.
|
||||
* Returns the attributes as a {@link Map} that are accessible to a particular {@link UserProfileContext}.
|
||||
*
|
||||
* @param name the attribute name
|
||||
* @return
|
||||
* @return a map with all the attributes
|
||||
*/
|
||||
default boolean isRootAttribute(String name) {
|
||||
return UserModel.USERNAME.equals(name)
|
||||
|| UserModel.EMAIL.equals(name)
|
||||
|| UserModel.FIRST_NAME.equals(name)
|
||||
|| UserModel.LAST_NAME.equals(name)
|
||||
|| UserModel.LOCALE.equals(name);
|
||||
}
|
||||
|
||||
Map<String, List<String>> toMap();
|
||||
|
||||
/**
|
||||
* Returns a {@link Map} holding any unmanaged attribute.
|
||||
*
|
||||
* @return a map with any unmanaged attribute
|
||||
*/
|
||||
Map<String, List<String>> getUnmanagedAttributes();
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ package org.keycloak.userprofile;
|
|||
|
||||
/**
|
||||
* <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.
|
||||
* available here representing the different areas 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.
|
||||
|
@ -30,16 +30,39 @@ package org.keycloak.userprofile;
|
|||
*/
|
||||
public enum UserProfileContext {
|
||||
|
||||
/**
|
||||
* In this context, a user profile is managed by themselves during an authentication flow such as when updating the user profile.
|
||||
*/
|
||||
UPDATE_PROFILE(true),
|
||||
|
||||
/**
|
||||
* In this context, a user profile is managed through the management interface such as the Admin API.
|
||||
*/
|
||||
USER_API(false),
|
||||
|
||||
/**
|
||||
* In this context, a user profile is managed by themselves through the account console.
|
||||
*/
|
||||
ACCOUNT(true),
|
||||
|
||||
/**
|
||||
* In this context, a user profile is managed by themselves when authenticating through a broker.
|
||||
*/
|
||||
IDP_REVIEW(false),
|
||||
|
||||
/**
|
||||
* In this context, a user profile is managed by themselves when registering to a realm.
|
||||
*/
|
||||
REGISTRATION(false),
|
||||
|
||||
/**
|
||||
* In this context, a user profile is managed by themselves when updating their email through an application initiated action.
|
||||
*/
|
||||
UPDATE_EMAIL(false);
|
||||
|
||||
protected boolean resetEmailVerified;
|
||||
private boolean resetEmailVerified;
|
||||
|
||||
private UserProfileContext(boolean resetEmailVerified){
|
||||
UserProfileContext(boolean resetEmailVerified){
|
||||
this.resetEmailVerified = resetEmailVerified;
|
||||
}
|
||||
|
||||
|
|
|
@ -166,7 +166,7 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
|
|||
profile.update((attributeName, userModel, oldValue) -> {
|
||||
if (attributeName.equals(UserModel.EMAIL)) {
|
||||
context.getAuthenticationSession().setAuthNote(UPDATE_PROFILE_EMAIL_CHANGED, "true");
|
||||
event.clone().event(EventType.UPDATE_EMAIL).detail(Details.CONTEXT, UserProfileContext.IDP_REVIEW.name()).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, profile.getAttributes().getFirstValue(UserModel.EMAIL)).success();
|
||||
event.clone().event(EventType.UPDATE_EMAIL).detail(Details.CONTEXT, UserProfileContext.IDP_REVIEW.name()).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, profile.getAttributes().getFirst(UserModel.EMAIL)).success();
|
||||
}
|
||||
});
|
||||
} catch (ValidationException pve) {
|
||||
|
@ -187,7 +187,7 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
|
|||
|
||||
logger.debugf("Profile updated successfully after first authentication with identity provider '%s' for broker user '%s'.", brokerContext.getIdpConfig().getAlias(), userCtx.getUsername());
|
||||
|
||||
String newEmail = profile.getAttributes().getFirstValue(UserModel.EMAIL);
|
||||
String newEmail = profile.getAttributes().getFirst(UserModel.EMAIL);
|
||||
|
||||
event.detail(Details.UPDATED_EMAIL, newEmail);
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
|||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.validation.Validation;
|
||||
import org.keycloak.userprofile.Attributes;
|
||||
import org.keycloak.userprofile.UserProfileContext;
|
||||
import org.keycloak.userprofile.UserProfileProvider;
|
||||
import org.keycloak.userprofile.ValidationException;
|
||||
|
@ -71,11 +72,11 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
|
|||
context.getEvent().detail(Details.REGISTER_METHOD, "form");
|
||||
|
||||
UserProfile profile = getOrCreateUserProfile(context, formData);
|
||||
String email = profile.getAttributes().getFirstValue(UserModel.EMAIL);
|
||||
|
||||
String username = profile.getAttributes().getFirstValue(UserModel.USERNAME);
|
||||
String firstName = profile.getAttributes().getFirstValue(UserModel.FIRST_NAME);
|
||||
String lastName = profile.getAttributes().getFirstValue(UserModel.LAST_NAME);
|
||||
Attributes attributes = profile.getAttributes();
|
||||
String email = attributes.getFirst(UserModel.EMAIL);
|
||||
String username = attributes.getFirst(UserModel.USERNAME);
|
||||
String firstName = attributes.getFirst(UserModel.FIRST_NAME);
|
||||
String lastName = attributes.getFirst(UserModel.LAST_NAME);
|
||||
context.getEvent().detail(Details.EMAIL, email);
|
||||
|
||||
context.getEvent().detail(Details.USERNAME, username);
|
||||
|
@ -92,7 +93,7 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
|
|||
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));
|
||||
context.getEvent().detail(Details.EMAIL, attributes.getFirst(UserModel.EMAIL));
|
||||
}
|
||||
|
||||
if (pve.hasError(Messages.EMAIL_EXISTS)) {
|
||||
|
|
|
@ -168,7 +168,7 @@ public class UpdateEmail implements RequiredActionProvider, RequiredActionFactor
|
|||
public static void updateEmailNow(EventBuilder event, UserModel user, UserProfile emailUpdateValidationResult) {
|
||||
|
||||
String oldEmail = user.getEmail();
|
||||
String newEmail = emailUpdateValidationResult.getAttributes().getFirstValue(UserModel.EMAIL);
|
||||
String newEmail = emailUpdateValidationResult.getAttributes().getFirst(UserModel.EMAIL);
|
||||
event.event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, newEmail);
|
||||
emailUpdateValidationResult.update(false, new EventAuditingAttributeChangeListener(emailUpdateValidationResult, event));
|
||||
}
|
||||
|
|
|
@ -24,12 +24,9 @@ import java.util.LinkedList;
|
|||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
|
@ -66,12 +63,9 @@ import org.keycloak.models.RealmModel;
|
|||
import org.keycloak.models.UserConsentModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
import org.keycloak.provider.ConfiguredProvider;
|
||||
import org.keycloak.representations.account.ClientRepresentation;
|
||||
import org.keycloak.representations.account.ConsentRepresentation;
|
||||
import org.keycloak.representations.account.ConsentScopeRepresentation;
|
||||
import org.keycloak.representations.idm.UserProfileAttributeMetadata;
|
||||
import org.keycloak.representations.idm.UserProfileMetadata;
|
||||
import org.keycloak.representations.account.UserRepresentation;
|
||||
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||
import org.keycloak.representations.idm.GroupRepresentation;
|
||||
|
@ -80,7 +74,6 @@ import org.keycloak.services.managers.Auth;
|
|||
import org.keycloak.services.managers.UserConsentManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.account.resources.ResourcesService;
|
||||
import org.keycloak.services.resources.admin.UserProfileResource;
|
||||
import org.keycloak.services.util.ResolveRelative;
|
||||
import org.keycloak.storage.ReadOnlyException;
|
||||
import org.keycloak.theme.Theme;
|
||||
|
@ -92,8 +85,6 @@ import org.keycloak.userprofile.UserProfileProvider;
|
|||
import org.keycloak.userprofile.EventAuditingAttributeChangeListener;
|
||||
import org.keycloak.userprofile.ValidationException;
|
||||
import org.keycloak.userprofile.ValidationException.Error;
|
||||
import org.keycloak.utils.GroupUtils;
|
||||
import org.keycloak.validate.Validators;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
|
@ -142,38 +133,17 @@ public class AccountRestService {
|
|||
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
|
||||
|
||||
UserModel user = auth.getUser();
|
||||
|
||||
UserRepresentation rep = new UserRepresentation();
|
||||
rep.setId(user.getId());
|
||||
|
||||
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||
UserProfile profile = provider.create(UserProfileContext.ACCOUNT, user);
|
||||
UserRepresentation rep = profile.toRepresentation();
|
||||
|
||||
rep.setAttributes(profile.getAttributes().getReadable(false));
|
||||
|
||||
addReadableBuiltinAttributes(user, rep, profile.getAttributes().getReadable(true).keySet());
|
||||
|
||||
if(userProfileMetadata == null || userProfileMetadata.booleanValue())
|
||||
rep.setUserProfileMetadata(UserProfileResource.createUserProfileMetadata(session, profile));
|
||||
if (userProfileMetadata != null && !userProfileMetadata) {
|
||||
rep.setUserProfileMetadata(null);
|
||||
}
|
||||
|
||||
return rep;
|
||||
}
|
||||
|
||||
private void addReadableBuiltinAttributes(UserModel user, UserRepresentation rep, Set<String> readableAttributes) {
|
||||
setIfReadable(UserModel.USERNAME, readableAttributes, rep::setUsername, user::getUsername);
|
||||
setIfReadable(UserModel.FIRST_NAME, readableAttributes, rep::setFirstName, user::getFirstName);
|
||||
setIfReadable(UserModel.LAST_NAME, readableAttributes, rep::setLastName, user::getLastName);
|
||||
setIfReadable(UserModel.EMAIL, readableAttributes, rep::setEmail, user::getEmail);
|
||||
// emailVerified is readable when email is readable
|
||||
setIfReadable(UserModel.EMAIL, readableAttributes, rep::setEmailVerified, user::isEmailVerified);
|
||||
}
|
||||
|
||||
private <T> void setIfReadable(String attributeName, Set<String> readableAttributes, Consumer<T> setter, Supplier<T> getter) {
|
||||
if (readableAttributes.contains(attributeName)) {
|
||||
setter.accept(getter.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Path("/")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
|
@ -185,7 +155,7 @@ public class AccountRestService {
|
|||
event.event(EventType.UPDATE_PROFILE).detail(Details.CONTEXT, UserProfileContext.ACCOUNT.name());
|
||||
|
||||
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
|
||||
UserProfile profile = profileProvider.create(UserProfileContext.ACCOUNT, rep.toAttributes(), auth.getUser());
|
||||
UserProfile profile = profileProvider.create(UserProfileContext.ACCOUNT, rep.getRawAttributes(), auth.getUser());
|
||||
|
||||
try {
|
||||
|
||||
|
|
|
@ -16,13 +16,9 @@
|
|||
*/
|
||||
package org.keycloak.services.resources.admin;
|
||||
|
||||
import static org.keycloak.userprofile.UserProfileUtil.createUserProfileMetadata;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
|
@ -117,56 +113,4 @@ public class UserProfileResource {
|
|||
|
||||
return Response.ok(t.getConfiguration()).type(MediaType.APPLICATION_JSON).build();
|
||||
}
|
||||
|
||||
public static UserProfileMetadata createUserProfileMetadata(KeycloakSession session, UserProfile profile) {
|
||||
Attributes profileAttributes = profile.getAttributes();
|
||||
Map<String, List<String>> am = profileAttributes.getReadable();
|
||||
|
||||
if(am == null)
|
||||
return null;
|
||||
Map<String, List<String>> unmanagedAttributes = profileAttributes.getUnmanagedAttributes();
|
||||
|
||||
List<UserProfileAttributeMetadata> attributes = am.keySet().stream()
|
||||
.map(profileAttributes::getMetadata)
|
||||
.filter(Objects::nonNull)
|
||||
.filter(attributeMetadata -> !unmanagedAttributes.containsKey(attributeMetadata.getName()))
|
||||
.sorted(Comparator.comparingInt(AttributeMetadata::getGuiOrder))
|
||||
.map(sam -> toRestMetadata(sam, session, profile))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||
UPConfig config = provider.getConfiguration();
|
||||
|
||||
List<UserProfileAttributeGroupMetadata> groups = config.getGroups().stream().map(new Function<UPGroup, UserProfileAttributeGroupMetadata>() {
|
||||
@Override
|
||||
public UserProfileAttributeGroupMetadata apply(UPGroup upGroup) {
|
||||
return new UserProfileAttributeGroupMetadata(upGroup.getName(), upGroup.getDisplayHeader(), upGroup.getDisplayDescription(), upGroup.getAnnotations());
|
||||
}
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
return new UserProfileMetadata(attributes, groups);
|
||||
}
|
||||
|
||||
private static UserProfileAttributeMetadata toRestMetadata(AttributeMetadata am, KeycloakSession session, UserProfile profile) {
|
||||
String group = null;
|
||||
|
||||
if (am.getAttributeGroupMetadata() != null) {
|
||||
group = am.getAttributeGroupMetadata().getName();
|
||||
}
|
||||
|
||||
return new UserProfileAttributeMetadata(am.getName(),
|
||||
am.getAttributeDisplayName(),
|
||||
profile.getAttributes().isRequired(am.getName()),
|
||||
profile.getAttributes().isReadOnly(am.getName()),
|
||||
group,
|
||||
am.getAnnotations(),
|
||||
toValidatorMetadata(am, session));
|
||||
}
|
||||
|
||||
private static Map<String, Map<String, Object>> toValidatorMetadata(AttributeMetadata am, KeycloakSession session){
|
||||
// we return only validators which are instance of ConfiguredProvider. Others are expected as internal.
|
||||
return am.getValidators() == null ? null : am.getValidators().stream()
|
||||
.filter(avm -> (Validators.validator(session, avm.getValidatorId()) instanceof ConfiguredProvider))
|
||||
.collect(Collectors.toMap(AttributeValidatorMetadata::getValidatorId, AttributeValidatorMetadata::getValidatorConfig));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -123,7 +123,6 @@ import java.util.stream.Stream;
|
|||
|
||||
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
|
||||
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
|
||||
import static org.keycloak.services.resources.admin.UserProfileResource.createUserProfileMetadata;
|
||||
import static org.keycloak.userprofile.UserProfileContext.USER_API;
|
||||
|
||||
/**
|
||||
|
@ -184,7 +183,7 @@ public class UserResource {
|
|||
wasPermanentlyLockedOut = session.getProvider(BruteForceProtector.class).isPermanentlyLockedOut(session, realm, user);
|
||||
}
|
||||
|
||||
Map<String, List<String>> attributes = new HashMap<>(rep.toAttributes());
|
||||
Map<String, List<String>> attributes = new HashMap<>(rep.getRawAttributes());
|
||||
|
||||
if (rep.getAttributes() == null) {
|
||||
// include existing attributes in case no attributes are set so that validation takes into account the existing
|
||||
|
@ -302,7 +301,9 @@ public class UserResource {
|
|||
) {
|
||||
auth.users().requireView(user);
|
||||
|
||||
UserRepresentation rep = ModelToRepresentation.toRepresentation(session, realm, user);
|
||||
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||
UserProfile profile = provider.create(USER_API, user);
|
||||
UserRepresentation rep = profile.toRepresentation();
|
||||
|
||||
if (realm.isIdentityFederationEnabled()) {
|
||||
List<FederatedIdentityRepresentation> reps = getFederatedIdentities(user).collect(Collectors.toList());
|
||||
|
@ -314,16 +315,8 @@ public class UserResource {
|
|||
}
|
||||
rep.setAccess(auth.users().getAccess(user));
|
||||
|
||||
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||
UserProfile profile = provider.create(USER_API, user);
|
||||
Map<String, List<String>> readableAttributes = profile.getAttributes().getReadable(false);
|
||||
|
||||
if (rep.getAttributes() != null) {
|
||||
rep.setAttributes(readableAttributes);
|
||||
}
|
||||
|
||||
if (userProfileMetadata) {
|
||||
rep.setUserProfileMetadata(createUserProfileMetadata(session, profile));
|
||||
if (!userProfileMetadata) {
|
||||
rep.setUserProfileMetadata(null);
|
||||
}
|
||||
|
||||
return rep;
|
||||
|
|
|
@ -154,7 +154,7 @@ public class UsersResource {
|
|||
|
||||
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
|
||||
|
||||
UserProfile profile = profileProvider.create(USER_API, rep.toAttributes());
|
||||
UserProfile profile = profileProvider.create(USER_API, rep.getRawAttributes());
|
||||
|
||||
try {
|
||||
Response response = UserResource.validateUserProfile(profile, session, auth.adminAuth());
|
||||
|
|
|
@ -117,6 +117,7 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
|
|||
}
|
||||
return new DefaultAttributes(context, attributes, user, metadata, session);
|
||||
}
|
||||
|
||||
return new LegacyAttributes(context, attributes, user, metadata, session);
|
||||
}
|
||||
|
||||
|
@ -153,11 +154,11 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
|
|||
@Override
|
||||
public UserModel apply(Attributes attributes) {
|
||||
if (user == null) {
|
||||
String userName = attributes.getFirstValue(UserModel.USERNAME);
|
||||
String userName = attributes.getFirst(UserModel.USERNAME);
|
||||
|
||||
// fallback to email in case email is allowed
|
||||
if (userName == null) {
|
||||
userName = attributes.getFirstValue(UserModel.EMAIL);
|
||||
userName = attributes.getFirst(UserModel.EMAIL);
|
||||
}
|
||||
|
||||
user = session.users().addUser(session.getContext().getRealm(), userName);
|
||||
|
|
|
@ -75,7 +75,7 @@ public class ImmutableAttributeValidator implements SimpleValidator {
|
|||
return context;
|
||||
}
|
||||
|
||||
List<String> email = attributeContext.getAttributes().getValues(UserModel.EMAIL);
|
||||
List<String> email = attributeContext.getAttributes().get(UserModel.EMAIL);
|
||||
|
||||
if (UserModel.USERNAME.equals(attributeName) && collectionEquals(values, email)) {
|
||||
return context;
|
||||
|
|
|
@ -70,7 +70,7 @@ public class UsernameMutationValidator implements SimpleValidator {
|
|||
|
||||
if (!realm.isEditUsernameAllowed() && user != null && !value.equals(user.getFirstAttribute(UserModel.USERNAME))) {
|
||||
Attributes attributes = attributeContext.getAttributes();
|
||||
if (realm.isRegistrationEmailAsUsername() && value.equals(attributes.getFirstValue(UserModel.EMAIL))) {
|
||||
if (realm.isRegistrationEmailAsUsername() && value.equals(attributes.getFirst(UserModel.EMAIL))) {
|
||||
// if username changed is because email as username is allowed so no validation should happen for update profile
|
||||
// it is expected that username changes when attributes are normalized by the provider
|
||||
return context;
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
package org.keycloak.testsuite.account;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
|
||||
|
@ -99,7 +101,7 @@ public class AccountRestServiceReadOnlyAttributesTest extends AbstractRestServic
|
|||
private void testAccountUpdateAttributeExpectFailure(String attrName, boolean deniedForAdminAsWell) throws IOException {
|
||||
// Attribute not yet supposed to be on the user
|
||||
UserRepresentation user = SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
|
||||
assertThat(user.getAttributes().keySet(), not(contains(attrName)));
|
||||
assertThat(Optional.ofNullable(user.getAttributes()).orElse(Map.of()).keySet(), not(contains(attrName)));
|
||||
|
||||
// Assert not possible to add the attribute to the user
|
||||
user.singleAttribute(attrName, "foo");
|
||||
|
@ -147,7 +149,7 @@ public class AccountRestServiceReadOnlyAttributesTest extends AbstractRestServic
|
|||
private void testAccountUpdateAttributeExpectSuccess(String attrName) throws IOException {
|
||||
// Attribute not yet supposed to be on the user
|
||||
UserRepresentation user = SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
|
||||
assertThat(user.getAttributes().keySet(), not(contains(attrName)));
|
||||
assertThat(Optional.ofNullable(user.getAttributes()).orElse(Map.of()).keySet(), not(contains(attrName)));
|
||||
|
||||
// Assert not possible to add the attribute to the user
|
||||
user.singleAttribute(attrName, "foo");
|
||||
|
|
|
@ -79,6 +79,7 @@ import java.util.Collections;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
|
@ -278,7 +279,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
|||
assertEquals("Brady", user.getLastName());
|
||||
assertEquals("test-user@localhost", user.getEmail());
|
||||
assertFalse(user.isEmailVerified());
|
||||
assertTrue(user.getAttributes().isEmpty());
|
||||
assertNull(user.getAttributes());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -288,7 +289,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
|||
String originalFirstName = user.getFirstName();
|
||||
String originalLastName = user.getLastName();
|
||||
String originalEmail = user.getEmail();
|
||||
Map<String, List<String>> originalAttributes = new HashMap<>(user.getAttributes());
|
||||
user.setAttributes(Optional.ofNullable(user.getAttributes()).orElse(new HashMap<>()));
|
||||
|
||||
try {
|
||||
RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
|
||||
|
@ -316,7 +317,6 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
|||
user.setFirstName(originalFirstName);
|
||||
user.setLastName(originalLastName);
|
||||
user.setEmail(originalEmail);
|
||||
user.setAttributes(originalAttributes);
|
||||
SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asResponse();
|
||||
System.out.println(response.asString());
|
||||
assertEquals(204, response.getStatus());
|
||||
|
@ -379,7 +379,8 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
|||
String originalFirstName = user.getFirstName();
|
||||
String originalLastName = user.getLastName();
|
||||
String originalEmail = user.getEmail();
|
||||
Map<String, List<String>> originalAttributes = new HashMap<>(user.getAttributes());
|
||||
assertNull(user.getAttributes());
|
||||
user.setAttributes(new HashMap<>());
|
||||
|
||||
try {
|
||||
RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
|
||||
|
@ -417,7 +418,6 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
|||
user.setFirstName(originalFirstName);
|
||||
user.setLastName(originalLastName);
|
||||
user.setEmail(originalEmail);
|
||||
user.setAttributes(originalAttributes);
|
||||
SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asResponse();
|
||||
System.out.println(response.asString());
|
||||
assertEquals(204, response.getStatus());
|
||||
|
@ -431,7 +431,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
|||
String originalFirstName = user.getFirstName();
|
||||
String originalLastName = user.getLastName();
|
||||
String originalEmail = user.getEmail();
|
||||
Map<String, List<String>> originalAttributes = new HashMap<>(user.getAttributes());
|
||||
user.setAttributes(Optional.ofNullable(user.getAttributes()).orElse(new HashMap<>()));
|
||||
|
||||
try {
|
||||
RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
|
||||
|
@ -460,12 +460,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
|||
|
||||
user = updateAndGet(user);
|
||||
|
||||
if (isDeclarativeUserProfile()) {
|
||||
assertEquals(2, user.getAttributes().size());
|
||||
assertTrue(user.getAttributes().get("attr1").isEmpty());
|
||||
} else {
|
||||
assertEquals(1, user.getAttributes().size());
|
||||
}
|
||||
assertEquals(1, user.getAttributes().size());
|
||||
assertEquals(2, user.getAttributes().get("attr2").size());
|
||||
assertThat(user.getAttributes().get("attr2"), containsInAnyOrder("val2", "val3"));
|
||||
|
||||
|
@ -522,7 +517,6 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
|||
user.setFirstName(originalFirstName);
|
||||
user.setLastName(originalLastName);
|
||||
user.setEmail(originalEmail);
|
||||
user.setAttributes(originalAttributes);
|
||||
SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asResponse();
|
||||
System.out.println(response.asString());
|
||||
assertEquals(204, response.getStatus());
|
||||
|
@ -556,6 +550,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
|||
public void testUpdateProfileCannotChangeThroughAttributes() throws IOException {
|
||||
UserRepresentation user = getUser();
|
||||
String originalUsername = user.getUsername();
|
||||
user.setAttributes(Optional.ofNullable(user.getAttributes()).orElse(new HashMap<>()));
|
||||
Map<String, List<String>> originalAttributes = new HashMap<>(user.getAttributes());
|
||||
|
||||
try {
|
||||
|
|
|
@ -29,6 +29,7 @@ import java.util.Collections;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
@ -292,6 +293,7 @@ public class AccountRestServiceWithUserProfileTest extends AccountRestServiceTes
|
|||
String originalFirstName = user.getFirstName();
|
||||
String originalLastName = user.getLastName();
|
||||
String originalEmail = user.getEmail();
|
||||
user.setAttributes(Optional.ofNullable(user.getAttributes()).orElse(new HashMap<>()));
|
||||
Map<String, List<String>> originalAttributes = new HashMap<>(user.getAttributes());
|
||||
|
||||
try {
|
||||
|
@ -303,13 +305,13 @@ public class AccountRestServiceWithUserProfileTest extends AccountRestServiceTes
|
|||
user.setEmail("bobby@localhost");
|
||||
user.setFirstName("Homer");
|
||||
user.setLastName("Simpsons");
|
||||
user.getAttributes().put("attr1", Collections.singletonList("val1"));
|
||||
user.getAttributes().put("attr2", Collections.singletonList("val2"));
|
||||
user.getAttributes().put("attr1", Collections.singletonList("val11"));
|
||||
user.getAttributes().put("attr2", Collections.singletonList("val22"));
|
||||
|
||||
events.clear();
|
||||
user = updateAndGet(user);
|
||||
|
||||
//skip login to the REST API event
|
||||
events.poll();
|
||||
events.expectAccount(EventType.UPDATE_PROFILE).user(user.getId())
|
||||
.detail(Details.CONTEXT, UserProfileContext.ACCOUNT.name())
|
||||
.detail(Details.PREVIOUS_EMAIL, originalEmail)
|
||||
|
@ -318,7 +320,7 @@ public class AccountRestServiceWithUserProfileTest extends AccountRestServiceTes
|
|||
.detail(Details.PREVIOUS_LAST_NAME, originalLastName)
|
||||
.detail(Details.UPDATED_FIRST_NAME, "Homer")
|
||||
.detail(Details.UPDATED_LAST_NAME, "Simpsons")
|
||||
.detail(Details.PREF_UPDATED+"attr2", "val2")
|
||||
.detail(Details.PREF_UPDATED+"attr2", "val22")
|
||||
.assertEvent();
|
||||
events.assertEmpty();
|
||||
|
||||
|
@ -379,22 +381,23 @@ public class AccountRestServiceWithUserProfileTest extends AccountRestServiceTes
|
|||
realmRep.setInternationalizationEnabled(false);
|
||||
testRealm().update(realmRep);
|
||||
UserRepresentation user = getUser();
|
||||
user.setAttributes(Optional.ofNullable(user.getAttributes()).orElse(new HashMap<>()));
|
||||
|
||||
try {
|
||||
user.getAttributes().put(UserModel.LOCALE, List.of("pt_BR"));
|
||||
user = updateAndGet(user);
|
||||
assertNull(user.getAttributes().get(UserModel.LOCALE));
|
||||
assertNull(user.getAttributes());
|
||||
|
||||
realmRep.setInternationalizationEnabled(true);
|
||||
testRealm().update(realmRep);
|
||||
|
||||
user.getAttributes().put(UserModel.LOCALE, List.of("pt_BR"));
|
||||
user.singleAttribute(UserModel.LOCALE, "pt_BR");
|
||||
user = updateAndGet(user);
|
||||
assertEquals("pt_BR", user.getAttributes().get(UserModel.LOCALE).get(0));
|
||||
|
||||
user.getAttributes().remove(UserModel.LOCALE);
|
||||
user = updateAndGet(user);
|
||||
assertNull(user.getAttributes().get(UserModel.LOCALE));
|
||||
assertNull(user.getAttributes());
|
||||
|
||||
UserProfileMetadata metadata = user.getUserProfileMetadata();
|
||||
|
||||
|
@ -406,7 +409,6 @@ public class AccountRestServiceWithUserProfileTest extends AccountRestServiceTes
|
|||
} finally {
|
||||
realmRep.setInternationalizationEnabled(internationalizationEnabled);
|
||||
testRealm().update(realmRep);
|
||||
user.getAttributes().remove(UserModel.LOCALE);
|
||||
updateAndGet(user);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ import org.keycloak.userprofile.UserProfileProvider;
|
|||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -52,7 +53,6 @@ import jakarta.ws.rs.core.Response;
|
|||
@EnableFeature(value = Profile.Feature.DECLARATIVE_USER_PROFILE)
|
||||
public class DeclarativeUserTest extends AbstractAdminTest {
|
||||
|
||||
private static final String LOCALE_ATTR_KEY = "locale";
|
||||
private static final String TEST_REALM_USER_MANAGER_NAME = "test-realm-user-manager";
|
||||
private static final String REQUIRED_ATTR_KEY = "required-attr";
|
||||
|
||||
|
@ -120,29 +120,6 @@ public class DeclarativeUserTest extends AbstractAdminTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReturnAllConfiguredAttributesEvenIfNotSet() {
|
||||
UserRepresentation user1 = new UserRepresentation();
|
||||
user1.setUsername("user1");
|
||||
user1.singleAttribute("attr1", "value1user1");
|
||||
user1.singleAttribute("attr2", "value2user1");
|
||||
String user1Id = createUser(user1);
|
||||
|
||||
user1 = realm.users().get(user1Id).toRepresentation();
|
||||
Map<String, List<String>> attributes = user1.getAttributes();
|
||||
assertEquals(4, attributes.size());
|
||||
List<String> attr1 = attributes.get("attr1");
|
||||
assertEquals(1, attr1.size());
|
||||
assertEquals("value1user1", attr1.get(0));
|
||||
List<String> attr2 = attributes.get("attr2");
|
||||
assertEquals(1, attr2.size());
|
||||
assertEquals("value2user1", attr2.get(0));
|
||||
List<String> attrCustomA = attributes.get("custom-a");
|
||||
assertTrue(attrCustomA.isEmpty());
|
||||
assertTrue(attributes.containsKey("custom-a"));
|
||||
assertTrue(attributes.containsKey("aName"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoNotReturnAttributeIfNotReadble() {
|
||||
UserRepresentation user1 = new UserRepresentation();
|
||||
|
@ -153,7 +130,7 @@ public class DeclarativeUserTest extends AbstractAdminTest {
|
|||
|
||||
user1 = realm.users().get(user1Id).toRepresentation();
|
||||
Map<String, List<String>> attributes = user1.getAttributes();
|
||||
assertEquals(4, attributes.size());
|
||||
assertEquals(2, attributes.size());
|
||||
assertFalse(attributes.containsKey("custom-hidden"));
|
||||
|
||||
setUserProfileConfiguration(this.realm, "{\"attributes\": ["
|
||||
|
@ -170,8 +147,8 @@ public class DeclarativeUserTest extends AbstractAdminTest {
|
|||
|
||||
user1 = realm.users().get(user1Id).toRepresentation();
|
||||
attributes = user1.getAttributes();
|
||||
assertEquals(5, attributes.size());
|
||||
assertTrue(attributes.containsKey("custom-hidden"));
|
||||
assertEquals(2, attributes.size());
|
||||
assertFalse(attributes.containsKey("custom-hidden"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -200,12 +177,17 @@ public class DeclarativeUserTest extends AbstractAdminTest {
|
|||
|
||||
UserResource userResource = realm.users().get(user1Id);
|
||||
user1 = userResource.toRepresentation();
|
||||
Map<String, List<String>> attributes = user1.getAttributes();
|
||||
attributes.put("attr2", Collections.singletonList(""));
|
||||
assertNull(user1.getAttributes());
|
||||
user1.singleAttribute("attr2", "");
|
||||
// should be able to update the user when a read-only attribute has an empty or null value
|
||||
userResource.update(user1);
|
||||
attributes.put("attr2", null);
|
||||
user1 = userResource.toRepresentation();
|
||||
assertNull(user1.getAttributes());
|
||||
user1.setAttributes(new HashMap<>());
|
||||
user1.getAttributes().put("attr2", null);
|
||||
userResource.update(user1);
|
||||
user1 = userResource.toRepresentation();
|
||||
assertNull(user1.getAttributes());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -288,7 +270,7 @@ public class DeclarativeUserTest extends AbstractAdminTest {
|
|||
realm.update(realmRep);
|
||||
|
||||
user1 = userResource.toRepresentation();
|
||||
assertNull(user1.getAttributes().get(UserModel.LOCALE));
|
||||
assertNull(user1.getAttributes());
|
||||
} finally {
|
||||
realmRep.setInternationalizationEnabled(internationalizationEnabled);
|
||||
realm.update(realmRep);
|
||||
|
|
|
@ -1529,20 +1529,12 @@ public class UserTest extends AbstractAdminTest {
|
|||
String user2Id = createUser(user2);
|
||||
|
||||
user1 = realm.users().get(user1Id).toRepresentation();
|
||||
if (isDeclarativeUserProfile()) {
|
||||
assertEquals(managedAttributes.size(), user1.getAttributes().size());
|
||||
} else {
|
||||
assertEquals(2, user1.getAttributes().size());
|
||||
}
|
||||
assertEquals(2, user1.getAttributes().size());
|
||||
assertAttributeValue("value1user1", user1.getAttributes().get("attr1"));
|
||||
assertAttributeValue("value2user1", user1.getAttributes().get("attr2"));
|
||||
|
||||
user2 = realm.users().get(user2Id).toRepresentation();
|
||||
if (isDeclarativeUserProfile()) {
|
||||
assertEquals(managedAttributes.size(), user2.getAttributes().size());
|
||||
} else {
|
||||
assertEquals(2, user2.getAttributes().size());
|
||||
}
|
||||
assertEquals(2, user2.getAttributes().size());
|
||||
assertAttributeValue("value1user2", user2.getAttributes().get("attr1"));
|
||||
vals = user2.getAttributes().get("attr2");
|
||||
assertEquals(2, vals.size());
|
||||
|
@ -1554,11 +1546,7 @@ public class UserTest extends AbstractAdminTest {
|
|||
updateUser(realm.users().get(user1Id), user1);
|
||||
|
||||
user1 = realm.users().get(user1Id).toRepresentation();
|
||||
if (isDeclarativeUserProfile()) {
|
||||
assertEquals(managedAttributes.size(), user1.getAttributes().size());
|
||||
} else {
|
||||
assertEquals(3, user1.getAttributes().size());
|
||||
}
|
||||
assertEquals(3, user1.getAttributes().size());
|
||||
assertAttributeValue("value3user1", user1.getAttributes().get("attr1"));
|
||||
assertAttributeValue("value2user1", user1.getAttributes().get("attr2"));
|
||||
assertAttributeValue("value4user1", user1.getAttributes().get("attr3"));
|
||||
|
@ -1567,11 +1555,7 @@ public class UserTest extends AbstractAdminTest {
|
|||
updateUser(realm.users().get(user1Id), user1);
|
||||
|
||||
user1 = realm.users().get(user1Id).toRepresentation();
|
||||
if (isDeclarativeUserProfile()) {
|
||||
assertEquals(managedAttributes.size(), user1.getAttributes().size());
|
||||
} else {
|
||||
assertEquals(2, user1.getAttributes().size());
|
||||
}
|
||||
assertEquals(2, user1.getAttributes().size());
|
||||
assertAttributeValue("value2user1", user1.getAttributes().get("attr2"));
|
||||
assertAttributeValue("value4user1", user1.getAttributes().get("attr3"));
|
||||
|
||||
|
@ -1580,11 +1564,7 @@ public class UserTest extends AbstractAdminTest {
|
|||
updateUser(realm.users().get(user1Id), user1);
|
||||
user1 = realm.users().get(user1Id).toRepresentation();
|
||||
assertNotNull(user1.getAttributes());
|
||||
if (isDeclarativeUserProfile()) {
|
||||
assertEquals(managedAttributes.size(), user1.getAttributes().size());
|
||||
} else {
|
||||
assertEquals(2, user1.getAttributes().size());
|
||||
}
|
||||
assertEquals(2, user1.getAttributes().size());
|
||||
|
||||
// empty attributes should remove attributes
|
||||
user1.setAttributes(Collections.emptyMap());
|
||||
|
@ -1602,21 +1582,13 @@ public class UserTest extends AbstractAdminTest {
|
|||
|
||||
realm.users().get(user1Id).update(user1);
|
||||
user1 = realm.users().get(user1Id).toRepresentation();
|
||||
if (isDeclarativeUserProfile()) {
|
||||
assertEquals(managedAttributes.size(), user1.getAttributes().size());
|
||||
} else {
|
||||
assertEquals(2, user1.getAttributes().size());
|
||||
}
|
||||
assertEquals(2, user1.getAttributes().size());
|
||||
|
||||
user1.getAttributes().remove("foo");
|
||||
|
||||
realm.users().get(user1Id).update(user1);
|
||||
user1 = realm.users().get(user1Id).toRepresentation();
|
||||
if (isDeclarativeUserProfile()) {
|
||||
assertEquals(managedAttributes.size(), user1.getAttributes().size());
|
||||
} else {
|
||||
assertEquals(1, user1.getAttributes().size());
|
||||
}
|
||||
assertEquals(1, user1.getAttributes().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1669,11 +1641,7 @@ public class UserTest extends AbstractAdminTest {
|
|||
user1 = realm.users().get(user1Id).toRepresentation();
|
||||
assertEquals("foo", user1.getAttributes().get("usercertificate").get(0));
|
||||
assertEquals("bar", user1.getAttributes().get("saml.persistent.name.id.for.foo").get(0));
|
||||
if (isDeclarativeUserProfile()) {
|
||||
assertTrue(user1.getAttributes().get(LDAPConstants.LDAP_ID).isEmpty());
|
||||
} else {
|
||||
assertFalse(user1.getAttributes().containsKey(LDAPConstants.LDAP_ID));
|
||||
}
|
||||
assertFalse(user1.getAttributes().containsKey(LDAPConstants.LDAP_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
|
||||
package org.keycloak.testsuite.federation.ldap;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.keycloak.testsuite.forms.VerifyProfileTest.disableDynamicUserProfile;
|
||||
|
@ -71,14 +73,16 @@ public class LDAPAdminRestApiWithUserProfileTest extends LDAPAdminRestApiTest {
|
|||
|
||||
UserResource user = testRealm().users().get(newUserId);
|
||||
UserRepresentation userRep = user.toRepresentation();
|
||||
|
||||
assertTrue(userRep.getAttributes().containsKey(LDAPConstants.LDAP_ID));
|
||||
assertTrue(userRep.getAttributes().get(LDAPConstants.LDAP_ID).isEmpty());
|
||||
assertNull(userRep.getAttributes());
|
||||
|
||||
userRep.singleAttribute(LDAPConstants.LDAP_ID, "");
|
||||
user.update(userRep);
|
||||
userRep = testRealm().users().get(newUserId).toRepresentation();
|
||||
assertNull(userRep.getAttributes());
|
||||
userRep.singleAttribute(LDAPConstants.LDAP_ID, null);
|
||||
user.update(userRep);
|
||||
userRep = testRealm().users().get(newUserId).toRepresentation();
|
||||
assertNull(userRep.getAttributes());
|
||||
|
||||
try {
|
||||
userRep.singleAttribute(LDAPConstants.LDAP_ID, "should-fail");
|
||||
|
|
|
@ -55,6 +55,7 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.idm.AbstractUserRepresentation;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
|
@ -114,6 +115,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
// create a user with attribute foo value 123 allowed by the profile now but disallowed later
|
||||
UPConfig config = parseDefaultConfig();
|
||||
config.addOrReplaceAttribute(new UPAttribute("foo", new UPAttributePermissions(Set.of(), Set.of(ROLE_ADMIN))));
|
||||
config.getAttribute(UserModel.EMAIL).setPermissions(new UPAttributePermissions(Set.of(ROLE_USER), Set.of(ROLE_ADMIN)));
|
||||
RealmResource realmRes = testRealm();
|
||||
realmRes.users().userProfile().update(config);
|
||||
|
||||
|
@ -142,6 +144,18 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
profile.validate();
|
||||
});
|
||||
|
||||
// it should work if foo is read-only in the context
|
||||
getTestingClient().server(TEST_REALM_NAME).run(session -> {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
UserModel user = session.users().getUserById(realm, userId);
|
||||
user.setEmail(null);
|
||||
UserProfileProvider provider = getUserProfileProvider(session);
|
||||
Map<String, Object> attributes = new HashMap<>(user.getAttributes());
|
||||
attributes.put("email", "");
|
||||
UserProfile profile = provider.create(UserProfileContext.ACCOUNT, attributes, user);
|
||||
profile.validate();
|
||||
});
|
||||
|
||||
// it should fail if foo can be modified
|
||||
getTestingClient().server(TEST_REALM_NAME).run(session -> {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
|
@ -174,7 +188,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
|
||||
// once created, profile attributes can not be changed
|
||||
assertTrue(profile.getAttributes().contains(UserModel.USERNAME));
|
||||
assertNull(profile.getAttributes().getFirstValue(UserModel.USERNAME));
|
||||
assertNull(profile.getAttributes().getFirst(UserModel.USERNAME));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -413,11 +427,11 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
assertTrue(ve.isAttributeOnError("address"));
|
||||
}
|
||||
|
||||
assertNotNull(attributes.getFirstValue(UserModel.USERNAME));
|
||||
assertNotNull(attributes.getFirstValue(UserModel.EMAIL));
|
||||
assertNotNull(attributes.getFirstValue(UserModel.FIRST_NAME));
|
||||
assertNotNull(attributes.getFirstValue(UserModel.LAST_NAME));
|
||||
assertNull(attributes.getFirstValue("address"));
|
||||
assertNotNull(attributes.getFirst(UserModel.USERNAME));
|
||||
assertNotNull(attributes.getFirst(UserModel.EMAIL));
|
||||
assertNotNull(attributes.getFirst(UserModel.FIRST_NAME));
|
||||
assertNotNull(attributes.getFirst(UserModel.LAST_NAME));
|
||||
assertNull(attributes.getFirst("address"));
|
||||
|
||||
user.setAttribute("address", Arrays.asList("fixed-address"));
|
||||
|
||||
|
@ -426,7 +440,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
|
||||
profile.validate();
|
||||
|
||||
assertNotNull(attributes.getFirstValue("address"));
|
||||
assertNotNull(attributes.getFirst("address"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1516,20 +1530,20 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
|
||||
profile = provider.create(UserProfileContext.USER_API, user);
|
||||
Attributes userAttributes = profile.getAttributes();
|
||||
assertEquals("new-email@test.com", userAttributes.getFirstValue(UserModel.EMAIL));
|
||||
assertEquals("Test Value", userAttributes.getFirstValue("test-attribute"));
|
||||
assertEquals("changed", userAttributes.getFirstValue("foo"));
|
||||
assertEquals("new-email@test.com", userAttributes.getFirst(UserModel.EMAIL));
|
||||
assertEquals("Test Value", userAttributes.getFirst("test-attribute"));
|
||||
assertEquals("changed", userAttributes.getFirst("foo"));
|
||||
|
||||
attributes.remove("foo");
|
||||
attributes.put("test-attribute", userAttributes.getFirstValue("test-attribute"));
|
||||
attributes.put("test-attribute", userAttributes.getFirst("test-attribute"));
|
||||
profile = provider.create(UserProfileContext.USER_API, attributes, user);
|
||||
profile.update(true);
|
||||
profile = provider.create(UserProfileContext.USER_API, user);
|
||||
userAttributes = profile.getAttributes();
|
||||
// remove attribute if not set
|
||||
assertEquals("new-email@test.com", userAttributes.getFirstValue(UserModel.EMAIL));
|
||||
assertEquals("Test Value", userAttributes.getFirstValue("test-attribute"));
|
||||
assertNull(userAttributes.getFirstValue("foo"));
|
||||
assertEquals("new-email@test.com", userAttributes.getFirst(UserModel.EMAIL));
|
||||
assertEquals("Test Value", userAttributes.getFirst("test-attribute"));
|
||||
assertNull(userAttributes.getFirst("foo"));
|
||||
|
||||
config.addOrReplaceAttribute(new UPAttribute("test-attribute", new UPAttributePermissions(Set.of(), Set.of(ROLE_USER))));
|
||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||
|
@ -1539,8 +1553,8 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
profile = provider.create(UserProfileContext.USER_API, user);
|
||||
userAttributes = profile.getAttributes();
|
||||
// do not remove test-attribute because admin does not have write permissions
|
||||
assertEquals("new-email@test.com", userAttributes.getFirstValue(UserModel.EMAIL));
|
||||
assertEquals("Test Value", userAttributes.getFirstValue("test-attribute"));
|
||||
assertEquals("new-email@test.com", userAttributes.getFirst(UserModel.EMAIL));
|
||||
assertEquals("Test Value", userAttributes.getFirst("test-attribute"));
|
||||
|
||||
config.addOrReplaceAttribute(new UPAttribute("test-attribute", new UPAttributePermissions(Set.of(), Set.of(ROLE_USER, ROLE_ADMIN))));
|
||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||
|
@ -1550,8 +1564,8 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
profile = provider.create(UserProfileContext.USER_API, user);
|
||||
userAttributes = profile.getAttributes();
|
||||
// removes the test-attribute attribute because now admin has write permission
|
||||
assertEquals("new-email@test.com", userAttributes.getFirstValue(UserModel.EMAIL));
|
||||
assertNull(userAttributes.getFirstValue("test-attribute"));
|
||||
assertEquals("new-email@test.com", userAttributes.getFirst(UserModel.EMAIL));
|
||||
assertNull(userAttributes.getFirst("test-attribute"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1594,11 +1608,11 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
}
|
||||
|
||||
private static void assertRemoveEmptyRootAttribute(Map<String, List<String>> attributes, UserModel user, Attributes upAttributes) {
|
||||
assertNull(upAttributes.getFirstValue(UserModel.LAST_NAME));
|
||||
assertNull(upAttributes.getFirst(UserModel.LAST_NAME));
|
||||
assertNull(user.getLastName());
|
||||
assertNull(upAttributes.getFirstValue(UserModel.EMAIL));
|
||||
assertNull(upAttributes.getFirst(UserModel.EMAIL));
|
||||
assertNull(user.getEmail());
|
||||
assertEquals(upAttributes.getFirstValue(UserModel.FIRST_NAME), attributes.get(UserModel.FIRST_NAME).get(0));
|
||||
assertEquals(upAttributes.getFirst(UserModel.FIRST_NAME), attributes.get(UserModel.FIRST_NAME).get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1693,6 +1707,77 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
assertFalse(profile.getAttributes().isReadOnly("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOptionalRootAttributesAsUnmanagedAttribute() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testOptionalRootAttributesAsUnmanagedAttribute);
|
||||
}
|
||||
|
||||
private static void testOptionalRootAttributesAsUnmanagedAttribute(KeycloakSession session) throws IOException {
|
||||
UPConfig config = parseDefaultConfig();
|
||||
UserProfileProvider provider = getUserProfileProvider(session);
|
||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||
Map<String, String> rawAttributes = new HashMap<>();
|
||||
rawAttributes.put(UserModel.USERNAME, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org");
|
||||
rawAttributes.put(UserModel.EMAIL, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org");
|
||||
rawAttributes.put(UserModel.FIRST_NAME, "firstName");
|
||||
rawAttributes.put(UserModel.LAST_NAME, "lastName");
|
||||
UserProfile profile = provider.create(UserProfileContext.USER_API, rawAttributes);
|
||||
UserModel user = profile.create();
|
||||
assertEquals(rawAttributes.get(UserModel.FIRST_NAME), user.getFirstName());
|
||||
assertEquals(rawAttributes.get(UserModel.LAST_NAME), user.getLastName());
|
||||
AbstractUserRepresentation rep = profile.toRepresentation();
|
||||
assertEquals(rawAttributes.get(UserModel.FIRST_NAME), rep.getFirstName());
|
||||
assertEquals(rawAttributes.get(UserModel.LAST_NAME), rep.getLastName());
|
||||
assertNull(rep.getAttributes());
|
||||
|
||||
config.removeAttribute(UserModel.FIRST_NAME);
|
||||
config.removeAttribute(UserModel.LAST_NAME);
|
||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||
profile = provider.create(UserProfileContext.USER_API, user);
|
||||
Attributes attributes = profile.getAttributes();
|
||||
assertNull(attributes.getFirst(UserModel.FIRST_NAME));
|
||||
assertNull(attributes.getFirst(UserModel.LAST_NAME));
|
||||
rep = profile.toRepresentation();
|
||||
assertNull(rep.getFirstName());
|
||||
assertNull(rep.getLastName());
|
||||
assertNull(rep.getAttributes());
|
||||
|
||||
rawAttributes.put(UserModel.FIRST_NAME, "firstName");
|
||||
rawAttributes.put(UserModel.LAST_NAME, "lastName");
|
||||
config.setUnmanagedAttributePolicy(UnmanagedAttributePolicy.ADMIN_EDIT);
|
||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||
profile = provider.create(UserProfileContext.USER_API, user);
|
||||
attributes = profile.getAttributes();
|
||||
assertEquals(rawAttributes.get(UserModel.FIRST_NAME), attributes.getFirst(UserModel.FIRST_NAME));
|
||||
assertEquals(rawAttributes.get(UserModel.LAST_NAME), attributes.getFirst(UserModel.LAST_NAME));
|
||||
rep = profile.toRepresentation();
|
||||
assertNull(rep.getFirstName());
|
||||
assertNull(rep.getLastName());
|
||||
assertNull(rep.getAttributes());
|
||||
|
||||
rawAttributes.remove(UserModel.LAST_NAME);
|
||||
rawAttributes.put(UserModel.FIRST_NAME, "firstName");
|
||||
profile = provider.create(UserProfileContext.USER_API, rawAttributes, user);
|
||||
attributes = profile.getAttributes();
|
||||
assertEquals(rawAttributes.get(UserModel.FIRST_NAME), attributes.getFirst(UserModel.FIRST_NAME));
|
||||
assertNull(attributes.getFirst(UserModel.LAST_NAME));
|
||||
rep = profile.toRepresentation();
|
||||
assertNull(rep.getFirstName());
|
||||
assertNull(rep.getLastName());
|
||||
assertNull(rep.getAttributes());
|
||||
|
||||
rawAttributes.put(UserModel.LAST_NAME, "lastNameChanged");
|
||||
rawAttributes.put(UserModel.FIRST_NAME, "firstNameChanged");
|
||||
profile = provider.create(UserProfileContext.USER_API, rawAttributes, user);
|
||||
attributes = profile.getAttributes();
|
||||
assertEquals(rawAttributes.get(UserModel.FIRST_NAME), attributes.getFirst(UserModel.FIRST_NAME));
|
||||
assertEquals(rawAttributes.get(UserModel.LAST_NAME), attributes.getFirst(UserModel.LAST_NAME));
|
||||
rep = profile.toRepresentation();
|
||||
assertNull(rep.getFirstName());
|
||||
assertNull(rep.getLastName());
|
||||
assertNull(rep.getAttributes());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAttributeNormalization() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testAttributeNormalization);
|
||||
|
@ -1705,7 +1790,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
|||
attributes.put(UserModel.EMAIL, "TesT@TesT.org");
|
||||
UserProfile profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||
Attributes profileAttributes = profile.getAttributes();
|
||||
assertEquals(attributes.get(UserModel.USERNAME).toLowerCase(), profileAttributes.getFirstValue(UserModel.USERNAME));
|
||||
assertEquals(attributes.get(UserModel.EMAIL).toLowerCase(), profileAttributes.getFirstValue(UserModel.EMAIL));
|
||||
assertEquals(attributes.get(UserModel.USERNAME).toLowerCase(), profileAttributes.getFirst(UserModel.USERNAME));
|
||||
assertEquals(attributes.get(UserModel.EMAIL).toLowerCase(), profileAttributes.getFirst(UserModel.EMAIL));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue