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}:
+ *
+ *
{@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);
+ * }
*
- *
A {@code UserProfile} instance can be obtained through the {@link UserProfileProvider}.
+ *
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 cfa6e0f4bd65..45059631accb 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 static boolean addMetadataAttributeToUserProfile(String attrName, UserPro
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 af5a560b0a37..a55da0e31bcc 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 @@ default String getFirstValue(String name) {
*
* @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 @@ default String getFirstValue(String name) {
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 @@ default String getFirstValue(String name) {
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.
+ * @return the attributes with read 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));
- }
+ Map> getReadable();
/**
- * Returns only the attributes that have read/write permissions.
+ * Returns the attributes as a {@link Map} that are accessible to a particular {@link UserProfileContext}.
*
- * @return the attributes with read/write permission.
+ * @return a map with all the attributes
*/
- Map> getReadable();
+ Map> toMap();
/**
- * Returns whether the attribute with the given {@code name} is a root attribute.
+ * Returns a {@link Map} holding any unmanaged attribute.
*
- * @param name the attribute name
- * @return
+ * @return a map with any unmanaged attribute
*/
- 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();
-
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 537f4ebea685..3b6e5317c0ad 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 @@
/**
*
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.
*
*