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;
|
package org.keycloak.representations.account;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
import org.keycloak.representations.idm.AbstractUserRepresentation;
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @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;
|
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.List;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ -32,24 +25,16 @@ import java.util.Set;
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class UserRepresentation {
|
public class UserRepresentation extends AbstractUserRepresentation{
|
||||||
|
|
||||||
protected String self; // link
|
protected String self; // link
|
||||||
protected String id;
|
|
||||||
protected String origin;
|
protected String origin;
|
||||||
protected Long createdTimestamp;
|
protected Long createdTimestamp;
|
||||||
protected String username;
|
|
||||||
protected Boolean enabled;
|
protected Boolean enabled;
|
||||||
protected Boolean totp;
|
protected Boolean totp;
|
||||||
protected Boolean emailVerified;
|
|
||||||
protected String firstName;
|
|
||||||
protected String lastName;
|
|
||||||
protected String email;
|
|
||||||
protected String federationLink;
|
protected String federationLink;
|
||||||
protected String serviceAccountClientId; // For rep, it points to clientId (not DB ID)
|
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 List<CredentialRepresentation> credentials;
|
||||||
protected Set<String> disableableCredentialTypes;
|
protected Set<String> disableableCredentialTypes;
|
||||||
protected List<String> requiredActions;
|
protected List<String> requiredActions;
|
||||||
|
@ -66,7 +51,6 @@ public class UserRepresentation {
|
||||||
|
|
||||||
protected List<String> groups;
|
protected List<String> groups;
|
||||||
private Map<String, Boolean> access;
|
private Map<String, Boolean> access;
|
||||||
private UserProfileMetadata userProfileMetadata;
|
|
||||||
|
|
||||||
public String getSelf() {
|
public String getSelf() {
|
||||||
return self;
|
return self;
|
||||||
|
@ -76,14 +60,6 @@ public class UserRepresentation {
|
||||||
this.self = self;
|
this.self = self;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setId(String id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getCreatedTimestamp() {
|
public Long getCreatedTimestamp() {
|
||||||
return createdTimestamp;
|
return createdTimestamp;
|
||||||
}
|
}
|
||||||
|
@ -92,38 +68,6 @@ public class UserRepresentation {
|
||||||
this.createdTimestamp = createdTimestamp;
|
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() {
|
public Boolean isEnabled() {
|
||||||
return enabled;
|
return enabled;
|
||||||
}
|
}
|
||||||
|
@ -142,32 +86,6 @@ public class UserRepresentation {
|
||||||
this.totp = totp;
|
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() {
|
public List<CredentialRepresentation> getCredentials() {
|
||||||
return credentials;
|
return credentials;
|
||||||
}
|
}
|
||||||
|
@ -289,36 +207,4 @@ public class UserRepresentation {
|
||||||
public void setAccess(Map<String, Boolean> access) {
|
public void setAccess(Map<String, Boolean> access) {
|
||||||
this.access = access;
|
this.access = access;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, List<String>> toAttributes() {
|
|
||||||
Map<String, List<String>> attrs = new HashMap<>();
|
|
||||||
|
|
||||||
if (getAttributes() != null) attrs.putAll(getAttributes());
|
|
||||||
|
|
||||||
if (getUsername() != null)
|
|
||||||
attrs.put("username", Collections.singletonList(getUsername()));
|
|
||||||
else
|
|
||||||
attrs.remove("username");
|
|
||||||
|
|
||||||
if (getEmail() != null)
|
|
||||||
attrs.put("email", Collections.singletonList(getEmail()));
|
|
||||||
else
|
|
||||||
attrs.remove("email");
|
|
||||||
|
|
||||||
if (getLastName() != null)
|
|
||||||
attrs.put("lastName", Collections.singletonList(getLastName()));
|
|
||||||
|
|
||||||
if (getFirstName() != null)
|
|
||||||
attrs.put("firstName", Collections.singletonList(getFirstName()));
|
|
||||||
|
|
||||||
return attrs;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 class UPConfig {
|
||||||
|
|
||||||
public enum UnmanagedAttributePolicy {
|
public enum UnmanagedAttributePolicy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unmanaged attributes are enabled and available from any context.
|
||||||
|
*/
|
||||||
ENABLED,
|
ENABLED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unmanaged attributes are only available as read-only and only through the management interfaces.
|
||||||
|
*/
|
||||||
ADMIN_VIEW,
|
ADMIN_VIEW,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unmanaged attributes are only available as read-write and only through the management interfaces.
|
||||||
|
*/
|
||||||
ADMIN_EDIT
|
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.
|
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.
|
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 `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.
|
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;
|
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 static org.keycloak.userprofile.UserProfileContext.USER_API;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
|
@ -18,6 +22,7 @@ import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||||
import org.keycloak.userprofile.UserProfile;
|
import org.keycloak.userprofile.UserProfile;
|
||||||
import org.keycloak.userprofile.UserProfileProvider;
|
import org.keycloak.userprofile.UserProfileProvider;
|
||||||
|
import org.keycloak.utils.StringUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
@ -42,7 +47,7 @@ public class UserResource {
|
||||||
|
|
||||||
if (provider.isEnabled(realm)) {
|
if (provider.isEnabled(realm)) {
|
||||||
UserProfile profile = provider.create(USER_API, user);
|
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());
|
Map<String, List<String>> attributes = new HashMap<>(user.getAttributes());
|
||||||
UPConfig upConfig = provider.getConfiguration();
|
UPConfig upConfig = provider.getConfiguration();
|
||||||
|
|
||||||
|
@ -56,10 +61,10 @@ public class UserResource {
|
||||||
|
|
||||||
attributes.remove(UserModel.USERNAME);
|
attributes.remove(UserModel.USERNAME);
|
||||||
attributes.remove(UserModel.EMAIL);
|
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();
|
return Collections.emptyMap();
|
||||||
|
|
|
@ -19,6 +19,8 @@
|
||||||
|
|
||||||
package org.keycloak.userprofile;
|
package org.keycloak.userprofile;
|
||||||
|
|
||||||
|
import static java.util.Collections.emptyList;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -29,6 +31,8 @@ import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.common.util.CollectionUtil;
|
import org.keycloak.common.util.CollectionUtil;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
|
@ -38,6 +42,7 @@ import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||||
import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy;
|
import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy;
|
||||||
|
import org.keycloak.utils.StringUtil;
|
||||||
import org.keycloak.validate.ValidationContext;
|
import org.keycloak.validate.ValidationContext;
|
||||||
import org.keycloak.validate.ValidationError;
|
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 Map<String, AttributeMetadata> metadataByAttribute;
|
||||||
private final UPConfig upConfig;
|
private final UPConfig upConfig;
|
||||||
protected final UserModel user;
|
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,
|
public DefaultAttributes(UserProfileContext context, Map<String, ?> attributes, UserModel user,
|
||||||
UserProfileMetadata profileMetadata,
|
UserProfileMetadata profileMetadata,
|
||||||
|
@ -82,28 +87,28 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isReadOnly(String attributeName) {
|
public boolean isReadOnly(String name) {
|
||||||
if (!isManagedAttribute(attributeName)) {
|
if (!isManagedAttribute(name)) {
|
||||||
return !isAllowEditUnmanagedAttribute();
|
return !isAllowEditUnmanagedAttribute();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (UserModel.USERNAME.equals(attributeName)) {
|
if (UserModel.USERNAME.equals(name)) {
|
||||||
if (isServiceAccountUser()) {
|
if (isServiceAccountUser()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (UserModel.EMAIL.equals(attributeName)) {
|
if (UserModel.EMAIL.equals(name)) {
|
||||||
if (isServiceAccountUser()) {
|
if (isServiceAccountUser()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isReadOnlyFromMetadata(attributeName) || isReadOnlyInternalAttribute(attributeName)) {
|
if (isReadOnlyFromMetadata(name) || isReadOnlyInternalAttribute(name)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getMetadata(attributeName) == null;
|
return getMetadata(name) == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isAllowEditUnmanagedAttribute() {
|
private boolean isAllowEditUnmanagedAttribute() {
|
||||||
|
@ -156,9 +161,9 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
||||||
List<AttributeMetadata> metadatas = new ArrayList<>();
|
List<AttributeMetadata> metadatas = new ArrayList<>();
|
||||||
|
|
||||||
metadatas.addAll(Optional.ofNullable(this.metadataByAttribute.get(attribute.getKey()))
|
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))
|
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;
|
Boolean result = null;
|
||||||
|
|
||||||
|
@ -172,12 +177,15 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user != null && metadata.isReadOnly(attributeContext)
|
if (user != null && metadata.isReadOnly(attributeContext)) {
|
||||||
&& CollectionUtil.collectionEquals(user.getAttributeStream(name).collect(Collectors.toList()), attribute.getValue())) {
|
List<String> value = user.getAttributeStream(name).filter(StringUtil::isNotBlank).collect(Collectors.toList());
|
||||||
// allow update if the value was already wrong in the user and is read-only in this context
|
List<String> newValue = attribute.getValue().stream().filter(StringUtil::isNotBlank).collect(Collectors.toList());
|
||||||
logger.warnf("User '%s' attribute '%s' has previous validation errors %s but is read-only in context %s.",
|
if (CollectionUtil.collectionEquals(value, newValue)) {
|
||||||
user.getUsername(), name, vc.getErrors(), attributeContext.getContext());
|
// allow update if the value was already wrong in the user and is read-only in this context
|
||||||
continue;
|
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) {
|
if (result == null) {
|
||||||
|
@ -198,7 +206,7 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> getValues(String name) {
|
public List<String> get(String name) {
|
||||||
return getOrDefault(name, EMPTY_VALUE);
|
return getOrDefault(name, EMPTY_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,21 +244,7 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
||||||
@Override
|
@Override
|
||||||
public AttributeMetadata getMetadata(String name) {
|
public AttributeMetadata getMetadata(String name) {
|
||||||
if (unmanagedAttributes.containsKey(name)) {
|
if (unmanagedAttributes.containsKey(name)) {
|
||||||
return new AttributeMetadata(name, Integer.MAX_VALUE) {
|
return createUnmanagedAttributeMetadata(name);
|
||||||
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 Optional.ofNullable(metadataByAttribute.get(name))
|
return Optional.ofNullable(metadataByAttribute.get(name))
|
||||||
|
@ -265,9 +259,14 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
||||||
for (String name : nameSet()) {
|
for (String name : nameSet()) {
|
||||||
AttributeMetadata metadata = getMetadata(name);
|
AttributeMetadata metadata = getMetadata(name);
|
||||||
|
|
||||||
if (metadata == null
|
if (metadata == null) {
|
||||||
|| !metadata.canView(createAttributeContext(metadata))
|
attributes.remove(name);
|
||||||
|| !metadata.isSelected(createAttributeContext(metadata))) {
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
AttributeContext attributeContext = createAttributeContext(metadata);
|
||||||
|
|
||||||
|
if (!metadata.canView(attributeContext) || !metadata.isSelected(attributeContext)) {
|
||||||
attributes.remove(name);
|
attributes.remove(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -277,7 +276,7 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, List<String>> toMap() {
|
public Map<String, List<String>> toMap() {
|
||||||
return this;
|
return Collections.unmodifiableMap(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean isServiceAccountUser() {
|
protected boolean isServiceAccountUser() {
|
||||||
|
@ -342,7 +341,7 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
||||||
|
|
||||||
if (!isSupportedAttribute(key)) {
|
if (!isSupportedAttribute(key)) {
|
||||||
if (!isManagedAttribute(key) && isAllowUnmanagedAttribute()) {
|
if (!isManagedAttribute(key) && isAllowUnmanagedAttribute()) {
|
||||||
unmanagedAttributes.put(key, (List<String>) entry.getValue());
|
unmanagedAttributes.put(key, normalizeAttributeValues(key, entry.getValue()));
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -351,18 +350,7 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
||||||
key = key.substring(Constants.USER_ATTRIBUTES_PREFIX.length());
|
key = key.substring(Constants.USER_ATTRIBUTES_PREFIX.length());
|
||||||
}
|
}
|
||||||
|
|
||||||
Object value = entry.getValue();
|
List<String> values = normalizeAttributeValues(key, 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());
|
|
||||||
}
|
|
||||||
|
|
||||||
newAttributes.put(key, Collections.unmodifiableList(values));
|
newAttributes.put(key, Collections.unmodifiableList(values));
|
||||||
}
|
}
|
||||||
|
@ -378,28 +366,24 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
||||||
AttributeMetadata metadata = metadataByAttribute.get(attributeName);
|
AttributeMetadata metadata = metadataByAttribute.get(attributeName);
|
||||||
|
|
||||||
if (user != null && isIncludeAttributeIfNotProvided(metadata)) {
|
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);
|
newAttributes.put(attributeName, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user != null) {
|
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)) {
|
if (username.isEmpty() && isReadOnly(UserModel.USERNAME)) {
|
||||||
setUserName(newAttributes, Collections.singletonList(user.getUsername()));
|
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()) {
|
if (!email.isEmpty() && realm.isRegistrationEmailAsUsername()) {
|
||||||
List<String> lowerCaseEmailList = email.stream()
|
setUserName(newAttributes, email);
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
setUserName(newAttributes, lowerCaseEmailList);
|
|
||||||
|
|
||||||
if (user != null && isReadOnly(UserModel.EMAIL)) {
|
if (user != null && isReadOnly(UserModel.EMAIL)) {
|
||||||
newAttributes.put(UserModel.EMAIL, Collections.singletonList(user.getEmail()));
|
newAttributes.put(UserModel.EMAIL, Collections.singletonList(user.getEmail()));
|
||||||
|
@ -414,6 +398,24 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
||||||
return newAttributes;
|
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() {
|
private boolean isAllowUnmanagedAttribute() {
|
||||||
UnmanagedAttributePolicy unmanagedAttributePolicy = upConfig.getUnmanagedAttributePolicy();
|
UnmanagedAttributePolicy unmanagedAttributePolicy = upConfig.getUnmanagedAttributePolicy();
|
||||||
|
|
||||||
|
@ -466,12 +468,7 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isReadOnlyInternalAttribute(name)) {
|
return isReadOnlyInternalAttribute(name);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// checks whether the attribute is a core attribute
|
|
||||||
return isRootAttribute(name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isManagedAttribute(String 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() {
|
public Map<String, List<String>> getUnmanagedAttributes() {
|
||||||
return unmanagedAttributes;
|
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;
|
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.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -31,7 +35,10 @@ import java.util.stream.Collectors;
|
||||||
import org.keycloak.common.util.CollectionUtil;
|
import org.keycloak.common.util.CollectionUtil;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.ModelException;
|
import org.keycloak.models.ModelException;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
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.storage.ReadOnlyException;
|
||||||
import org.keycloak.utils.StringUtil;
|
import org.keycloak.utils.StringUtil;
|
||||||
|
|
||||||
|
@ -45,10 +52,11 @@ import org.keycloak.utils.StringUtil;
|
||||||
*/
|
*/
|
||||||
public final class DefaultUserProfile implements UserProfile {
|
public final class DefaultUserProfile implements UserProfile {
|
||||||
|
|
||||||
protected final UserProfileMetadata metadata;
|
private final UserProfileMetadata metadata;
|
||||||
private final Function<Attributes, UserModel> userSupplier;
|
private final Function<Attributes, UserModel> userSupplier;
|
||||||
private final Attributes attributes;
|
private final Attributes attributes;
|
||||||
private final KeycloakSession session;
|
private final KeycloakSession session;
|
||||||
|
private final boolean isUserProfileEnabled;
|
||||||
private boolean validated;
|
private boolean validated;
|
||||||
private UserModel user;
|
private UserModel user;
|
||||||
|
|
||||||
|
@ -59,6 +67,8 @@ public final class DefaultUserProfile implements UserProfile {
|
||||||
this.attributes = attributes;
|
this.attributes = attributes;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.session = session;
|
this.session = session;
|
||||||
|
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||||
|
isUserProfileEnabled = provider.isEnabled(session.getContext().getRealm());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -144,16 +154,27 @@ public final class DefaultUserProfile implements UserProfile {
|
||||||
|
|
||||||
attrsToRemove.removeAll(attributes.nameSet());
|
attrsToRemove.removeAll(attributes.nameSet());
|
||||||
|
|
||||||
for (String attr : attrsToRemove) {
|
for (String name : attrsToRemove) {
|
||||||
if (attributes.isReadOnly(attr)) {
|
if (attributes.isReadOnly(name)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> currentValue = user.getAttributeStream(attr).filter(Objects::nonNull).collect(Collectors.toList());
|
List<String> currentValue = user.getAttributeStream(name).filter(Objects::nonNull).collect(Collectors.toList());
|
||||||
user.removeAttribute(attr);
|
|
||||||
|
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) {
|
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) {
|
private boolean isCustomAttribute(String name) {
|
||||||
return !getAttributes().isRootAttribute(name);
|
return !isRootAttribute(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Attributes getAttributes() {
|
public Attributes getAttributes() {
|
||||||
return attributes;
|
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;
|
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.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.
|
* <p>A {@code UserProfile} provides methods for creating, and updating users as well as for accessing their attributes.
|
||||||
* The context represents the different places in Keycloak where users are created, updated, or validated.
|
* 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.
|
* 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
|
* <p>A {@code UserProfile} instance can be obtained through the {@link UserProfileProvider}:
|
||||||
* 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}.
|
* <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 UserProfileContext
|
||||||
* @see UserProfileProvider
|
* @see UserProfileProvider
|
||||||
|
@ -69,20 +85,23 @@ public interface UserProfile {
|
||||||
void update(boolean removeAttributes, AttributeChangeListener... changeListener) throws ValidationException;
|
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
|
* @param changeListener a set of one or more listeners to listen for attribute changes
|
||||||
* @throws ValidationException in case of any validation error
|
* @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);
|
update(true, changeListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the attributes associated with this instance. Note that the attributes returned by this method are not necessarily
|
* 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.
|
* @return the attributes associated with this instance.
|
||||||
*/
|
*/
|
||||||
Attributes getAttributes();
|
Attributes getAttributes();
|
||||||
|
|
||||||
|
<R extends AbstractUserRepresentation> R toRepresentation();
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,11 +20,23 @@
|
||||||
package org.keycloak.userprofile;
|
package org.keycloak.userprofile;
|
||||||
|
|
||||||
import java.util.Collections;
|
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.function.Predicate;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.models.KeycloakSession;
|
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.UPConfig;
|
||||||
|
import org.keycloak.representations.userprofile.config.UPGroup;
|
||||||
|
import org.keycloak.validate.Validators;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
@ -82,4 +94,70 @@ public class UserProfileUtil {
|
||||||
return true;
|
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;
|
package org.keycloak.userprofile;
|
||||||
|
|
||||||
|
import static java.util.Optional.ofNullable;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.keycloak.models.UserModel;
|
|
||||||
import org.keycloak.validate.ValidationError;
|
import org.keycloak.validate.ValidationError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>This interface wraps the attributes associated with a user profile. Different operations are provided to access and
|
* <p>This interface wraps the attributes associated with a user profile. Different operations are provided to access and
|
||||||
* manage these attributes.
|
* 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>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
*/
|
*/
|
||||||
public interface Attributes {
|
public interface Attributes {
|
||||||
|
@ -49,8 +67,8 @@ public interface Attributes {
|
||||||
*
|
*
|
||||||
* @return the first value
|
* @return the first value
|
||||||
*/
|
*/
|
||||||
default String getFirstValue(String name) {
|
default String getFirst(String name) {
|
||||||
List<String> values = getValues(name);
|
List<String> values = ofNullable(get(name)).orElse(List.of());
|
||||||
|
|
||||||
if (values.isEmpty()) {
|
if (values.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -66,16 +84,16 @@ public interface Attributes {
|
||||||
*
|
*
|
||||||
* @return the attribute values
|
* @return the attribute values
|
||||||
*/
|
*/
|
||||||
List<String> getValues(String name);
|
List<String> get(String name);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether an attribute is read-only.
|
* 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}.
|
* Validates the attribute with the given {@code name}.
|
||||||
|
@ -105,7 +123,7 @@ public interface Attributes {
|
||||||
Set<String> nameSet();
|
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
|
* @return the attributes
|
||||||
*/
|
*/
|
||||||
|
@ -131,52 +149,23 @@ public interface Attributes {
|
||||||
boolean isRequired(String name);
|
boolean isRequired(String name);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Similar to {{@link #getReadable(boolean)}} but with the possibility to add or remove
|
* Returns only the attributes that have read permissions in a particular {@link UserProfileContext}.
|
||||||
* the root attributes.
|
|
||||||
*
|
*
|
||||||
* @param includeBuiltin if the root attributes should be included.
|
* @return the attributes with read permission.
|
||||||
* @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.
|
|
||||||
*/
|
*/
|
||||||
Map<String, List<String>> getReadable();
|
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 a map with all the attributes
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
default boolean isRootAttribute(String name) {
|
|
||||||
return UserModel.USERNAME.equals(name)
|
|
||||||
|| UserModel.EMAIL.equals(name)
|
|
||||||
|| UserModel.FIRST_NAME.equals(name)
|
|
||||||
|| UserModel.LAST_NAME.equals(name)
|
|
||||||
|| UserModel.LOCALE.equals(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, List<String>> toMap();
|
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();
|
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
|
* <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
|
* <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.
|
* 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 {
|
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),
|
UPDATE_PROFILE(true),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In this context, a user profile is managed through the management interface such as the Admin API.
|
||||||
|
*/
|
||||||
USER_API(false),
|
USER_API(false),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In this context, a user profile is managed by themselves through the account console.
|
||||||
|
*/
|
||||||
ACCOUNT(true),
|
ACCOUNT(true),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In this context, a user profile is managed by themselves when authenticating through a broker.
|
||||||
|
*/
|
||||||
IDP_REVIEW(false),
|
IDP_REVIEW(false),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In this context, a user profile is managed by themselves when registering to a realm.
|
||||||
|
*/
|
||||||
REGISTRATION(false),
|
REGISTRATION(false),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In this context, a user profile is managed by themselves when updating their email through an application initiated action.
|
||||||
|
*/
|
||||||
UPDATE_EMAIL(false);
|
UPDATE_EMAIL(false);
|
||||||
|
|
||||||
protected boolean resetEmailVerified;
|
private boolean resetEmailVerified;
|
||||||
|
|
||||||
private UserProfileContext(boolean resetEmailVerified){
|
UserProfileContext(boolean resetEmailVerified){
|
||||||
this.resetEmailVerified = resetEmailVerified;
|
this.resetEmailVerified = resetEmailVerified;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -166,7 +166,7 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
|
||||||
profile.update((attributeName, userModel, oldValue) -> {
|
profile.update((attributeName, userModel, oldValue) -> {
|
||||||
if (attributeName.equals(UserModel.EMAIL)) {
|
if (attributeName.equals(UserModel.EMAIL)) {
|
||||||
context.getAuthenticationSession().setAuthNote(UPDATE_PROFILE_EMAIL_CHANGED, "true");
|
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) {
|
} 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());
|
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);
|
event.detail(Details.UPDATED_EMAIL, newEmail);
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,7 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.validation.Validation;
|
import org.keycloak.services.validation.Validation;
|
||||||
|
import org.keycloak.userprofile.Attributes;
|
||||||
import org.keycloak.userprofile.UserProfileContext;
|
import org.keycloak.userprofile.UserProfileContext;
|
||||||
import org.keycloak.userprofile.UserProfileProvider;
|
import org.keycloak.userprofile.UserProfileProvider;
|
||||||
import org.keycloak.userprofile.ValidationException;
|
import org.keycloak.userprofile.ValidationException;
|
||||||
|
@ -71,11 +72,11 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
|
||||||
context.getEvent().detail(Details.REGISTER_METHOD, "form");
|
context.getEvent().detail(Details.REGISTER_METHOD, "form");
|
||||||
|
|
||||||
UserProfile profile = getOrCreateUserProfile(context, formData);
|
UserProfile profile = getOrCreateUserProfile(context, formData);
|
||||||
String email = profile.getAttributes().getFirstValue(UserModel.EMAIL);
|
Attributes attributes = profile.getAttributes();
|
||||||
|
String email = attributes.getFirst(UserModel.EMAIL);
|
||||||
String username = profile.getAttributes().getFirstValue(UserModel.USERNAME);
|
String username = attributes.getFirst(UserModel.USERNAME);
|
||||||
String firstName = profile.getAttributes().getFirstValue(UserModel.FIRST_NAME);
|
String firstName = attributes.getFirst(UserModel.FIRST_NAME);
|
||||||
String lastName = profile.getAttributes().getFirstValue(UserModel.LAST_NAME);
|
String lastName = attributes.getFirst(UserModel.LAST_NAME);
|
||||||
context.getEvent().detail(Details.EMAIL, email);
|
context.getEvent().detail(Details.EMAIL, email);
|
||||||
|
|
||||||
context.getEvent().detail(Details.USERNAME, username);
|
context.getEvent().detail(Details.USERNAME, username);
|
||||||
|
@ -92,7 +93,7 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
|
||||||
List<FormMessage> errors = Validation.getFormErrorsFromValidation(pve.getErrors());
|
List<FormMessage> errors = Validation.getFormErrorsFromValidation(pve.getErrors());
|
||||||
|
|
||||||
if (pve.hasError(Messages.EMAIL_EXISTS, Messages.INVALID_EMAIL)) {
|
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)) {
|
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) {
|
public static void updateEmailNow(EventBuilder event, UserModel user, UserProfile emailUpdateValidationResult) {
|
||||||
|
|
||||||
String oldEmail = user.getEmail();
|
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);
|
event.event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, newEmail);
|
||||||
emailUpdateValidationResult.update(false, new EventAuditingAttributeChangeListener(emailUpdateValidationResult, event));
|
emailUpdateValidationResult.update(false, new EventAuditingAttributeChangeListener(emailUpdateValidationResult, event));
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,12 +24,9 @@ import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Supplier;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
@ -66,12 +63,9 @@ import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserConsentModel;
|
import org.keycloak.models.UserConsentModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.utils.ModelToRepresentation;
|
import org.keycloak.models.utils.ModelToRepresentation;
|
||||||
import org.keycloak.provider.ConfiguredProvider;
|
|
||||||
import org.keycloak.representations.account.ClientRepresentation;
|
import org.keycloak.representations.account.ClientRepresentation;
|
||||||
import org.keycloak.representations.account.ConsentRepresentation;
|
import org.keycloak.representations.account.ConsentRepresentation;
|
||||||
import org.keycloak.representations.account.ConsentScopeRepresentation;
|
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.account.UserRepresentation;
|
||||||
import org.keycloak.representations.idm.ErrorRepresentation;
|
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||||
import org.keycloak.representations.idm.GroupRepresentation;
|
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.managers.UserConsentManager;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.resources.account.resources.ResourcesService;
|
import org.keycloak.services.resources.account.resources.ResourcesService;
|
||||||
import org.keycloak.services.resources.admin.UserProfileResource;
|
|
||||||
import org.keycloak.services.util.ResolveRelative;
|
import org.keycloak.services.util.ResolveRelative;
|
||||||
import org.keycloak.storage.ReadOnlyException;
|
import org.keycloak.storage.ReadOnlyException;
|
||||||
import org.keycloak.theme.Theme;
|
import org.keycloak.theme.Theme;
|
||||||
|
@ -92,8 +85,6 @@ import org.keycloak.userprofile.UserProfileProvider;
|
||||||
import org.keycloak.userprofile.EventAuditingAttributeChangeListener;
|
import org.keycloak.userprofile.EventAuditingAttributeChangeListener;
|
||||||
import org.keycloak.userprofile.ValidationException;
|
import org.keycloak.userprofile.ValidationException;
|
||||||
import org.keycloak.userprofile.ValidationException.Error;
|
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>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
@ -142,36 +133,15 @@ public class AccountRestService {
|
||||||
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
|
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
|
||||||
|
|
||||||
UserModel user = auth.getUser();
|
UserModel user = auth.getUser();
|
||||||
|
|
||||||
UserRepresentation rep = new UserRepresentation();
|
|
||||||
rep.setId(user.getId());
|
|
||||||
|
|
||||||
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||||
UserProfile profile = provider.create(UserProfileContext.ACCOUNT, user);
|
UserProfile profile = provider.create(UserProfileContext.ACCOUNT, user);
|
||||||
|
UserRepresentation rep = profile.toRepresentation();
|
||||||
|
|
||||||
rep.setAttributes(profile.getAttributes().getReadable(false));
|
if (userProfileMetadata != null && !userProfileMetadata) {
|
||||||
|
rep.setUserProfileMetadata(null);
|
||||||
addReadableBuiltinAttributes(user, rep, profile.getAttributes().getReadable(true).keySet());
|
|
||||||
|
|
||||||
if(userProfileMetadata == null || userProfileMetadata.booleanValue())
|
|
||||||
rep.setUserProfileMetadata(UserProfileResource.createUserProfileMetadata(session, profile));
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return rep;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path("/")
|
@Path("/")
|
||||||
|
@ -185,7 +155,7 @@ public class AccountRestService {
|
||||||
event.event(EventType.UPDATE_PROFILE).detail(Details.CONTEXT, UserProfileContext.ACCOUNT.name());
|
event.event(EventType.UPDATE_PROFILE).detail(Details.CONTEXT, UserProfileContext.ACCOUNT.name());
|
||||||
|
|
||||||
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
|
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 {
|
try {
|
||||||
|
|
||||||
|
|
|
@ -16,13 +16,9 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.services.resources.admin;
|
package org.keycloak.services.resources.admin;
|
||||||
|
|
||||||
|
import static org.keycloak.userprofile.UserProfileUtil.createUserProfileMetadata;
|
||||||
|
|
||||||
import java.util.Collections;
|
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.Consumes;
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
|
@ -117,56 +113,4 @@ public class UserProfileResource {
|
||||||
|
|
||||||
return Response.ok(t.getConfiguration()).type(MediaType.APPLICATION_JSON).build();
|
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_ID;
|
||||||
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
|
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;
|
import static org.keycloak.userprofile.UserProfileContext.USER_API;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -184,7 +183,7 @@ public class UserResource {
|
||||||
wasPermanentlyLockedOut = session.getProvider(BruteForceProtector.class).isPermanentlyLockedOut(session, realm, user);
|
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) {
|
if (rep.getAttributes() == null) {
|
||||||
// include existing attributes in case no attributes are set so that validation takes into account the existing
|
// 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);
|
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()) {
|
if (realm.isIdentityFederationEnabled()) {
|
||||||
List<FederatedIdentityRepresentation> reps = getFederatedIdentities(user).collect(Collectors.toList());
|
List<FederatedIdentityRepresentation> reps = getFederatedIdentities(user).collect(Collectors.toList());
|
||||||
|
@ -314,16 +315,8 @@ public class UserResource {
|
||||||
}
|
}
|
||||||
rep.setAccess(auth.users().getAccess(user));
|
rep.setAccess(auth.users().getAccess(user));
|
||||||
|
|
||||||
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
if (!userProfileMetadata) {
|
||||||
UserProfile profile = provider.create(USER_API, user);
|
rep.setUserProfileMetadata(null);
|
||||||
Map<String, List<String>> readableAttributes = profile.getAttributes().getReadable(false);
|
|
||||||
|
|
||||||
if (rep.getAttributes() != null) {
|
|
||||||
rep.setAttributes(readableAttributes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userProfileMetadata) {
|
|
||||||
rep.setUserProfileMetadata(createUserProfileMetadata(session, profile));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return rep;
|
return rep;
|
||||||
|
|
|
@ -154,7 +154,7 @@ public class UsersResource {
|
||||||
|
|
||||||
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
|
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
|
||||||
|
|
||||||
UserProfile profile = profileProvider.create(USER_API, rep.toAttributes());
|
UserProfile profile = profileProvider.create(USER_API, rep.getRawAttributes());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Response response = UserResource.validateUserProfile(profile, session, auth.adminAuth());
|
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 DefaultAttributes(context, attributes, user, metadata, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new LegacyAttributes(context, attributes, user, metadata, session);
|
return new LegacyAttributes(context, attributes, user, metadata, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,11 +154,11 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
|
||||||
@Override
|
@Override
|
||||||
public UserModel apply(Attributes attributes) {
|
public UserModel apply(Attributes attributes) {
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
String userName = attributes.getFirstValue(UserModel.USERNAME);
|
String userName = attributes.getFirst(UserModel.USERNAME);
|
||||||
|
|
||||||
// fallback to email in case email is allowed
|
// fallback to email in case email is allowed
|
||||||
if (userName == null) {
|
if (userName == null) {
|
||||||
userName = attributes.getFirstValue(UserModel.EMAIL);
|
userName = attributes.getFirst(UserModel.EMAIL);
|
||||||
}
|
}
|
||||||
|
|
||||||
user = session.users().addUser(session.getContext().getRealm(), userName);
|
user = session.users().addUser(session.getContext().getRealm(), userName);
|
||||||
|
|
|
@ -75,7 +75,7 @@ public class ImmutableAttributeValidator implements SimpleValidator {
|
||||||
return context;
|
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)) {
|
if (UserModel.USERNAME.equals(attributeName) && collectionEquals(values, email)) {
|
||||||
return context;
|
return context;
|
||||||
|
|
|
@ -70,7 +70,7 @@ public class UsernameMutationValidator implements SimpleValidator {
|
||||||
|
|
||||||
if (!realm.isEditUsernameAllowed() && user != null && !value.equals(user.getFirstAttribute(UserModel.USERNAME))) {
|
if (!realm.isEditUsernameAllowed() && user != null && !value.equals(user.getFirstAttribute(UserModel.USERNAME))) {
|
||||||
Attributes attributes = attributeContext.getAttributes();
|
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
|
// 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
|
// it is expected that username changes when attributes are normalized by the provider
|
||||||
return context;
|
return context;
|
||||||
|
|
|
@ -19,6 +19,8 @@
|
||||||
package org.keycloak.testsuite.account;
|
package org.keycloak.testsuite.account;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import jakarta.ws.rs.BadRequestException;
|
import jakarta.ws.rs.BadRequestException;
|
||||||
|
|
||||||
|
@ -99,7 +101,7 @@ public class AccountRestServiceReadOnlyAttributesTest extends AbstractRestServic
|
||||||
private void testAccountUpdateAttributeExpectFailure(String attrName, boolean deniedForAdminAsWell) throws IOException {
|
private void testAccountUpdateAttributeExpectFailure(String attrName, boolean deniedForAdminAsWell) throws IOException {
|
||||||
// Attribute not yet supposed to be on the user
|
// Attribute not yet supposed to be on the user
|
||||||
UserRepresentation user = SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
|
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
|
// Assert not possible to add the attribute to the user
|
||||||
user.singleAttribute(attrName, "foo");
|
user.singleAttribute(attrName, "foo");
|
||||||
|
@ -147,7 +149,7 @@ public class AccountRestServiceReadOnlyAttributesTest extends AbstractRestServic
|
||||||
private void testAccountUpdateAttributeExpectSuccess(String attrName) throws IOException {
|
private void testAccountUpdateAttributeExpectSuccess(String attrName) throws IOException {
|
||||||
// Attribute not yet supposed to be on the user
|
// Attribute not yet supposed to be on the user
|
||||||
UserRepresentation user = SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
|
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
|
// Assert not possible to add the attribute to the user
|
||||||
user.singleAttribute(attrName, "foo");
|
user.singleAttribute(attrName, "foo");
|
||||||
|
|
|
@ -79,6 +79,7 @@ import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.is;
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
|
@ -278,7 +279,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
||||||
assertEquals("Brady", user.getLastName());
|
assertEquals("Brady", user.getLastName());
|
||||||
assertEquals("test-user@localhost", user.getEmail());
|
assertEquals("test-user@localhost", user.getEmail());
|
||||||
assertFalse(user.isEmailVerified());
|
assertFalse(user.isEmailVerified());
|
||||||
assertTrue(user.getAttributes().isEmpty());
|
assertNull(user.getAttributes());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -288,7 +289,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
||||||
String originalFirstName = user.getFirstName();
|
String originalFirstName = user.getFirstName();
|
||||||
String originalLastName = user.getLastName();
|
String originalLastName = user.getLastName();
|
||||||
String originalEmail = user.getEmail();
|
String originalEmail = user.getEmail();
|
||||||
Map<String, List<String>> originalAttributes = new HashMap<>(user.getAttributes());
|
user.setAttributes(Optional.ofNullable(user.getAttributes()).orElse(new HashMap<>()));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
|
RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
|
||||||
|
@ -316,7 +317,6 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
||||||
user.setFirstName(originalFirstName);
|
user.setFirstName(originalFirstName);
|
||||||
user.setLastName(originalLastName);
|
user.setLastName(originalLastName);
|
||||||
user.setEmail(originalEmail);
|
user.setEmail(originalEmail);
|
||||||
user.setAttributes(originalAttributes);
|
|
||||||
SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asResponse();
|
SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asResponse();
|
||||||
System.out.println(response.asString());
|
System.out.println(response.asString());
|
||||||
assertEquals(204, response.getStatus());
|
assertEquals(204, response.getStatus());
|
||||||
|
@ -379,7 +379,8 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
||||||
String originalFirstName = user.getFirstName();
|
String originalFirstName = user.getFirstName();
|
||||||
String originalLastName = user.getLastName();
|
String originalLastName = user.getLastName();
|
||||||
String originalEmail = user.getEmail();
|
String originalEmail = user.getEmail();
|
||||||
Map<String, List<String>> originalAttributes = new HashMap<>(user.getAttributes());
|
assertNull(user.getAttributes());
|
||||||
|
user.setAttributes(new HashMap<>());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
|
RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
|
||||||
|
@ -417,7 +418,6 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
||||||
user.setFirstName(originalFirstName);
|
user.setFirstName(originalFirstName);
|
||||||
user.setLastName(originalLastName);
|
user.setLastName(originalLastName);
|
||||||
user.setEmail(originalEmail);
|
user.setEmail(originalEmail);
|
||||||
user.setAttributes(originalAttributes);
|
|
||||||
SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asResponse();
|
SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asResponse();
|
||||||
System.out.println(response.asString());
|
System.out.println(response.asString());
|
||||||
assertEquals(204, response.getStatus());
|
assertEquals(204, response.getStatus());
|
||||||
|
@ -431,7 +431,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
||||||
String originalFirstName = user.getFirstName();
|
String originalFirstName = user.getFirstName();
|
||||||
String originalLastName = user.getLastName();
|
String originalLastName = user.getLastName();
|
||||||
String originalEmail = user.getEmail();
|
String originalEmail = user.getEmail();
|
||||||
Map<String, List<String>> originalAttributes = new HashMap<>(user.getAttributes());
|
user.setAttributes(Optional.ofNullable(user.getAttributes()).orElse(new HashMap<>()));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
|
RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
|
||||||
|
@ -460,12 +460,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
||||||
|
|
||||||
user = updateAndGet(user);
|
user = updateAndGet(user);
|
||||||
|
|
||||||
if (isDeclarativeUserProfile()) {
|
assertEquals(1, user.getAttributes().size());
|
||||||
assertEquals(2, user.getAttributes().size());
|
|
||||||
assertTrue(user.getAttributes().get("attr1").isEmpty());
|
|
||||||
} else {
|
|
||||||
assertEquals(1, user.getAttributes().size());
|
|
||||||
}
|
|
||||||
assertEquals(2, user.getAttributes().get("attr2").size());
|
assertEquals(2, user.getAttributes().get("attr2").size());
|
||||||
assertThat(user.getAttributes().get("attr2"), containsInAnyOrder("val2", "val3"));
|
assertThat(user.getAttributes().get("attr2"), containsInAnyOrder("val2", "val3"));
|
||||||
|
|
||||||
|
@ -522,7 +517,6 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
||||||
user.setFirstName(originalFirstName);
|
user.setFirstName(originalFirstName);
|
||||||
user.setLastName(originalLastName);
|
user.setLastName(originalLastName);
|
||||||
user.setEmail(originalEmail);
|
user.setEmail(originalEmail);
|
||||||
user.setAttributes(originalAttributes);
|
|
||||||
SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asResponse();
|
SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asResponse();
|
||||||
System.out.println(response.asString());
|
System.out.println(response.asString());
|
||||||
assertEquals(204, response.getStatus());
|
assertEquals(204, response.getStatus());
|
||||||
|
@ -556,6 +550,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
||||||
public void testUpdateProfileCannotChangeThroughAttributes() throws IOException {
|
public void testUpdateProfileCannotChangeThroughAttributes() throws IOException {
|
||||||
UserRepresentation user = getUser();
|
UserRepresentation user = getUser();
|
||||||
String originalUsername = user.getUsername();
|
String originalUsername = user.getUsername();
|
||||||
|
user.setAttributes(Optional.ofNullable(user.getAttributes()).orElse(new HashMap<>()));
|
||||||
Map<String, List<String>> originalAttributes = new HashMap<>(user.getAttributes());
|
Map<String, List<String>> originalAttributes = new HashMap<>(user.getAttributes());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -29,6 +29,7 @@ import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
@ -292,6 +293,7 @@ public class AccountRestServiceWithUserProfileTest extends AccountRestServiceTes
|
||||||
String originalFirstName = user.getFirstName();
|
String originalFirstName = user.getFirstName();
|
||||||
String originalLastName = user.getLastName();
|
String originalLastName = user.getLastName();
|
||||||
String originalEmail = user.getEmail();
|
String originalEmail = user.getEmail();
|
||||||
|
user.setAttributes(Optional.ofNullable(user.getAttributes()).orElse(new HashMap<>()));
|
||||||
Map<String, List<String>> originalAttributes = new HashMap<>(user.getAttributes());
|
Map<String, List<String>> originalAttributes = new HashMap<>(user.getAttributes());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -303,13 +305,13 @@ public class AccountRestServiceWithUserProfileTest extends AccountRestServiceTes
|
||||||
user.setEmail("bobby@localhost");
|
user.setEmail("bobby@localhost");
|
||||||
user.setFirstName("Homer");
|
user.setFirstName("Homer");
|
||||||
user.setLastName("Simpsons");
|
user.setLastName("Simpsons");
|
||||||
user.getAttributes().put("attr1", Collections.singletonList("val1"));
|
user.getAttributes().put("attr1", Collections.singletonList("val11"));
|
||||||
user.getAttributes().put("attr2", Collections.singletonList("val2"));
|
user.getAttributes().put("attr2", Collections.singletonList("val22"));
|
||||||
|
|
||||||
|
events.clear();
|
||||||
user = updateAndGet(user);
|
user = updateAndGet(user);
|
||||||
|
|
||||||
//skip login to the REST API event
|
//skip login to the REST API event
|
||||||
events.poll();
|
|
||||||
events.expectAccount(EventType.UPDATE_PROFILE).user(user.getId())
|
events.expectAccount(EventType.UPDATE_PROFILE).user(user.getId())
|
||||||
.detail(Details.CONTEXT, UserProfileContext.ACCOUNT.name())
|
.detail(Details.CONTEXT, UserProfileContext.ACCOUNT.name())
|
||||||
.detail(Details.PREVIOUS_EMAIL, originalEmail)
|
.detail(Details.PREVIOUS_EMAIL, originalEmail)
|
||||||
|
@ -318,7 +320,7 @@ public class AccountRestServiceWithUserProfileTest extends AccountRestServiceTes
|
||||||
.detail(Details.PREVIOUS_LAST_NAME, originalLastName)
|
.detail(Details.PREVIOUS_LAST_NAME, originalLastName)
|
||||||
.detail(Details.UPDATED_FIRST_NAME, "Homer")
|
.detail(Details.UPDATED_FIRST_NAME, "Homer")
|
||||||
.detail(Details.UPDATED_LAST_NAME, "Simpsons")
|
.detail(Details.UPDATED_LAST_NAME, "Simpsons")
|
||||||
.detail(Details.PREF_UPDATED+"attr2", "val2")
|
.detail(Details.PREF_UPDATED+"attr2", "val22")
|
||||||
.assertEvent();
|
.assertEvent();
|
||||||
events.assertEmpty();
|
events.assertEmpty();
|
||||||
|
|
||||||
|
@ -379,22 +381,23 @@ public class AccountRestServiceWithUserProfileTest extends AccountRestServiceTes
|
||||||
realmRep.setInternationalizationEnabled(false);
|
realmRep.setInternationalizationEnabled(false);
|
||||||
testRealm().update(realmRep);
|
testRealm().update(realmRep);
|
||||||
UserRepresentation user = getUser();
|
UserRepresentation user = getUser();
|
||||||
|
user.setAttributes(Optional.ofNullable(user.getAttributes()).orElse(new HashMap<>()));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
user.getAttributes().put(UserModel.LOCALE, List.of("pt_BR"));
|
user.getAttributes().put(UserModel.LOCALE, List.of("pt_BR"));
|
||||||
user = updateAndGet(user);
|
user = updateAndGet(user);
|
||||||
assertNull(user.getAttributes().get(UserModel.LOCALE));
|
assertNull(user.getAttributes());
|
||||||
|
|
||||||
realmRep.setInternationalizationEnabled(true);
|
realmRep.setInternationalizationEnabled(true);
|
||||||
testRealm().update(realmRep);
|
testRealm().update(realmRep);
|
||||||
|
|
||||||
user.getAttributes().put(UserModel.LOCALE, List.of("pt_BR"));
|
user.singleAttribute(UserModel.LOCALE, "pt_BR");
|
||||||
user = updateAndGet(user);
|
user = updateAndGet(user);
|
||||||
assertEquals("pt_BR", user.getAttributes().get(UserModel.LOCALE).get(0));
|
assertEquals("pt_BR", user.getAttributes().get(UserModel.LOCALE).get(0));
|
||||||
|
|
||||||
user.getAttributes().remove(UserModel.LOCALE);
|
user.getAttributes().remove(UserModel.LOCALE);
|
||||||
user = updateAndGet(user);
|
user = updateAndGet(user);
|
||||||
assertNull(user.getAttributes().get(UserModel.LOCALE));
|
assertNull(user.getAttributes());
|
||||||
|
|
||||||
UserProfileMetadata metadata = user.getUserProfileMetadata();
|
UserProfileMetadata metadata = user.getUserProfileMetadata();
|
||||||
|
|
||||||
|
@ -406,7 +409,6 @@ public class AccountRestServiceWithUserProfileTest extends AccountRestServiceTes
|
||||||
} finally {
|
} finally {
|
||||||
realmRep.setInternationalizationEnabled(internationalizationEnabled);
|
realmRep.setInternationalizationEnabled(internationalizationEnabled);
|
||||||
testRealm().update(realmRep);
|
testRealm().update(realmRep);
|
||||||
user.getAttributes().remove(UserModel.LOCALE);
|
|
||||||
updateAndGet(user);
|
updateAndGet(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ import org.keycloak.userprofile.UserProfileProvider;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -52,7 +53,6 @@ import jakarta.ws.rs.core.Response;
|
||||||
@EnableFeature(value = Profile.Feature.DECLARATIVE_USER_PROFILE)
|
@EnableFeature(value = Profile.Feature.DECLARATIVE_USER_PROFILE)
|
||||||
public class DeclarativeUserTest extends AbstractAdminTest {
|
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 TEST_REALM_USER_MANAGER_NAME = "test-realm-user-manager";
|
||||||
private static final String REQUIRED_ATTR_KEY = "required-attr";
|
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
|
@Test
|
||||||
public void testDoNotReturnAttributeIfNotReadble() {
|
public void testDoNotReturnAttributeIfNotReadble() {
|
||||||
UserRepresentation user1 = new UserRepresentation();
|
UserRepresentation user1 = new UserRepresentation();
|
||||||
|
@ -153,7 +130,7 @@ public class DeclarativeUserTest extends AbstractAdminTest {
|
||||||
|
|
||||||
user1 = realm.users().get(user1Id).toRepresentation();
|
user1 = realm.users().get(user1Id).toRepresentation();
|
||||||
Map<String, List<String>> attributes = user1.getAttributes();
|
Map<String, List<String>> attributes = user1.getAttributes();
|
||||||
assertEquals(4, attributes.size());
|
assertEquals(2, attributes.size());
|
||||||
assertFalse(attributes.containsKey("custom-hidden"));
|
assertFalse(attributes.containsKey("custom-hidden"));
|
||||||
|
|
||||||
setUserProfileConfiguration(this.realm, "{\"attributes\": ["
|
setUserProfileConfiguration(this.realm, "{\"attributes\": ["
|
||||||
|
@ -170,8 +147,8 @@ public class DeclarativeUserTest extends AbstractAdminTest {
|
||||||
|
|
||||||
user1 = realm.users().get(user1Id).toRepresentation();
|
user1 = realm.users().get(user1Id).toRepresentation();
|
||||||
attributes = user1.getAttributes();
|
attributes = user1.getAttributes();
|
||||||
assertEquals(5, attributes.size());
|
assertEquals(2, attributes.size());
|
||||||
assertTrue(attributes.containsKey("custom-hidden"));
|
assertFalse(attributes.containsKey("custom-hidden"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -200,12 +177,17 @@ public class DeclarativeUserTest extends AbstractAdminTest {
|
||||||
|
|
||||||
UserResource userResource = realm.users().get(user1Id);
|
UserResource userResource = realm.users().get(user1Id);
|
||||||
user1 = userResource.toRepresentation();
|
user1 = userResource.toRepresentation();
|
||||||
Map<String, List<String>> attributes = user1.getAttributes();
|
assertNull(user1.getAttributes());
|
||||||
attributes.put("attr2", Collections.singletonList(""));
|
user1.singleAttribute("attr2", "");
|
||||||
// should be able to update the user when a read-only attribute has an empty or null value
|
// should be able to update the user when a read-only attribute has an empty or null value
|
||||||
userResource.update(user1);
|
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);
|
userResource.update(user1);
|
||||||
|
user1 = userResource.toRepresentation();
|
||||||
|
assertNull(user1.getAttributes());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -288,7 +270,7 @@ public class DeclarativeUserTest extends AbstractAdminTest {
|
||||||
realm.update(realmRep);
|
realm.update(realmRep);
|
||||||
|
|
||||||
user1 = userResource.toRepresentation();
|
user1 = userResource.toRepresentation();
|
||||||
assertNull(user1.getAttributes().get(UserModel.LOCALE));
|
assertNull(user1.getAttributes());
|
||||||
} finally {
|
} finally {
|
||||||
realmRep.setInternationalizationEnabled(internationalizationEnabled);
|
realmRep.setInternationalizationEnabled(internationalizationEnabled);
|
||||||
realm.update(realmRep);
|
realm.update(realmRep);
|
||||||
|
|
|
@ -1529,20 +1529,12 @@ public class UserTest extends AbstractAdminTest {
|
||||||
String user2Id = createUser(user2);
|
String user2Id = createUser(user2);
|
||||||
|
|
||||||
user1 = realm.users().get(user1Id).toRepresentation();
|
user1 = realm.users().get(user1Id).toRepresentation();
|
||||||
if (isDeclarativeUserProfile()) {
|
assertEquals(2, user1.getAttributes().size());
|
||||||
assertEquals(managedAttributes.size(), user1.getAttributes().size());
|
|
||||||
} else {
|
|
||||||
assertEquals(2, user1.getAttributes().size());
|
|
||||||
}
|
|
||||||
assertAttributeValue("value1user1", user1.getAttributes().get("attr1"));
|
assertAttributeValue("value1user1", user1.getAttributes().get("attr1"));
|
||||||
assertAttributeValue("value2user1", user1.getAttributes().get("attr2"));
|
assertAttributeValue("value2user1", user1.getAttributes().get("attr2"));
|
||||||
|
|
||||||
user2 = realm.users().get(user2Id).toRepresentation();
|
user2 = realm.users().get(user2Id).toRepresentation();
|
||||||
if (isDeclarativeUserProfile()) {
|
assertEquals(2, user2.getAttributes().size());
|
||||||
assertEquals(managedAttributes.size(), user2.getAttributes().size());
|
|
||||||
} else {
|
|
||||||
assertEquals(2, user2.getAttributes().size());
|
|
||||||
}
|
|
||||||
assertAttributeValue("value1user2", user2.getAttributes().get("attr1"));
|
assertAttributeValue("value1user2", user2.getAttributes().get("attr1"));
|
||||||
vals = user2.getAttributes().get("attr2");
|
vals = user2.getAttributes().get("attr2");
|
||||||
assertEquals(2, vals.size());
|
assertEquals(2, vals.size());
|
||||||
|
@ -1554,11 +1546,7 @@ public class UserTest extends AbstractAdminTest {
|
||||||
updateUser(realm.users().get(user1Id), user1);
|
updateUser(realm.users().get(user1Id), user1);
|
||||||
|
|
||||||
user1 = realm.users().get(user1Id).toRepresentation();
|
user1 = realm.users().get(user1Id).toRepresentation();
|
||||||
if (isDeclarativeUserProfile()) {
|
assertEquals(3, user1.getAttributes().size());
|
||||||
assertEquals(managedAttributes.size(), user1.getAttributes().size());
|
|
||||||
} else {
|
|
||||||
assertEquals(3, user1.getAttributes().size());
|
|
||||||
}
|
|
||||||
assertAttributeValue("value3user1", user1.getAttributes().get("attr1"));
|
assertAttributeValue("value3user1", user1.getAttributes().get("attr1"));
|
||||||
assertAttributeValue("value2user1", user1.getAttributes().get("attr2"));
|
assertAttributeValue("value2user1", user1.getAttributes().get("attr2"));
|
||||||
assertAttributeValue("value4user1", user1.getAttributes().get("attr3"));
|
assertAttributeValue("value4user1", user1.getAttributes().get("attr3"));
|
||||||
|
@ -1567,11 +1555,7 @@ public class UserTest extends AbstractAdminTest {
|
||||||
updateUser(realm.users().get(user1Id), user1);
|
updateUser(realm.users().get(user1Id), user1);
|
||||||
|
|
||||||
user1 = realm.users().get(user1Id).toRepresentation();
|
user1 = realm.users().get(user1Id).toRepresentation();
|
||||||
if (isDeclarativeUserProfile()) {
|
assertEquals(2, user1.getAttributes().size());
|
||||||
assertEquals(managedAttributes.size(), user1.getAttributes().size());
|
|
||||||
} else {
|
|
||||||
assertEquals(2, user1.getAttributes().size());
|
|
||||||
}
|
|
||||||
assertAttributeValue("value2user1", user1.getAttributes().get("attr2"));
|
assertAttributeValue("value2user1", user1.getAttributes().get("attr2"));
|
||||||
assertAttributeValue("value4user1", user1.getAttributes().get("attr3"));
|
assertAttributeValue("value4user1", user1.getAttributes().get("attr3"));
|
||||||
|
|
||||||
|
@ -1580,11 +1564,7 @@ public class UserTest extends AbstractAdminTest {
|
||||||
updateUser(realm.users().get(user1Id), user1);
|
updateUser(realm.users().get(user1Id), user1);
|
||||||
user1 = realm.users().get(user1Id).toRepresentation();
|
user1 = realm.users().get(user1Id).toRepresentation();
|
||||||
assertNotNull(user1.getAttributes());
|
assertNotNull(user1.getAttributes());
|
||||||
if (isDeclarativeUserProfile()) {
|
assertEquals(2, user1.getAttributes().size());
|
||||||
assertEquals(managedAttributes.size(), user1.getAttributes().size());
|
|
||||||
} else {
|
|
||||||
assertEquals(2, user1.getAttributes().size());
|
|
||||||
}
|
|
||||||
|
|
||||||
// empty attributes should remove attributes
|
// empty attributes should remove attributes
|
||||||
user1.setAttributes(Collections.emptyMap());
|
user1.setAttributes(Collections.emptyMap());
|
||||||
|
@ -1602,21 +1582,13 @@ public class UserTest extends AbstractAdminTest {
|
||||||
|
|
||||||
realm.users().get(user1Id).update(user1);
|
realm.users().get(user1Id).update(user1);
|
||||||
user1 = realm.users().get(user1Id).toRepresentation();
|
user1 = realm.users().get(user1Id).toRepresentation();
|
||||||
if (isDeclarativeUserProfile()) {
|
assertEquals(2, user1.getAttributes().size());
|
||||||
assertEquals(managedAttributes.size(), user1.getAttributes().size());
|
|
||||||
} else {
|
|
||||||
assertEquals(2, user1.getAttributes().size());
|
|
||||||
}
|
|
||||||
|
|
||||||
user1.getAttributes().remove("foo");
|
user1.getAttributes().remove("foo");
|
||||||
|
|
||||||
realm.users().get(user1Id).update(user1);
|
realm.users().get(user1Id).update(user1);
|
||||||
user1 = realm.users().get(user1Id).toRepresentation();
|
user1 = realm.users().get(user1Id).toRepresentation();
|
||||||
if (isDeclarativeUserProfile()) {
|
assertEquals(1, user1.getAttributes().size());
|
||||||
assertEquals(managedAttributes.size(), user1.getAttributes().size());
|
|
||||||
} else {
|
|
||||||
assertEquals(1, user1.getAttributes().size());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -1669,11 +1641,7 @@ public class UserTest extends AbstractAdminTest {
|
||||||
user1 = realm.users().get(user1Id).toRepresentation();
|
user1 = realm.users().get(user1Id).toRepresentation();
|
||||||
assertEquals("foo", user1.getAttributes().get("usercertificate").get(0));
|
assertEquals("foo", user1.getAttributes().get("usercertificate").get(0));
|
||||||
assertEquals("bar", user1.getAttributes().get("saml.persistent.name.id.for.foo").get(0));
|
assertEquals("bar", user1.getAttributes().get("saml.persistent.name.id.for.foo").get(0));
|
||||||
if (isDeclarativeUserProfile()) {
|
assertFalse(user1.getAttributes().containsKey(LDAPConstants.LDAP_ID));
|
||||||
assertTrue(user1.getAttributes().get(LDAPConstants.LDAP_ID).isEmpty());
|
|
||||||
} else {
|
|
||||||
assertFalse(user1.getAttributes().containsKey(LDAPConstants.LDAP_ID));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -18,6 +18,8 @@
|
||||||
|
|
||||||
package org.keycloak.testsuite.federation.ldap;
|
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.assertTrue;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
import static org.keycloak.testsuite.forms.VerifyProfileTest.disableDynamicUserProfile;
|
import static org.keycloak.testsuite.forms.VerifyProfileTest.disableDynamicUserProfile;
|
||||||
|
@ -71,14 +73,16 @@ public class LDAPAdminRestApiWithUserProfileTest extends LDAPAdminRestApiTest {
|
||||||
|
|
||||||
UserResource user = testRealm().users().get(newUserId);
|
UserResource user = testRealm().users().get(newUserId);
|
||||||
UserRepresentation userRep = user.toRepresentation();
|
UserRepresentation userRep = user.toRepresentation();
|
||||||
|
assertNull(userRep.getAttributes());
|
||||||
assertTrue(userRep.getAttributes().containsKey(LDAPConstants.LDAP_ID));
|
|
||||||
assertTrue(userRep.getAttributes().get(LDAPConstants.LDAP_ID).isEmpty());
|
|
||||||
|
|
||||||
userRep.singleAttribute(LDAPConstants.LDAP_ID, "");
|
userRep.singleAttribute(LDAPConstants.LDAP_ID, "");
|
||||||
user.update(userRep);
|
user.update(userRep);
|
||||||
|
userRep = testRealm().users().get(newUserId).toRepresentation();
|
||||||
|
assertNull(userRep.getAttributes());
|
||||||
userRep.singleAttribute(LDAPConstants.LDAP_ID, null);
|
userRep.singleAttribute(LDAPConstants.LDAP_ID, null);
|
||||||
user.update(userRep);
|
user.update(userRep);
|
||||||
|
userRep = testRealm().users().get(newUserId).toRepresentation();
|
||||||
|
assertNull(userRep.getAttributes());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
userRep.singleAttribute(LDAPConstants.LDAP_ID, "should-fail");
|
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.LDAPConstants;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.representations.idm.AbstractUserRepresentation;
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
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
|
// create a user with attribute foo value 123 allowed by the profile now but disallowed later
|
||||||
UPConfig config = parseDefaultConfig();
|
UPConfig config = parseDefaultConfig();
|
||||||
config.addOrReplaceAttribute(new UPAttribute("foo", new UPAttributePermissions(Set.of(), Set.of(ROLE_ADMIN))));
|
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();
|
RealmResource realmRes = testRealm();
|
||||||
realmRes.users().userProfile().update(config);
|
realmRes.users().userProfile().update(config);
|
||||||
|
|
||||||
|
@ -142,6 +144,18 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
profile.validate();
|
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
|
// it should fail if foo can be modified
|
||||||
getTestingClient().server(TEST_REALM_NAME).run(session -> {
|
getTestingClient().server(TEST_REALM_NAME).run(session -> {
|
||||||
RealmModel realm = session.getContext().getRealm();
|
RealmModel realm = session.getContext().getRealm();
|
||||||
|
@ -174,7 +188,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
|
|
||||||
// once created, profile attributes can not be changed
|
// once created, profile attributes can not be changed
|
||||||
assertTrue(profile.getAttributes().contains(UserModel.USERNAME));
|
assertTrue(profile.getAttributes().contains(UserModel.USERNAME));
|
||||||
assertNull(profile.getAttributes().getFirstValue(UserModel.USERNAME));
|
assertNull(profile.getAttributes().getFirst(UserModel.USERNAME));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -413,11 +427,11 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
assertTrue(ve.isAttributeOnError("address"));
|
assertTrue(ve.isAttributeOnError("address"));
|
||||||
}
|
}
|
||||||
|
|
||||||
assertNotNull(attributes.getFirstValue(UserModel.USERNAME));
|
assertNotNull(attributes.getFirst(UserModel.USERNAME));
|
||||||
assertNotNull(attributes.getFirstValue(UserModel.EMAIL));
|
assertNotNull(attributes.getFirst(UserModel.EMAIL));
|
||||||
assertNotNull(attributes.getFirstValue(UserModel.FIRST_NAME));
|
assertNotNull(attributes.getFirst(UserModel.FIRST_NAME));
|
||||||
assertNotNull(attributes.getFirstValue(UserModel.LAST_NAME));
|
assertNotNull(attributes.getFirst(UserModel.LAST_NAME));
|
||||||
assertNull(attributes.getFirstValue("address"));
|
assertNull(attributes.getFirst("address"));
|
||||||
|
|
||||||
user.setAttribute("address", Arrays.asList("fixed-address"));
|
user.setAttribute("address", Arrays.asList("fixed-address"));
|
||||||
|
|
||||||
|
@ -426,7 +440,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
|
|
||||||
profile.validate();
|
profile.validate();
|
||||||
|
|
||||||
assertNotNull(attributes.getFirstValue("address"));
|
assertNotNull(attributes.getFirst("address"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -1516,20 +1530,20 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
|
|
||||||
profile = provider.create(UserProfileContext.USER_API, user);
|
profile = provider.create(UserProfileContext.USER_API, user);
|
||||||
Attributes userAttributes = profile.getAttributes();
|
Attributes userAttributes = profile.getAttributes();
|
||||||
assertEquals("new-email@test.com", userAttributes.getFirstValue(UserModel.EMAIL));
|
assertEquals("new-email@test.com", userAttributes.getFirst(UserModel.EMAIL));
|
||||||
assertEquals("Test Value", userAttributes.getFirstValue("test-attribute"));
|
assertEquals("Test Value", userAttributes.getFirst("test-attribute"));
|
||||||
assertEquals("changed", userAttributes.getFirstValue("foo"));
|
assertEquals("changed", userAttributes.getFirst("foo"));
|
||||||
|
|
||||||
attributes.remove("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 = provider.create(UserProfileContext.USER_API, attributes, user);
|
||||||
profile.update(true);
|
profile.update(true);
|
||||||
profile = provider.create(UserProfileContext.USER_API, user);
|
profile = provider.create(UserProfileContext.USER_API, user);
|
||||||
userAttributes = profile.getAttributes();
|
userAttributes = profile.getAttributes();
|
||||||
// remove attribute if not set
|
// remove attribute if not set
|
||||||
assertEquals("new-email@test.com", userAttributes.getFirstValue(UserModel.EMAIL));
|
assertEquals("new-email@test.com", userAttributes.getFirst(UserModel.EMAIL));
|
||||||
assertEquals("Test Value", userAttributes.getFirstValue("test-attribute"));
|
assertEquals("Test Value", userAttributes.getFirst("test-attribute"));
|
||||||
assertNull(userAttributes.getFirstValue("foo"));
|
assertNull(userAttributes.getFirst("foo"));
|
||||||
|
|
||||||
config.addOrReplaceAttribute(new UPAttribute("test-attribute", new UPAttributePermissions(Set.of(), Set.of(ROLE_USER))));
|
config.addOrReplaceAttribute(new UPAttribute("test-attribute", new UPAttributePermissions(Set.of(), Set.of(ROLE_USER))));
|
||||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||||
|
@ -1539,8 +1553,8 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
profile = provider.create(UserProfileContext.USER_API, user);
|
profile = provider.create(UserProfileContext.USER_API, user);
|
||||||
userAttributes = profile.getAttributes();
|
userAttributes = profile.getAttributes();
|
||||||
// do not remove test-attribute because admin does not have write permissions
|
// do not remove test-attribute because admin does not have write permissions
|
||||||
assertEquals("new-email@test.com", userAttributes.getFirstValue(UserModel.EMAIL));
|
assertEquals("new-email@test.com", userAttributes.getFirst(UserModel.EMAIL));
|
||||||
assertEquals("Test Value", userAttributes.getFirstValue("test-attribute"));
|
assertEquals("Test Value", userAttributes.getFirst("test-attribute"));
|
||||||
|
|
||||||
config.addOrReplaceAttribute(new UPAttribute("test-attribute", new UPAttributePermissions(Set.of(), Set.of(ROLE_USER, ROLE_ADMIN))));
|
config.addOrReplaceAttribute(new UPAttribute("test-attribute", new UPAttributePermissions(Set.of(), Set.of(ROLE_USER, ROLE_ADMIN))));
|
||||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||||
|
@ -1550,8 +1564,8 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
profile = provider.create(UserProfileContext.USER_API, user);
|
profile = provider.create(UserProfileContext.USER_API, user);
|
||||||
userAttributes = profile.getAttributes();
|
userAttributes = profile.getAttributes();
|
||||||
// removes the test-attribute attribute because now admin has write permission
|
// removes the test-attribute attribute because now admin has write permission
|
||||||
assertEquals("new-email@test.com", userAttributes.getFirstValue(UserModel.EMAIL));
|
assertEquals("new-email@test.com", userAttributes.getFirst(UserModel.EMAIL));
|
||||||
assertNull(userAttributes.getFirstValue("test-attribute"));
|
assertNull(userAttributes.getFirst("test-attribute"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -1594,11 +1608,11 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void assertRemoveEmptyRootAttribute(Map<String, List<String>> attributes, UserModel user, Attributes upAttributes) {
|
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(user.getLastName());
|
||||||
assertNull(upAttributes.getFirstValue(UserModel.EMAIL));
|
assertNull(upAttributes.getFirst(UserModel.EMAIL));
|
||||||
assertNull(user.getEmail());
|
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
|
@Test
|
||||||
|
@ -1693,6 +1707,77 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
assertFalse(profile.getAttributes().isReadOnly("foo"));
|
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
|
@Test
|
||||||
public void testAttributeNormalization() {
|
public void testAttributeNormalization() {
|
||||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::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");
|
attributes.put(UserModel.EMAIL, "TesT@TesT.org");
|
||||||
UserProfile profile = provider.create(UserProfileContext.USER_API, attributes);
|
UserProfile profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||||
Attributes profileAttributes = profile.getAttributes();
|
Attributes profileAttributes = profile.getAttributes();
|
||||||
assertEquals(attributes.get(UserModel.USERNAME).toLowerCase(), profileAttributes.getFirstValue(UserModel.USERNAME));
|
assertEquals(attributes.get(UserModel.USERNAME).toLowerCase(), profileAttributes.getFirst(UserModel.USERNAME));
|
||||||
assertEquals(attributes.get(UserModel.EMAIL).toLowerCase(), profileAttributes.getFirstValue(UserModel.EMAIL));
|
assertEquals(attributes.get(UserModel.EMAIL).toLowerCase(), profileAttributes.getFirst(UserModel.EMAIL));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue