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;
+ }
}
diff --git a/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfile.java b/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfile.java
index 34c9ee05c51..761ae31004c 100644
--- a/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfile.java
+++ b/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfile.java
@@ -17,22 +17,38 @@
package org.keycloak.userprofile;
-import java.util.function.BiConsumer;
+import java.util.List;
+import java.util.Map;
import org.keycloak.models.UserModel;
+import org.keycloak.representations.idm.AbstractUserRepresentation;
/**
- * An interface providing as an entry point for managing users.
+ *
An interface that serves an entry point for managing users and their attributes.
*
- *
A {@code UserProfile} provides a manageable view for user information that also takes into account the context where it is being used.
- * The context represents the different places in Keycloak where users are created, updated, or validated.
+ *
A {@code UserProfile} provides methods for creating, and updating users as well as for accessing their attributes.
+ * All its operations are based the {@link UserProfileContext}. By taking the context into account, the state and behavior of
+ * {@link UserProfile} instances depend on the context they are associated with where creating, updating, validating, and
+ * accessing the attribute set of a user is based on the configuration (see {@link org.keycloak.representations.userprofile.config.UPConfig})
+ * and the constraints associated with a given context.
+ *
+ *
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.
*
- *
By taking the context into account, the state and behavior of {@link UserProfile} instances depend on the context they
- * are associated with, where validating, updating, creating, or obtaining representations of users is based on the configuration
- * and constraints associated with a context.
+ *
A {@code UserProfile} instance can be obtained through the {@link UserProfileProvider}:
*
- *
A {@code UserProfile} instance can be obtained through the {@link UserProfileProvider}.
+ *
{@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);
+ * }
+ *
+ * The {@link UserProfileProvider} provides different methods for creating {@link UserProfile} instances, each one
+ * target for a specific scenario such as creating a new user, updating an existing one, or only for accessing the attributes
+ * for an existing user as shown in the above example.
*
* @see UserProfileContext
* @see UserProfileProvider
@@ -69,20 +85,23 @@ public interface UserProfile {
void update(boolean removeAttributes, AttributeChangeListener... changeListener) throws ValidationException;
/**
- *
The same as {@link #update(boolean, BiConsumer[])} but forcing the removal of attributes.
+ *
The same as {@link #update(boolean, AttributeChangeListener...)}} but forcing the removal of attributes.
*
* @param changeListener a set of one or more listeners to listen for attribute changes
* @throws ValidationException in case of any validation error
*/
- default void update(AttributeChangeListener... changeListener) throws ValidationException, RuntimeException {
+ default void update(AttributeChangeListener... changeListener) throws ValidationException {
update(true, changeListener);
}
/**
* Returns the attributes associated with this instance. Note that the attributes returned by this method are not necessarily
- * the same from the {@link UserModel}, but those that should be validated and possibly updated to the {@link UserModel}.
+ * the same from the {@link UserModel} as they are based on the configurations set in the {@link org.keycloak.representations.userprofile.config.UPConfig} and
+ * the context this instance is based on.
*
* @return the attributes associated with this instance.
*/
Attributes getAttributes();
+
+ R toRepresentation();
}
diff --git a/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileUtil.java b/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileUtil.java
index cfa6e0f4bd6..45059631acc 100644
--- a/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileUtil.java
+++ b/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileUtil.java
@@ -20,11 +20,23 @@
package org.keycloak.userprofile;
import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
import java.util.function.Predicate;
+import java.util.stream.Collectors;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.ConfiguredProvider;
+import org.keycloak.representations.idm.UserProfileAttributeGroupMetadata;
+import org.keycloak.representations.idm.UserProfileAttributeMetadata;
import org.keycloak.representations.userprofile.config.UPConfig;
+import org.keycloak.representations.userprofile.config.UPGroup;
+import org.keycloak.validate.Validators;
/**
* @author Marek Posolda
@@ -82,4 +94,70 @@ public class UserProfileUtil {
return true;
}
}
+
+ /**
+ * Returns whether the attribute with the given {@code name} is a root attribute.
+ *
+ * @param name the attribute name
+ * @return
+ */
+ public static boolean isRootAttribute(String name) {
+ return UserModel.USERNAME.equals(name)
+ || UserModel.EMAIL.equals(name)
+ || UserModel.FIRST_NAME.equals(name)
+ || UserModel.LAST_NAME.equals(name)
+ || UserModel.LOCALE.equals(name);
+ }
+
+ public static org.keycloak.representations.idm.UserProfileMetadata createUserProfileMetadata(KeycloakSession session, UserProfile profile) {
+ Attributes profileAttributes = profile.getAttributes();
+ Map> am = profileAttributes.getReadable();
+
+ if(am == null)
+ return null;
+ Map> unmanagedAttributes = profileAttributes.getUnmanagedAttributes();
+
+ List 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 groups = config.getGroups().stream().map(new Function() {
+ @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> 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));
+ }
}
diff --git a/server-spi/src/main/java/org/keycloak/userprofile/Attributes.java b/server-spi/src/main/java/org/keycloak/userprofile/Attributes.java
index af5a560b0a3..a55da0e31bc 100644
--- a/server-spi/src/main/java/org/keycloak/userprofile/Attributes.java
+++ b/server-spi/src/main/java/org/keycloak/userprofile/Attributes.java
@@ -19,20 +19,38 @@
package org.keycloak.userprofile;
+import static java.util.Optional.ofNullable;
+
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
-import java.util.stream.Collectors;
-import org.keycloak.models.UserModel;
import org.keycloak.validate.ValidationError;
/**
* This interface wraps the attributes associated with a user profile. Different operations are provided to access and
* manage these attributes.
*
+ *
Attributes are classified as:
+ *
+ * - Managed
+ *
- Unmanaged
+ *
+ *
+ * A managed attribute is any attribute defined in the user profile configuration. Therefore, they are known by
+ * the server and can be managed accordingly.
+ *
+ *
A unmanaged attributes is any attribute not 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}.
+ *
+ *
Any attribute available from this interface has a corresponding {@link AttributeMetadata}
. 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 Pedro Igor
*/
public interface Attributes {
@@ -49,8 +67,8 @@ public interface Attributes {
*
* @return the first value
*/
- default String getFirstValue(String name) {
- List values = getValues(name);
+ default String getFirst(String name) {
+ List values = ofNullable(get(name)).orElse(List.of());
if (values.isEmpty()) {
return null;
@@ -66,16 +84,16 @@ public interface Attributes {
*
* @return the attribute values
*/
- List getValues(String name);
+ List get(String name);
/**
* Checks whether an attribute is read-only.
*
- * @param key
+ * @param name the attribute name
*
- * @return
+ * @return {@code true} if the attribute is read-only. Otherwise, {@code false}
*/
- boolean isReadOnly(String key);
+ boolean isReadOnly(String name);
/**
* Validates the attribute with the given {@code name}.
@@ -105,7 +123,7 @@ public interface Attributes {
Set nameSet();
/**
- * Returns all attributes that can be written.
+ * Returns all the attributes with read-write permissions in a particular {@link UserProfileContext}.
*
* @return the attributes
*/
@@ -131,52 +149,23 @@ public interface Attributes {
boolean isRequired(String name);
/**
- * Similar to {{@link #getReadable(boolean)}} but with the possibility to add or remove
- * the root attributes.
+ * Returns only the attributes that have read permissions in a particular {@link UserProfileContext}.
*
- * @param includeBuiltin if the root attributes should be included.
- * @return the attributes with read/write permission.
- */
- default Map> getReadable(boolean includeBuiltin) {
- return getReadable().entrySet().stream().filter(entry -> {
- if (includeBuiltin) {
- return true;
- }
- if (isRootAttribute(entry.getKey())) {
- if (UserModel.LOCALE.equals(entry.getKey()) && !entry.getValue().isEmpty()) {
- // locale is different form of built-in attribute in the sense it is related to a
- // specific feature (i18n) and does not have a top-level attribute in the user representation
- // the locale should be available from the attribute map if not empty
- return true;
- }
- return false;
- }
- return true;
- }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
- }
-
- /**
- * Returns only the attributes that have read/write permissions.
- *
- * @return the attributes with read/write permission.
+ * @return the attributes with read permission.
*/
Map> getReadable();
/**
- * Returns whether the attribute with the given {@code name} is a root attribute.
+ * Returns the attributes as a {@link Map} that are accessible to a particular {@link UserProfileContext}.
*
- * @param name the attribute name
- * @return
+ * @return a map with all the attributes
*/
- default boolean isRootAttribute(String name) {
- return UserModel.USERNAME.equals(name)
- || UserModel.EMAIL.equals(name)
- || UserModel.FIRST_NAME.equals(name)
- || UserModel.LAST_NAME.equals(name)
- || UserModel.LOCALE.equals(name);
- }
-
Map> toMap();
+ /**
+ * Returns a {@link Map} holding any unmanaged attribute.
+ *
+ * @return a map with any unmanaged attribute
+ */
Map> getUnmanagedAttributes();
}
diff --git a/server-spi/src/main/java/org/keycloak/userprofile/UserProfileContext.java b/server-spi/src/main/java/org/keycloak/userprofile/UserProfileContext.java
index 537f4ebea68..3b6e5317c0a 100644
--- a/server-spi/src/main/java/org/keycloak/userprofile/UserProfileContext.java
+++ b/server-spi/src/main/java/org/keycloak/userprofile/UserProfileContext.java
@@ -21,7 +21,7 @@ package org.keycloak.userprofile;
/**
* 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.
*
*
The context is crucial to drive the conditions that should be respected when managing user profiles. It might be possible
* to include in the future metadata about contexts. As well as support custom contexts.
@@ -30,16 +30,39 @@ package org.keycloak.userprofile;
*/
public enum UserProfileContext {
+ /**
+ * In this context, a user profile is managed by themselves during an authentication flow such as when updating the user profile.
+ */
UPDATE_PROFILE(true),
+
+ /**
+ * In this context, a user profile is managed through the management interface such as the Admin API.
+ */
USER_API(false),
+
+ /**
+ * In this context, a user profile is managed by themselves through the account console.
+ */
ACCOUNT(true),
+
+ /**
+ * In this context, a user profile is managed by themselves when authenticating through a broker.
+ */
IDP_REVIEW(false),
+
+ /**
+ * In this context, a user profile is managed by themselves when registering to a realm.
+ */
REGISTRATION(false),
+
+ /**
+ * In this context, a user profile is managed by themselves when updating their email through an application initiated action.
+ */
UPDATE_EMAIL(false);
- protected boolean resetEmailVerified;
+ private boolean resetEmailVerified;
- private UserProfileContext(boolean resetEmailVerified){
+ UserProfileContext(boolean resetEmailVerified){
this.resetEmailVerified = resetEmailVerified;
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java
index 5dd627daa92..e528e27c778 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java
@@ -166,7 +166,7 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
profile.update((attributeName, userModel, oldValue) -> {
if (attributeName.equals(UserModel.EMAIL)) {
context.getAuthenticationSession().setAuthNote(UPDATE_PROFILE_EMAIL_CHANGED, "true");
- event.clone().event(EventType.UPDATE_EMAIL).detail(Details.CONTEXT, UserProfileContext.IDP_REVIEW.name()).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, profile.getAttributes().getFirstValue(UserModel.EMAIL)).success();
+ event.clone().event(EventType.UPDATE_EMAIL).detail(Details.CONTEXT, UserProfileContext.IDP_REVIEW.name()).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, profile.getAttributes().getFirst(UserModel.EMAIL)).success();
}
});
} catch (ValidationException pve) {
@@ -187,7 +187,7 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
logger.debugf("Profile updated successfully after first authentication with identity provider '%s' for broker user '%s'.", brokerContext.getIdpConfig().getAlias(), userCtx.getUsername());
- String newEmail = profile.getAttributes().getFirstValue(UserModel.EMAIL);
+ String newEmail = profile.getAttributes().getFirst(UserModel.EMAIL);
event.detail(Details.UPDATED_EMAIL, newEmail);
diff --git a/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java b/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java
index 21fd8c1601c..cd40519f929 100755
--- a/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java
+++ b/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java
@@ -39,6 +39,7 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.validation.Validation;
+import org.keycloak.userprofile.Attributes;
import org.keycloak.userprofile.UserProfileContext;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.ValidationException;
@@ -71,11 +72,11 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
context.getEvent().detail(Details.REGISTER_METHOD, "form");
UserProfile profile = getOrCreateUserProfile(context, formData);
- String email = profile.getAttributes().getFirstValue(UserModel.EMAIL);
-
- String username = profile.getAttributes().getFirstValue(UserModel.USERNAME);
- String firstName = profile.getAttributes().getFirstValue(UserModel.FIRST_NAME);
- String lastName = profile.getAttributes().getFirstValue(UserModel.LAST_NAME);
+ Attributes attributes = profile.getAttributes();
+ String email = attributes.getFirst(UserModel.EMAIL);
+ String username = attributes.getFirst(UserModel.USERNAME);
+ String firstName = attributes.getFirst(UserModel.FIRST_NAME);
+ String lastName = attributes.getFirst(UserModel.LAST_NAME);
context.getEvent().detail(Details.EMAIL, email);
context.getEvent().detail(Details.USERNAME, username);
@@ -92,7 +93,7 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
List errors = Validation.getFormErrorsFromValidation(pve.getErrors());
if (pve.hasError(Messages.EMAIL_EXISTS, Messages.INVALID_EMAIL)) {
- context.getEvent().detail(Details.EMAIL, profile.getAttributes().getFirstValue(UserModel.EMAIL));
+ context.getEvent().detail(Details.EMAIL, attributes.getFirst(UserModel.EMAIL));
}
if (pve.hasError(Messages.EMAIL_EXISTS)) {
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateEmail.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateEmail.java
index d136af06ed6..d2066af3f01 100644
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateEmail.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateEmail.java
@@ -168,7 +168,7 @@ public class UpdateEmail implements RequiredActionProvider, RequiredActionFactor
public static void updateEmailNow(EventBuilder event, UserModel user, UserProfile emailUpdateValidationResult) {
String oldEmail = user.getEmail();
- String newEmail = emailUpdateValidationResult.getAttributes().getFirstValue(UserModel.EMAIL);
+ String newEmail = emailUpdateValidationResult.getAttributes().getFirst(UserModel.EMAIL);
event.event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, newEmail);
emailUpdateValidationResult.update(false, new EventAuditingAttributeChangeListener(emailUpdateValidationResult, event));
}
diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java
index 43a416411bc..f3a38985e79 100755
--- a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java
+++ b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java
@@ -24,12 +24,9 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
-import java.util.Objects;
import java.util.Properties;
import java.util.Set;
-import java.util.function.Consumer;
import java.util.function.Function;
-import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -66,12 +63,9 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.ModelToRepresentation;
-import org.keycloak.provider.ConfiguredProvider;
import org.keycloak.representations.account.ClientRepresentation;
import org.keycloak.representations.account.ConsentRepresentation;
import org.keycloak.representations.account.ConsentScopeRepresentation;
-import org.keycloak.representations.idm.UserProfileAttributeMetadata;
-import org.keycloak.representations.idm.UserProfileMetadata;
import org.keycloak.representations.account.UserRepresentation;
import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
@@ -80,7 +74,6 @@ import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.UserConsentManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.account.resources.ResourcesService;
-import org.keycloak.services.resources.admin.UserProfileResource;
import org.keycloak.services.util.ResolveRelative;
import org.keycloak.storage.ReadOnlyException;
import org.keycloak.theme.Theme;
@@ -92,8 +85,6 @@ import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.EventAuditingAttributeChangeListener;
import org.keycloak.userprofile.ValidationException;
import org.keycloak.userprofile.ValidationException.Error;
-import org.keycloak.utils.GroupUtils;
-import org.keycloak.validate.Validators;
/**
* @author Stian Thorgersen
@@ -142,36 +133,15 @@ public class AccountRestService {
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
UserModel user = auth.getUser();
-
- UserRepresentation rep = new UserRepresentation();
- rep.setId(user.getId());
-
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
UserProfile profile = provider.create(UserProfileContext.ACCOUNT, user);
+ UserRepresentation rep = profile.toRepresentation();
- rep.setAttributes(profile.getAttributes().getReadable(false));
-
- addReadableBuiltinAttributes(user, rep, profile.getAttributes().getReadable(true).keySet());
-
- if(userProfileMetadata == null || userProfileMetadata.booleanValue())
- rep.setUserProfileMetadata(UserProfileResource.createUserProfileMetadata(session, profile));
-
- return rep;
- }
-
- private void addReadableBuiltinAttributes(UserModel user, UserRepresentation rep, Set 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 void setIfReadable(String attributeName, Set readableAttributes, Consumer setter, Supplier getter) {
- if (readableAttributes.contains(attributeName)) {
- setter.accept(getter.get());
+ if (userProfileMetadata != null && !userProfileMetadata) {
+ rep.setUserProfileMetadata(null);
}
+
+ return rep;
}
@Path("/")
@@ -185,7 +155,7 @@ public class AccountRestService {
event.event(EventType.UPDATE_PROFILE).detail(Details.CONTEXT, UserProfileContext.ACCOUNT.name());
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
- UserProfile profile = profileProvider.create(UserProfileContext.ACCOUNT, rep.toAttributes(), auth.getUser());
+ UserProfile profile = profileProvider.create(UserProfileContext.ACCOUNT, rep.getRawAttributes(), auth.getUser());
try {
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserProfileResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserProfileResource.java
index a5beb764e40..7c86ba6abb8 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/UserProfileResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UserProfileResource.java
@@ -16,13 +16,9 @@
*/
package org.keycloak.services.resources.admin;
+import static org.keycloak.userprofile.UserProfileUtil.createUserProfileMetadata;
+
import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.function.Function;
-import java.util.stream.Collectors;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
@@ -117,56 +113,4 @@ public class UserProfileResource {
return Response.ok(t.getConfiguration()).type(MediaType.APPLICATION_JSON).build();
}
-
- public static UserProfileMetadata createUserProfileMetadata(KeycloakSession session, UserProfile profile) {
- Attributes profileAttributes = profile.getAttributes();
- Map> am = profileAttributes.getReadable();
-
- if(am == null)
- return null;
- Map> unmanagedAttributes = profileAttributes.getUnmanagedAttributes();
-
- List 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 groups = config.getGroups().stream().map(new Function() {
- @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> 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));
- }
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
index 7d21d4eb4a9..70e1479aee2 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
@@ -123,7 +123,6 @@ import java.util.stream.Stream;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
-import static org.keycloak.services.resources.admin.UserProfileResource.createUserProfileMetadata;
import static org.keycloak.userprofile.UserProfileContext.USER_API;
/**
@@ -184,7 +183,7 @@ public class UserResource {
wasPermanentlyLockedOut = session.getProvider(BruteForceProtector.class).isPermanentlyLockedOut(session, realm, user);
}
- Map> attributes = new HashMap<>(rep.toAttributes());
+ Map> attributes = new HashMap<>(rep.getRawAttributes());
if (rep.getAttributes() == null) {
// include existing attributes in case no attributes are set so that validation takes into account the existing
@@ -302,7 +301,9 @@ public class UserResource {
) {
auth.users().requireView(user);
- UserRepresentation rep = ModelToRepresentation.toRepresentation(session, realm, user);
+ UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
+ UserProfile profile = provider.create(USER_API, user);
+ UserRepresentation rep = profile.toRepresentation();
if (realm.isIdentityFederationEnabled()) {
List reps = getFederatedIdentities(user).collect(Collectors.toList());
@@ -314,16 +315,8 @@ public class UserResource {
}
rep.setAccess(auth.users().getAccess(user));
- UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
- UserProfile profile = provider.create(USER_API, user);
- Map> readableAttributes = profile.getAttributes().getReadable(false);
-
- if (rep.getAttributes() != null) {
- rep.setAttributes(readableAttributes);
- }
-
- if (userProfileMetadata) {
- rep.setUserProfileMetadata(createUserProfileMetadata(session, profile));
+ if (!userProfileMetadata) {
+ rep.setUserProfileMetadata(null);
}
return rep;
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index 55aded752a4..6b180ac8656 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -154,7 +154,7 @@ public class UsersResource {
UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class);
- UserProfile profile = profileProvider.create(USER_API, rep.toAttributes());
+ UserProfile profile = profileProvider.create(USER_API, rep.getRawAttributes());
try {
Response response = UserResource.validateUserProfile(profile, session, auth.adminAuth());
diff --git a/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java
index 8da699d60c2..f96417691c9 100644
--- a/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java
+++ b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java
@@ -117,6 +117,7 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
}
return new DefaultAttributes(context, attributes, user, metadata, session);
}
+
return new LegacyAttributes(context, attributes, user, metadata, session);
}
@@ -153,11 +154,11 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
@Override
public UserModel apply(Attributes attributes) {
if (user == null) {
- String userName = attributes.getFirstValue(UserModel.USERNAME);
+ String userName = attributes.getFirst(UserModel.USERNAME);
// fallback to email in case email is allowed
if (userName == null) {
- userName = attributes.getFirstValue(UserModel.EMAIL);
+ userName = attributes.getFirst(UserModel.EMAIL);
}
user = session.users().addUser(session.getContext().getRealm(), userName);
diff --git a/services/src/main/java/org/keycloak/userprofile/validator/ImmutableAttributeValidator.java b/services/src/main/java/org/keycloak/userprofile/validator/ImmutableAttributeValidator.java
index d76d5575eb9..a753ccb5c8e 100644
--- a/services/src/main/java/org/keycloak/userprofile/validator/ImmutableAttributeValidator.java
+++ b/services/src/main/java/org/keycloak/userprofile/validator/ImmutableAttributeValidator.java
@@ -75,7 +75,7 @@ public class ImmutableAttributeValidator implements SimpleValidator {
return context;
}
- List email = attributeContext.getAttributes().getValues(UserModel.EMAIL);
+ List email = attributeContext.getAttributes().get(UserModel.EMAIL);
if (UserModel.USERNAME.equals(attributeName) && collectionEquals(values, email)) {
return context;
diff --git a/services/src/main/java/org/keycloak/userprofile/validator/UsernameMutationValidator.java b/services/src/main/java/org/keycloak/userprofile/validator/UsernameMutationValidator.java
index f06da402d3f..4843484877f 100644
--- a/services/src/main/java/org/keycloak/userprofile/validator/UsernameMutationValidator.java
+++ b/services/src/main/java/org/keycloak/userprofile/validator/UsernameMutationValidator.java
@@ -70,7 +70,7 @@ public class UsernameMutationValidator implements SimpleValidator {
if (!realm.isEditUsernameAllowed() && user != null && !value.equals(user.getFirstAttribute(UserModel.USERNAME))) {
Attributes attributes = attributeContext.getAttributes();
- if (realm.isRegistrationEmailAsUsername() && value.equals(attributes.getFirstValue(UserModel.EMAIL))) {
+ if (realm.isRegistrationEmailAsUsername() && value.equals(attributes.getFirst(UserModel.EMAIL))) {
// if username changed is because email as username is allowed so no validation should happen for update profile
// it is expected that username changes when attributes are normalized by the provider
return context;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceReadOnlyAttributesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceReadOnlyAttributesTest.java
index 642fc6171da..68dc72f3225 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceReadOnlyAttributesTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceReadOnlyAttributesTest.java
@@ -19,6 +19,8 @@
package org.keycloak.testsuite.account;
import java.io.IOException;
+import java.util.Map;
+import java.util.Optional;
import jakarta.ws.rs.BadRequestException;
@@ -99,7 +101,7 @@ public class AccountRestServiceReadOnlyAttributesTest extends AbstractRestServic
private void testAccountUpdateAttributeExpectFailure(String attrName, boolean deniedForAdminAsWell) throws IOException {
// Attribute not yet supposed to be on the user
UserRepresentation user = SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
- assertThat(user.getAttributes().keySet(), not(contains(attrName)));
+ assertThat(Optional.ofNullable(user.getAttributes()).orElse(Map.of()).keySet(), not(contains(attrName)));
// Assert not possible to add the attribute to the user
user.singleAttribute(attrName, "foo");
@@ -147,7 +149,7 @@ public class AccountRestServiceReadOnlyAttributesTest extends AbstractRestServic
private void testAccountUpdateAttributeExpectSuccess(String attrName) throws IOException {
// Attribute not yet supposed to be on the user
UserRepresentation user = SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class);
- assertThat(user.getAttributes().keySet(), not(contains(attrName)));
+ assertThat(Optional.ofNullable(user.getAttributes()).orElse(Map.of()).keySet(), not(contains(attrName)));
// Assert not possible to add the attribute to the user
user.singleAttribute(attrName, "foo");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java
index f4cc39539ff..5228ce3e779 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java
@@ -79,6 +79,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.stream.Collectors;
import static org.hamcrest.CoreMatchers.is;
@@ -278,7 +279,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
assertEquals("Brady", user.getLastName());
assertEquals("test-user@localhost", user.getEmail());
assertFalse(user.isEmailVerified());
- assertTrue(user.getAttributes().isEmpty());
+ assertNull(user.getAttributes());
}
@Test
@@ -288,7 +289,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
String originalFirstName = user.getFirstName();
String originalLastName = user.getLastName();
String originalEmail = user.getEmail();
- Map> originalAttributes = new HashMap<>(user.getAttributes());
+ user.setAttributes(Optional.ofNullable(user.getAttributes()).orElse(new HashMap<>()));
try {
RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
@@ -316,7 +317,6 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
user.setFirstName(originalFirstName);
user.setLastName(originalLastName);
user.setEmail(originalEmail);
- user.setAttributes(originalAttributes);
SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asResponse();
System.out.println(response.asString());
assertEquals(204, response.getStatus());
@@ -379,7 +379,8 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
String originalFirstName = user.getFirstName();
String originalLastName = user.getLastName();
String originalEmail = user.getEmail();
- Map> originalAttributes = new HashMap<>(user.getAttributes());
+ assertNull(user.getAttributes());
+ user.setAttributes(new HashMap<>());
try {
RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
@@ -417,7 +418,6 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
user.setFirstName(originalFirstName);
user.setLastName(originalLastName);
user.setEmail(originalEmail);
- user.setAttributes(originalAttributes);
SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asResponse();
System.out.println(response.asString());
assertEquals(204, response.getStatus());
@@ -431,7 +431,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
String originalFirstName = user.getFirstName();
String originalLastName = user.getLastName();
String originalEmail = user.getEmail();
- Map> originalAttributes = new HashMap<>(user.getAttributes());
+ user.setAttributes(Optional.ofNullable(user.getAttributes()).orElse(new HashMap<>()));
try {
RealmRepresentation realmRep = adminClient.realm("test").toRepresentation();
@@ -460,12 +460,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
user = updateAndGet(user);
- if (isDeclarativeUserProfile()) {
- assertEquals(2, user.getAttributes().size());
- assertTrue(user.getAttributes().get("attr1").isEmpty());
- } else {
- assertEquals(1, user.getAttributes().size());
- }
+ assertEquals(1, user.getAttributes().size());
assertEquals(2, user.getAttributes().get("attr2").size());
assertThat(user.getAttributes().get("attr2"), containsInAnyOrder("val2", "val3"));
@@ -522,7 +517,6 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
user.setFirstName(originalFirstName);
user.setLastName(originalLastName);
user.setEmail(originalEmail);
- user.setAttributes(originalAttributes);
SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asResponse();
System.out.println(response.asString());
assertEquals(204, response.getStatus());
@@ -556,6 +550,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
public void testUpdateProfileCannotChangeThroughAttributes() throws IOException {
UserRepresentation user = getUser();
String originalUsername = user.getUsername();
+ user.setAttributes(Optional.ofNullable(user.getAttributes()).orElse(new HashMap<>()));
Map> originalAttributes = new HashMap<>(user.getAttributes());
try {
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceWithUserProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceWithUserProfileTest.java
index ba7d5bc8cf5..39e8c0d5946 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceWithUserProfileTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceWithUserProfileTest.java
@@ -29,6 +29,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
@@ -292,6 +293,7 @@ public class AccountRestServiceWithUserProfileTest extends AccountRestServiceTes
String originalFirstName = user.getFirstName();
String originalLastName = user.getLastName();
String originalEmail = user.getEmail();
+ user.setAttributes(Optional.ofNullable(user.getAttributes()).orElse(new HashMap<>()));
Map> originalAttributes = new HashMap<>(user.getAttributes());
try {
@@ -303,13 +305,13 @@ public class AccountRestServiceWithUserProfileTest extends AccountRestServiceTes
user.setEmail("bobby@localhost");
user.setFirstName("Homer");
user.setLastName("Simpsons");
- user.getAttributes().put("attr1", Collections.singletonList("val1"));
- user.getAttributes().put("attr2", Collections.singletonList("val2"));
+ user.getAttributes().put("attr1", Collections.singletonList("val11"));
+ user.getAttributes().put("attr2", Collections.singletonList("val22"));
+ events.clear();
user = updateAndGet(user);
//skip login to the REST API event
- events.poll();
events.expectAccount(EventType.UPDATE_PROFILE).user(user.getId())
.detail(Details.CONTEXT, UserProfileContext.ACCOUNT.name())
.detail(Details.PREVIOUS_EMAIL, originalEmail)
@@ -318,7 +320,7 @@ public class AccountRestServiceWithUserProfileTest extends AccountRestServiceTes
.detail(Details.PREVIOUS_LAST_NAME, originalLastName)
.detail(Details.UPDATED_FIRST_NAME, "Homer")
.detail(Details.UPDATED_LAST_NAME, "Simpsons")
- .detail(Details.PREF_UPDATED+"attr2", "val2")
+ .detail(Details.PREF_UPDATED+"attr2", "val22")
.assertEvent();
events.assertEmpty();
@@ -379,22 +381,23 @@ public class AccountRestServiceWithUserProfileTest extends AccountRestServiceTes
realmRep.setInternationalizationEnabled(false);
testRealm().update(realmRep);
UserRepresentation user = getUser();
+ user.setAttributes(Optional.ofNullable(user.getAttributes()).orElse(new HashMap<>()));
try {
user.getAttributes().put(UserModel.LOCALE, List.of("pt_BR"));
user = updateAndGet(user);
- assertNull(user.getAttributes().get(UserModel.LOCALE));
+ assertNull(user.getAttributes());
realmRep.setInternationalizationEnabled(true);
testRealm().update(realmRep);
- user.getAttributes().put(UserModel.LOCALE, List.of("pt_BR"));
+ user.singleAttribute(UserModel.LOCALE, "pt_BR");
user = updateAndGet(user);
assertEquals("pt_BR", user.getAttributes().get(UserModel.LOCALE).get(0));
user.getAttributes().remove(UserModel.LOCALE);
user = updateAndGet(user);
- assertNull(user.getAttributes().get(UserModel.LOCALE));
+ assertNull(user.getAttributes());
UserProfileMetadata metadata = user.getUserProfileMetadata();
@@ -406,7 +409,6 @@ public class AccountRestServiceWithUserProfileTest extends AccountRestServiceTes
} finally {
realmRep.setInternationalizationEnabled(internationalizationEnabled);
testRealm().update(realmRep);
- user.getAttributes().remove(UserModel.LOCALE);
updateAndGet(user);
}
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/DeclarativeUserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/DeclarativeUserTest.java
index c01783cfa8b..6439718964d 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/DeclarativeUserTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/DeclarativeUserTest.java
@@ -37,6 +37,7 @@ import org.keycloak.userprofile.UserProfileProvider;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -52,7 +53,6 @@ import jakarta.ws.rs.core.Response;
@EnableFeature(value = Profile.Feature.DECLARATIVE_USER_PROFILE)
public class DeclarativeUserTest extends AbstractAdminTest {
- private static final String LOCALE_ATTR_KEY = "locale";
private static final String TEST_REALM_USER_MANAGER_NAME = "test-realm-user-manager";
private static final String REQUIRED_ATTR_KEY = "required-attr";
@@ -120,29 +120,6 @@ public class DeclarativeUserTest extends AbstractAdminTest {
}
}
- @Test
- public void testReturnAllConfiguredAttributesEvenIfNotSet() {
- UserRepresentation user1 = new UserRepresentation();
- user1.setUsername("user1");
- user1.singleAttribute("attr1", "value1user1");
- user1.singleAttribute("attr2", "value2user1");
- String user1Id = createUser(user1);
-
- user1 = realm.users().get(user1Id).toRepresentation();
- Map> attributes = user1.getAttributes();
- assertEquals(4, attributes.size());
- List attr1 = attributes.get("attr1");
- assertEquals(1, attr1.size());
- assertEquals("value1user1", attr1.get(0));
- List attr2 = attributes.get("attr2");
- assertEquals(1, attr2.size());
- assertEquals("value2user1", attr2.get(0));
- List attrCustomA = attributes.get("custom-a");
- assertTrue(attrCustomA.isEmpty());
- assertTrue(attributes.containsKey("custom-a"));
- assertTrue(attributes.containsKey("aName"));
- }
-
@Test
public void testDoNotReturnAttributeIfNotReadble() {
UserRepresentation user1 = new UserRepresentation();
@@ -153,7 +130,7 @@ public class DeclarativeUserTest extends AbstractAdminTest {
user1 = realm.users().get(user1Id).toRepresentation();
Map> attributes = user1.getAttributes();
- assertEquals(4, attributes.size());
+ assertEquals(2, attributes.size());
assertFalse(attributes.containsKey("custom-hidden"));
setUserProfileConfiguration(this.realm, "{\"attributes\": ["
@@ -170,8 +147,8 @@ public class DeclarativeUserTest extends AbstractAdminTest {
user1 = realm.users().get(user1Id).toRepresentation();
attributes = user1.getAttributes();
- assertEquals(5, attributes.size());
- assertTrue(attributes.containsKey("custom-hidden"));
+ assertEquals(2, attributes.size());
+ assertFalse(attributes.containsKey("custom-hidden"));
}
@Test
@@ -200,12 +177,17 @@ public class DeclarativeUserTest extends AbstractAdminTest {
UserResource userResource = realm.users().get(user1Id);
user1 = userResource.toRepresentation();
- Map> attributes = user1.getAttributes();
- attributes.put("attr2", Collections.singletonList(""));
+ assertNull(user1.getAttributes());
+ user1.singleAttribute("attr2", "");
// should be able to update the user when a read-only attribute has an empty or null value
userResource.update(user1);
- attributes.put("attr2", null);
+ user1 = userResource.toRepresentation();
+ assertNull(user1.getAttributes());
+ user1.setAttributes(new HashMap<>());
+ user1.getAttributes().put("attr2", null);
userResource.update(user1);
+ user1 = userResource.toRepresentation();
+ assertNull(user1.getAttributes());
}
@Test
@@ -288,7 +270,7 @@ public class DeclarativeUserTest extends AbstractAdminTest {
realm.update(realmRep);
user1 = userResource.toRepresentation();
- assertNull(user1.getAttributes().get(UserModel.LOCALE));
+ assertNull(user1.getAttributes());
} finally {
realmRep.setInternationalizationEnabled(internationalizationEnabled);
realm.update(realmRep);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
index 4ac685b991b..78a6e134fb0 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
@@ -1529,20 +1529,12 @@ public class UserTest extends AbstractAdminTest {
String user2Id = createUser(user2);
user1 = realm.users().get(user1Id).toRepresentation();
- if (isDeclarativeUserProfile()) {
- assertEquals(managedAttributes.size(), user1.getAttributes().size());
- } else {
- assertEquals(2, user1.getAttributes().size());
- }
+ assertEquals(2, user1.getAttributes().size());
assertAttributeValue("value1user1", user1.getAttributes().get("attr1"));
assertAttributeValue("value2user1", user1.getAttributes().get("attr2"));
user2 = realm.users().get(user2Id).toRepresentation();
- if (isDeclarativeUserProfile()) {
- assertEquals(managedAttributes.size(), user2.getAttributes().size());
- } else {
- assertEquals(2, user2.getAttributes().size());
- }
+ assertEquals(2, user2.getAttributes().size());
assertAttributeValue("value1user2", user2.getAttributes().get("attr1"));
vals = user2.getAttributes().get("attr2");
assertEquals(2, vals.size());
@@ -1554,11 +1546,7 @@ public class UserTest extends AbstractAdminTest {
updateUser(realm.users().get(user1Id), user1);
user1 = realm.users().get(user1Id).toRepresentation();
- if (isDeclarativeUserProfile()) {
- assertEquals(managedAttributes.size(), user1.getAttributes().size());
- } else {
- assertEquals(3, user1.getAttributes().size());
- }
+ assertEquals(3, user1.getAttributes().size());
assertAttributeValue("value3user1", user1.getAttributes().get("attr1"));
assertAttributeValue("value2user1", user1.getAttributes().get("attr2"));
assertAttributeValue("value4user1", user1.getAttributes().get("attr3"));
@@ -1567,11 +1555,7 @@ public class UserTest extends AbstractAdminTest {
updateUser(realm.users().get(user1Id), user1);
user1 = realm.users().get(user1Id).toRepresentation();
- if (isDeclarativeUserProfile()) {
- assertEquals(managedAttributes.size(), user1.getAttributes().size());
- } else {
- assertEquals(2, user1.getAttributes().size());
- }
+ assertEquals(2, user1.getAttributes().size());
assertAttributeValue("value2user1", user1.getAttributes().get("attr2"));
assertAttributeValue("value4user1", user1.getAttributes().get("attr3"));
@@ -1580,11 +1564,7 @@ public class UserTest extends AbstractAdminTest {
updateUser(realm.users().get(user1Id), user1);
user1 = realm.users().get(user1Id).toRepresentation();
assertNotNull(user1.getAttributes());
- if (isDeclarativeUserProfile()) {
- assertEquals(managedAttributes.size(), user1.getAttributes().size());
- } else {
- assertEquals(2, user1.getAttributes().size());
- }
+ assertEquals(2, user1.getAttributes().size());
// empty attributes should remove attributes
user1.setAttributes(Collections.emptyMap());
@@ -1602,21 +1582,13 @@ public class UserTest extends AbstractAdminTest {
realm.users().get(user1Id).update(user1);
user1 = realm.users().get(user1Id).toRepresentation();
- if (isDeclarativeUserProfile()) {
- assertEquals(managedAttributes.size(), user1.getAttributes().size());
- } else {
- assertEquals(2, user1.getAttributes().size());
- }
+ assertEquals(2, user1.getAttributes().size());
user1.getAttributes().remove("foo");
realm.users().get(user1Id).update(user1);
user1 = realm.users().get(user1Id).toRepresentation();
- if (isDeclarativeUserProfile()) {
- assertEquals(managedAttributes.size(), user1.getAttributes().size());
- } else {
- assertEquals(1, user1.getAttributes().size());
- }
+ assertEquals(1, user1.getAttributes().size());
}
@Test
@@ -1669,11 +1641,7 @@ public class UserTest extends AbstractAdminTest {
user1 = realm.users().get(user1Id).toRepresentation();
assertEquals("foo", user1.getAttributes().get("usercertificate").get(0));
assertEquals("bar", user1.getAttributes().get("saml.persistent.name.id.for.foo").get(0));
- if (isDeclarativeUserProfile()) {
- assertTrue(user1.getAttributes().get(LDAPConstants.LDAP_ID).isEmpty());
- } else {
- assertFalse(user1.getAttributes().containsKey(LDAPConstants.LDAP_ID));
- }
+ assertFalse(user1.getAttributes().containsKey(LDAPConstants.LDAP_ID));
}
@Test
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPAdminRestApiWithUserProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPAdminRestApiWithUserProfileTest.java
index 00779ebd38c..e24648ffbd5 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPAdminRestApiWithUserProfileTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPAdminRestApiWithUserProfileTest.java
@@ -18,6 +18,8 @@
package org.keycloak.testsuite.federation.ldap;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.keycloak.testsuite.forms.VerifyProfileTest.disableDynamicUserProfile;
@@ -71,14 +73,16 @@ public class LDAPAdminRestApiWithUserProfileTest extends LDAPAdminRestApiTest {
UserResource user = testRealm().users().get(newUserId);
UserRepresentation userRep = user.toRepresentation();
-
- assertTrue(userRep.getAttributes().containsKey(LDAPConstants.LDAP_ID));
- assertTrue(userRep.getAttributes().get(LDAPConstants.LDAP_ID).isEmpty());
+ assertNull(userRep.getAttributes());
userRep.singleAttribute(LDAPConstants.LDAP_ID, "");
user.update(userRep);
+ userRep = testRealm().users().get(newUserId).toRepresentation();
+ assertNull(userRep.getAttributes());
userRep.singleAttribute(LDAPConstants.LDAP_ID, null);
user.update(userRep);
+ userRep = testRealm().users().get(newUserId).toRepresentation();
+ assertNull(userRep.getAttributes());
try {
userRep.singleAttribute(LDAPConstants.LDAP_ID, "should-fail");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java
index fb56277ae6d..7d38e5edba9 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java
@@ -55,6 +55,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
+import org.keycloak.representations.idm.AbstractUserRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
@@ -114,6 +115,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
// create a user with attribute foo value 123 allowed by the profile now but disallowed later
UPConfig config = parseDefaultConfig();
config.addOrReplaceAttribute(new UPAttribute("foo", new UPAttributePermissions(Set.of(), Set.of(ROLE_ADMIN))));
+ config.getAttribute(UserModel.EMAIL).setPermissions(new UPAttributePermissions(Set.of(ROLE_USER), Set.of(ROLE_ADMIN)));
RealmResource realmRes = testRealm();
realmRes.users().userProfile().update(config);
@@ -142,6 +144,18 @@ public class UserProfileTest extends AbstractUserProfileTest {
profile.validate();
});
+ // it should work if foo is read-only in the context
+ getTestingClient().server(TEST_REALM_NAME).run(session -> {
+ RealmModel realm = session.getContext().getRealm();
+ UserModel user = session.users().getUserById(realm, userId);
+ user.setEmail(null);
+ UserProfileProvider provider = getUserProfileProvider(session);
+ Map attributes = new HashMap<>(user.getAttributes());
+ attributes.put("email", "");
+ UserProfile profile = provider.create(UserProfileContext.ACCOUNT, attributes, user);
+ profile.validate();
+ });
+
// it should fail if foo can be modified
getTestingClient().server(TEST_REALM_NAME).run(session -> {
RealmModel realm = session.getContext().getRealm();
@@ -174,7 +188,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
// once created, profile attributes can not be changed
assertTrue(profile.getAttributes().contains(UserModel.USERNAME));
- assertNull(profile.getAttributes().getFirstValue(UserModel.USERNAME));
+ assertNull(profile.getAttributes().getFirst(UserModel.USERNAME));
}
@Test
@@ -413,11 +427,11 @@ public class UserProfileTest extends AbstractUserProfileTest {
assertTrue(ve.isAttributeOnError("address"));
}
- assertNotNull(attributes.getFirstValue(UserModel.USERNAME));
- assertNotNull(attributes.getFirstValue(UserModel.EMAIL));
- assertNotNull(attributes.getFirstValue(UserModel.FIRST_NAME));
- assertNotNull(attributes.getFirstValue(UserModel.LAST_NAME));
- assertNull(attributes.getFirstValue("address"));
+ assertNotNull(attributes.getFirst(UserModel.USERNAME));
+ assertNotNull(attributes.getFirst(UserModel.EMAIL));
+ assertNotNull(attributes.getFirst(UserModel.FIRST_NAME));
+ assertNotNull(attributes.getFirst(UserModel.LAST_NAME));
+ assertNull(attributes.getFirst("address"));
user.setAttribute("address", Arrays.asList("fixed-address"));
@@ -426,7 +440,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
profile.validate();
- assertNotNull(attributes.getFirstValue("address"));
+ assertNotNull(attributes.getFirst("address"));
}
@Test
@@ -1516,20 +1530,20 @@ public class UserProfileTest extends AbstractUserProfileTest {
profile = provider.create(UserProfileContext.USER_API, user);
Attributes userAttributes = profile.getAttributes();
- assertEquals("new-email@test.com", userAttributes.getFirstValue(UserModel.EMAIL));
- assertEquals("Test Value", userAttributes.getFirstValue("test-attribute"));
- assertEquals("changed", userAttributes.getFirstValue("foo"));
+ assertEquals("new-email@test.com", userAttributes.getFirst(UserModel.EMAIL));
+ assertEquals("Test Value", userAttributes.getFirst("test-attribute"));
+ assertEquals("changed", userAttributes.getFirst("foo"));
attributes.remove("foo");
- attributes.put("test-attribute", userAttributes.getFirstValue("test-attribute"));
+ attributes.put("test-attribute", userAttributes.getFirst("test-attribute"));
profile = provider.create(UserProfileContext.USER_API, attributes, user);
profile.update(true);
profile = provider.create(UserProfileContext.USER_API, user);
userAttributes = profile.getAttributes();
// remove attribute if not set
- assertEquals("new-email@test.com", userAttributes.getFirstValue(UserModel.EMAIL));
- assertEquals("Test Value", userAttributes.getFirstValue("test-attribute"));
- assertNull(userAttributes.getFirstValue("foo"));
+ assertEquals("new-email@test.com", userAttributes.getFirst(UserModel.EMAIL));
+ assertEquals("Test Value", userAttributes.getFirst("test-attribute"));
+ assertNull(userAttributes.getFirst("foo"));
config.addOrReplaceAttribute(new UPAttribute("test-attribute", new UPAttributePermissions(Set.of(), Set.of(ROLE_USER))));
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
@@ -1539,8 +1553,8 @@ public class UserProfileTest extends AbstractUserProfileTest {
profile = provider.create(UserProfileContext.USER_API, user);
userAttributes = profile.getAttributes();
// do not remove test-attribute because admin does not have write permissions
- assertEquals("new-email@test.com", userAttributes.getFirstValue(UserModel.EMAIL));
- assertEquals("Test Value", userAttributes.getFirstValue("test-attribute"));
+ assertEquals("new-email@test.com", userAttributes.getFirst(UserModel.EMAIL));
+ assertEquals("Test Value", userAttributes.getFirst("test-attribute"));
config.addOrReplaceAttribute(new UPAttribute("test-attribute", new UPAttributePermissions(Set.of(), Set.of(ROLE_USER, ROLE_ADMIN))));
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
@@ -1550,8 +1564,8 @@ public class UserProfileTest extends AbstractUserProfileTest {
profile = provider.create(UserProfileContext.USER_API, user);
userAttributes = profile.getAttributes();
// removes the test-attribute attribute because now admin has write permission
- assertEquals("new-email@test.com", userAttributes.getFirstValue(UserModel.EMAIL));
- assertNull(userAttributes.getFirstValue("test-attribute"));
+ assertEquals("new-email@test.com", userAttributes.getFirst(UserModel.EMAIL));
+ assertNull(userAttributes.getFirst("test-attribute"));
}
@Test
@@ -1594,11 +1608,11 @@ public class UserProfileTest extends AbstractUserProfileTest {
}
private static void assertRemoveEmptyRootAttribute(Map> attributes, UserModel user, Attributes upAttributes) {
- assertNull(upAttributes.getFirstValue(UserModel.LAST_NAME));
+ assertNull(upAttributes.getFirst(UserModel.LAST_NAME));
assertNull(user.getLastName());
- assertNull(upAttributes.getFirstValue(UserModel.EMAIL));
+ assertNull(upAttributes.getFirst(UserModel.EMAIL));
assertNull(user.getEmail());
- assertEquals(upAttributes.getFirstValue(UserModel.FIRST_NAME), attributes.get(UserModel.FIRST_NAME).get(0));
+ assertEquals(upAttributes.getFirst(UserModel.FIRST_NAME), attributes.get(UserModel.FIRST_NAME).get(0));
}
@Test
@@ -1693,6 +1707,77 @@ public class UserProfileTest extends AbstractUserProfileTest {
assertFalse(profile.getAttributes().isReadOnly("foo"));
}
+ @Test
+ public void testOptionalRootAttributesAsUnmanagedAttribute() {
+ getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testOptionalRootAttributesAsUnmanagedAttribute);
+ }
+
+ private static void testOptionalRootAttributesAsUnmanagedAttribute(KeycloakSession session) throws IOException {
+ UPConfig config = parseDefaultConfig();
+ UserProfileProvider provider = getUserProfileProvider(session);
+ provider.setConfiguration(JsonSerialization.writeValueAsString(config));
+ Map rawAttributes = new HashMap<>();
+ rawAttributes.put(UserModel.USERNAME, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org");
+ rawAttributes.put(UserModel.EMAIL, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org");
+ rawAttributes.put(UserModel.FIRST_NAME, "firstName");
+ rawAttributes.put(UserModel.LAST_NAME, "lastName");
+ UserProfile profile = provider.create(UserProfileContext.USER_API, rawAttributes);
+ UserModel user = profile.create();
+ assertEquals(rawAttributes.get(UserModel.FIRST_NAME), user.getFirstName());
+ assertEquals(rawAttributes.get(UserModel.LAST_NAME), user.getLastName());
+ AbstractUserRepresentation rep = profile.toRepresentation();
+ assertEquals(rawAttributes.get(UserModel.FIRST_NAME), rep.getFirstName());
+ assertEquals(rawAttributes.get(UserModel.LAST_NAME), rep.getLastName());
+ assertNull(rep.getAttributes());
+
+ config.removeAttribute(UserModel.FIRST_NAME);
+ config.removeAttribute(UserModel.LAST_NAME);
+ provider.setConfiguration(JsonSerialization.writeValueAsString(config));
+ profile = provider.create(UserProfileContext.USER_API, user);
+ Attributes attributes = profile.getAttributes();
+ assertNull(attributes.getFirst(UserModel.FIRST_NAME));
+ assertNull(attributes.getFirst(UserModel.LAST_NAME));
+ rep = profile.toRepresentation();
+ assertNull(rep.getFirstName());
+ assertNull(rep.getLastName());
+ assertNull(rep.getAttributes());
+
+ rawAttributes.put(UserModel.FIRST_NAME, "firstName");
+ rawAttributes.put(UserModel.LAST_NAME, "lastName");
+ config.setUnmanagedAttributePolicy(UnmanagedAttributePolicy.ADMIN_EDIT);
+ provider.setConfiguration(JsonSerialization.writeValueAsString(config));
+ profile = provider.create(UserProfileContext.USER_API, user);
+ attributes = profile.getAttributes();
+ assertEquals(rawAttributes.get(UserModel.FIRST_NAME), attributes.getFirst(UserModel.FIRST_NAME));
+ assertEquals(rawAttributes.get(UserModel.LAST_NAME), attributes.getFirst(UserModel.LAST_NAME));
+ rep = profile.toRepresentation();
+ assertNull(rep.getFirstName());
+ assertNull(rep.getLastName());
+ assertNull(rep.getAttributes());
+
+ rawAttributes.remove(UserModel.LAST_NAME);
+ rawAttributes.put(UserModel.FIRST_NAME, "firstName");
+ profile = provider.create(UserProfileContext.USER_API, rawAttributes, user);
+ attributes = profile.getAttributes();
+ assertEquals(rawAttributes.get(UserModel.FIRST_NAME), attributes.getFirst(UserModel.FIRST_NAME));
+ assertNull(attributes.getFirst(UserModel.LAST_NAME));
+ rep = profile.toRepresentation();
+ assertNull(rep.getFirstName());
+ assertNull(rep.getLastName());
+ assertNull(rep.getAttributes());
+
+ rawAttributes.put(UserModel.LAST_NAME, "lastNameChanged");
+ rawAttributes.put(UserModel.FIRST_NAME, "firstNameChanged");
+ profile = provider.create(UserProfileContext.USER_API, rawAttributes, user);
+ attributes = profile.getAttributes();
+ assertEquals(rawAttributes.get(UserModel.FIRST_NAME), attributes.getFirst(UserModel.FIRST_NAME));
+ assertEquals(rawAttributes.get(UserModel.LAST_NAME), attributes.getFirst(UserModel.LAST_NAME));
+ rep = profile.toRepresentation();
+ assertNull(rep.getFirstName());
+ assertNull(rep.getLastName());
+ assertNull(rep.getAttributes());
+ }
+
@Test
public void testAttributeNormalization() {
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testAttributeNormalization);
@@ -1705,7 +1790,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
attributes.put(UserModel.EMAIL, "TesT@TesT.org");
UserProfile profile = provider.create(UserProfileContext.USER_API, attributes);
Attributes profileAttributes = profile.getAttributes();
- assertEquals(attributes.get(UserModel.USERNAME).toLowerCase(), profileAttributes.getFirstValue(UserModel.USERNAME));
- assertEquals(attributes.get(UserModel.EMAIL).toLowerCase(), profileAttributes.getFirstValue(UserModel.EMAIL));
+ assertEquals(attributes.get(UserModel.USERNAME).toLowerCase(), profileAttributes.getFirst(UserModel.USERNAME));
+ assertEquals(attributes.get(UserModel.EMAIL).toLowerCase(), profileAttributes.getFirst(UserModel.EMAIL));
}
}