createUserDefinedProfileDecorator(KeycloakSession session, UserProfileMetadata decoratedMetadata, ComponentModel component) {
return (c) -> {
UPConfig parsedConfig = getParsedConfig(getConfigJsonFromComponentModel(component));
diff --git a/services/src/main/java/org/keycloak/userprofile/AbstractUserProfileProvider.java b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProviderFactory.java
similarity index 67%
rename from services/src/main/java/org/keycloak/userprofile/AbstractUserProfileProvider.java
rename to services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProviderFactory.java
index fe11825018..b70611132a 100644
--- a/services/src/main/java/org/keycloak/userprofile/AbstractUserProfileProvider.java
+++ b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProviderFactory.java
@@ -1,44 +1,39 @@
/*
+ * Copyright 2023 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
*
- * * Copyright 2021 Red Hat, Inc. and/or its affiliates
- * * and other contributors as indicated by the @author tags.
- * *
- * * Licensed under the Apache License, Version 2.0 (the "License");
- * * you may not use this file except in compliance with the License.
- * * You may obtain a copy of the License at
- * *
- * * http://www.apache.org/licenses/LICENSE-2.0
- * *
- * * Unless required by applicable law or agreed to in writing, software
- * * distributed under the License is distributed on an "AS IS" BASIS,
- * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * * See the License for the specific language governing permissions and
- * * limitations under the License.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*
*/
package org.keycloak.userprofile;
-import static org.keycloak.userprofile.DefaultAttributes.READ_ONLY_ATTRIBUTE_KEY;
-import static org.keycloak.userprofile.UserProfileContext.ACCOUNT;
-import static org.keycloak.userprofile.UserProfileContext.IDP_REVIEW;
-import static org.keycloak.userprofile.UserProfileContext.REGISTRATION;
-import static org.keycloak.userprofile.UserProfileContext.UPDATE_EMAIL;
-import static org.keycloak.userprofile.UserProfileContext.UPDATE_PROFILE;
-import static org.keycloak.userprofile.UserProfileContext.USER_API;
-
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
+
import org.keycloak.Config;
import org.keycloak.common.Profile;
-import org.keycloak.common.Profile.Feature;
+import org.keycloak.component.AmphibianProviderFactory;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@@ -48,6 +43,7 @@ import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.representations.userprofile.config.UPConfig;
import org.keycloak.services.messages.Messages;
+import org.keycloak.userprofile.config.UPConfigUtils;
import org.keycloak.userprofile.validator.BlankAttributeValidator;
import org.keycloak.userprofile.validator.BrokeringFederatedUsernameHasValueValidator;
import org.keycloak.userprofile.validator.DuplicateEmailValidator;
@@ -62,17 +58,40 @@ import org.keycloak.userprofile.validator.UsernameMutationValidator;
import org.keycloak.validate.ValidatorConfig;
import org.keycloak.validate.validators.EmailValidator;
-/**
- * A base class for {@link UserProfileProvider} implementations providing the main hooks for customizations.
- *
- * @author Markus Till
- */
-public abstract class AbstractUserProfileProvider implements UserProfileProvider, UserProfileProviderFactory {
+import static org.keycloak.common.util.ObjectUtil.isBlank;
+import static org.keycloak.userprofile.DefaultAttributes.READ_ONLY_ATTRIBUTE_KEY;
+import static org.keycloak.userprofile.UserProfileContext.ACCOUNT;
+import static org.keycloak.userprofile.UserProfileContext.IDP_REVIEW;
+import static org.keycloak.userprofile.UserProfileContext.REGISTRATION;
+import static org.keycloak.userprofile.UserProfileContext.UPDATE_EMAIL;
+import static org.keycloak.userprofile.UserProfileContext.UPDATE_PROFILE;
+import static org.keycloak.userprofile.UserProfileContext.USER_API;
+
+public class DeclarativeUserProfileProviderFactory implements UserProfileProviderFactory, AmphibianProviderFactory {
public static final String CONFIG_ADMIN_READ_ONLY_ATTRIBUTES = "admin-read-only-attributes";
public static final String CONFIG_READ_ONLY_ATTRIBUTES = "read-only-attributes";
public static final String MAX_EMAIL_LOCAL_PART_LENGTH = "max-email-local-part-length";
+ public static final String ID = "declarative-user-profile";
+ public static final int PROVIDER_PRIORITY = 1;
+
+ /**
+ * There are the declarations for creating the built-in validations for read-only attributes. Regardless of the context where
+ * user profiles are used. They are related to internal attributes with hard conditions on them in terms of management.
+ */
+ private static final String[] DEFAULT_READ_ONLY_ATTRIBUTES = { "KERBEROS_PRINCIPAL", "LDAP_ID", "LDAP_ENTRY_DN", "CREATED_TIMESTAMP", "createTimestamp", "modifyTimestamp", "userCertificate", "saml.persistent.name.id.for.*", "ENABLED", "EMAIL_VERIFIED", "disabledReason" };
+ private static final String[] DEFAULT_ADMIN_READ_ONLY_ATTRIBUTES = { "KERBEROS_PRINCIPAL", "LDAP_ID", "LDAP_ENTRY_DN", "CREATED_TIMESTAMP", "createTimestamp", "modifyTimestamp" };
+ private static final Pattern readOnlyAttributesPattern = getRegexPatternString(DEFAULT_READ_ONLY_ATTRIBUTES);
+ private static final Pattern adminReadOnlyAttributesPattern = getRegexPatternString(DEFAULT_ADMIN_READ_ONLY_ATTRIBUTES);
+
+ private boolean isDeclarativeConfigurationEnabled;
+
+ private String defaultRawConfig;
+ private UPConfig parsedDefaultRawConfig;
+ private final Map contextualMetadataRegistry = new HashMap<>();
+
+
private static boolean editUsernameCondition(AttributeContext c) {
KeycloakSession session = c.getSession();
KeycloakContext context = session.getContext();
@@ -119,7 +138,7 @@ public abstract class AbstractUserProfileProvider
return true;
}
- if (Profile.isFeatureEnabled(Feature.UPDATE_EMAIL)) {
+ if (Profile.isFeatureEnabled(Profile.Feature.UPDATE_EMAIL)) {
return !(UPDATE_PROFILE.equals(c.getContext()) || ACCOUNT.equals(c.getContext()));
}
@@ -137,7 +156,7 @@ public abstract class AbstractUserProfileProvider
return true;
}
- if (Profile.isFeatureEnabled(Feature.UPDATE_EMAIL)) {
+ if (Profile.isFeatureEnabled(Profile.Feature.UPDATE_EMAIL)) {
return !UPDATE_PROFILE.equals(context);
}
@@ -152,6 +171,15 @@ public abstract class AbstractUserProfileProvider
return true;
}
+ private static boolean isInternationalizationEnabled(AttributeContext context) {
+ RealmModel realm = context.getSession().getContext().getRealm();
+ return realm.isInternationalizationEnabled();
+ }
+
+ private static boolean isNewUser(AttributeContext c) {
+ return c.getUser() == null;
+ }
+
public static Pattern getRegexPatternString(String[] builtinReadOnlyAttributes) {
if (builtinReadOnlyAttributes != null) {
List readOnlyAttributes = new ArrayList<>(Arrays.asList(builtinReadOnlyAttributes));
@@ -169,59 +197,16 @@ public abstract class AbstractUserProfileProvider
return null;
}
- private static boolean isInternationalizationEnabled(AttributeContext context) {
- RealmModel realm = context.getSession().getContext().getRealm();
- return realm.isInternationalizationEnabled();
- }
-
- private static boolean isNewUser(AttributeContext c) {
- return c.getUser() == null;
- }
-
- /**
- * There are the declarations for creating the built-in validations for read-only attributes. Regardless of the context where
- * user profiles are used. They are related to internal attributes with hard conditions on them in terms of management.
- */
- private static final String[] DEFAULT_READ_ONLY_ATTRIBUTES = { "KERBEROS_PRINCIPAL", "LDAP_ID", "LDAP_ENTRY_DN", "CREATED_TIMESTAMP", "createTimestamp", "modifyTimestamp", "userCertificate", "saml.persistent.name.id.for.*", "ENABLED", "EMAIL_VERIFIED", "disabledReason" };
- private static final String[] DEFAULT_ADMIN_READ_ONLY_ATTRIBUTES = { "KERBEROS_PRINCIPAL", "LDAP_ID", "LDAP_ENTRY_DN", "CREATED_TIMESTAMP", "createTimestamp", "modifyTimestamp" };
- private static final Pattern readOnlyAttributesPattern = getRegexPatternString(DEFAULT_READ_ONLY_ATTRIBUTES);
- private static final Pattern adminReadOnlyAttributesPattern = getRegexPatternString(DEFAULT_ADMIN_READ_ONLY_ATTRIBUTES);
-
- protected final Map contextualMetadataRegistry;
- protected final KeycloakSession session;
-
- public AbstractUserProfileProvider() {
- // for reflection
- this(null, new HashMap<>());
- }
-
- public AbstractUserProfileProvider(KeycloakSession session, Map contextualMetadataRegistry) {
- this.session = session;
- this.contextualMetadataRegistry = contextualMetadataRegistry;
- }
-
- @Override
- public UserProfile create(UserProfileContext context, UserModel user) {
- return createUserProfile(context, user.getAttributes(), user);
- }
-
- @Override
- public UserProfile create(UserProfileContext context, Map attributes, UserModel user) {
- return createUserProfile(context, attributes, user);
- }
-
- @Override
- public UserProfile create(UserProfileContext context, Map attributes) {
- return createUserProfile(context, attributes, null);
- }
-
- @Override
- public U create(KeycloakSession session) {
- return create(session, contextualMetadataRegistry);
- }
-
@Override
public void init(Config.Scope config) {
+ isDeclarativeConfigurationEnabled = Profile.isFeatureEnabled(Profile.Feature.DECLARATIVE_USER_PROFILE);
+ defaultRawConfig = UPConfigUtils.readDefaultConfig();
+ try {
+ parsedDefaultRawConfig = UPConfigUtils.parseConfig(defaultRawConfig);
+ } catch (IOException cause) {
+ throw new RuntimeException("Failed to parse default user profile configuration", cause);
+ }
+
// make sure registry is clear in case of re-deploy
contextualMetadataRegistry.clear();
Pattern pattern = getRegexPatternString(config.getArray(CONFIG_READ_ONLY_ATTRIBUTES));
@@ -240,198 +225,6 @@ public abstract class AbstractUserProfileProvider
addContextualProfileMetadata(configureUserProfile(createRegistrationUserCreationProfile(readOnlyValidator)));
addContextualProfileMetadata(configureUserProfile(createUserResourceValidation(config)));
}
-
- private AttributeValidatorMetadata createReadOnlyAttributeUnchangedValidator(Pattern pattern) {
- return new AttributeValidatorMetadata(ReadOnlyAttributeUnchangedValidator.ID,
- ValidatorConfig.builder().config(ReadOnlyAttributeUnchangedValidator.CFG_PATTERN, pattern)
- .build());
- }
-
- @Override
- public void postInit(KeycloakSessionFactory factory) {
- }
-
- @Override
- public void close() {
-
- }
-
- @Override
- public UPConfig getConfiguration() {
- return null;
- }
-
- @Override
- public void setConfiguration(String configuration) {
-
- }
-
- /**
- * Subclasses can override this method to create their instances of {@link UserProfileProvider}.
- *
- * @param session the session
- * @param metadataRegistry the profile metadata
- *
- * @return the profile provider instance
- */
- protected abstract U create(KeycloakSession session, Map metadataRegistry);
-
- /**
- * Sub-types can override this method to customize how contextual profile metadata is configured at init time.
- *
- * @param metadata the profile metadata
- * @return the metadata
- */
- protected UserProfileMetadata configureUserProfile(UserProfileMetadata metadata) {
- return metadata;
- }
-
- /**
- * Sub-types can override this method to customize how contextual profile metadata is configured at runtime.
- *
- * @param metadata the profile metadata
- * @param session the current session
- * @return the metadata
- */
- protected UserProfileMetadata configureUserProfile(UserProfileMetadata metadata, KeycloakSession session) {
- return metadata;
- }
-
- /**
- * Creates a {@link Function} for creating new users when the creating them using {@link UserProfile#create()}.
- *
- * @return a function for creating new users.
- */
- private Function createUserFactory() {
- return new Function() {
- private UserModel user;
-
- @Override
- public UserModel apply(Attributes attributes) {
- if (user == null) {
- String userName = attributes.getFirstValue(UserModel.USERNAME);
-
- // fallback to email in case email is allowed
- if (userName == null) {
- userName = attributes.getFirstValue(UserModel.EMAIL);
- }
-
- user = session.users().addUser(session.getContext().getRealm(), userName);
- }
-
- return user;
- }
- };
- }
-
- private UserProfile createUserProfile(UserProfileContext context, Map attributes, UserModel user) {
- UserProfileMetadata metadata = configureUserProfile(contextualMetadataRegistry.get(context), session);
- Attributes profileAttributes = createAttributes(context, attributes, user, metadata);
- return new DefaultUserProfile(metadata, profileAttributes, createUserFactory(), user, session);
- }
-
- protected Attributes createAttributes(UserProfileContext context, Map attributes, UserModel user,
- UserProfileMetadata metadata) {
- return new DefaultAttributes(context, attributes, user, metadata, session);
- }
-
- private void addContextualProfileMetadata(UserProfileMetadata metadata) {
- if (contextualMetadataRegistry.putIfAbsent(metadata.getContext(), metadata) != null) {
- throw new IllegalStateException("Multiple profile metadata found for context " + metadata.getContext());
- }
- }
-
- private UserProfileMetadata createRegistrationUserCreationProfile(AttributeValidatorMetadata readOnlyValidator) {
- UserProfileMetadata metadata = createDefaultProfile(REGISTRATION, readOnlyValidator);
-
- metadata.getAttribute(UserModel.USERNAME).get(0).addValidators(Arrays.asList(
- new AttributeValidatorMetadata(RegistrationEmailAsUsernameUsernameValueValidator.ID), new AttributeValidatorMetadata(RegistrationUsernameExistsValidator.ID), new AttributeValidatorMetadata(UsernameHasValueValidator.ID)));
-
- metadata.getAttribute(UserModel.EMAIL).get(0).addValidators(Collections.singletonList(
- new AttributeValidatorMetadata(RegistrationEmailAsUsernameEmailValueValidator.ID)));
-
- return metadata;
- }
-
- private UserProfileMetadata createDefaultProfile(UserProfileContext context, AttributeValidatorMetadata readOnlyValidator) {
- UserProfileMetadata metadata = new UserProfileMetadata(context);
-
- metadata.addAttribute(UserModel.USERNAME, -2,
- AbstractUserProfileProvider::editUsernameCondition,
- AbstractUserProfileProvider::readUsernameCondition,
- new AttributeValidatorMetadata(UsernameHasValueValidator.ID),
- new AttributeValidatorMetadata(DuplicateUsernameValidator.ID),
- new AttributeValidatorMetadata(UsernameMutationValidator.ID)).setAttributeDisplayName("${username}");
-
- metadata.addAttribute(UserModel.EMAIL, -1,
- AbstractUserProfileProvider::editEmailCondition,
- AbstractUserProfileProvider::readEmailCondition,
- new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_EMAIL, false)),
- new AttributeValidatorMetadata(DuplicateEmailValidator.ID),
- new AttributeValidatorMetadata(EmailExistsAsUsernameValidator.ID),
- new AttributeValidatorMetadata(EmailValidator.ID, ValidatorConfig.builder().config(EmailValidator.IGNORE_EMPTY_VALUE, true).build()))
- .setAttributeDisplayName("${email}");
-
- List readonlyValidators = new ArrayList<>();
-
- readonlyValidators.add(createReadOnlyAttributeUnchangedValidator(readOnlyAttributesPattern));
-
- if (readOnlyValidator != null) {
- readonlyValidators.add(readOnlyValidator);
- }
-
- metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, 1000, readonlyValidators);
-
- return metadata;
- }
-
- private UserProfileMetadata createBrokeringProfile(AttributeValidatorMetadata readOnlyValidator) {
- UserProfileMetadata metadata = new UserProfileMetadata(IDP_REVIEW);
-
- metadata.addAttribute(UserModel.USERNAME, -2, AbstractUserProfileProvider::editUsernameCondition,
- AbstractUserProfileProvider::readUsernameCondition, new AttributeValidatorMetadata(BrokeringFederatedUsernameHasValueValidator.ID)).setAttributeDisplayName("${username}");
-
- metadata.addAttribute(UserModel.EMAIL, -1,
- new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_EMAIL, true)))
- .setAttributeDisplayName("${email}");
-
- List readonlyValidators = new ArrayList<>();
-
- readonlyValidators.add(createReadOnlyAttributeUnchangedValidator(readOnlyAttributesPattern));
-
- if (readOnlyValidator != null) {
- readonlyValidators.add(readOnlyValidator);
- }
-
- metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, 1000, readonlyValidators);
-
- return metadata;
- }
-
- private UserProfileMetadata createUserResourceValidation(Config.Scope config) {
- Pattern p = getRegexPatternString(config.getArray(CONFIG_ADMIN_READ_ONLY_ATTRIBUTES));
- UserProfileMetadata metadata = new UserProfileMetadata(USER_API);
-
-
- metadata.addAttribute(UserModel.USERNAME, -2, new AttributeValidatorMetadata(UsernameHasValueValidator.ID))
- .addWriteCondition(AbstractUserProfileProvider::editUsernameCondition);
- metadata.addAttribute(UserModel.EMAIL, -1, new AttributeValidatorMetadata(EmailValidator.ID, ValidatorConfig.builder().config(EmailValidator.IGNORE_EMPTY_VALUE, true).build()))
- .addWriteCondition(AbstractUserProfileProvider::editEmailCondition);
-
- List readonlyValidators = new ArrayList<>();
-
- if (p != null) {
- readonlyValidators.add(createReadOnlyAttributeUnchangedValidator(p));
- }
-
- readonlyValidators.add(createReadOnlyAttributeUnchangedValidator(adminReadOnlyAttributesPattern));
- metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, 1000, readonlyValidators);
-
- metadata.addAttribute(UserModel.LOCALE, -1, AbstractUserProfileProvider::isInternationalizationEnabled, AbstractUserProfileProvider::isInternationalizationEnabled)
- .setRequired(AttributeMetadata.ALWAYS_FALSE);
-
- return metadata;
- }
@Override
public List getConfigMetadata() {
@@ -457,12 +250,212 @@ public abstract class AbstractUserProfileProvider
.build();
}
+ @Override
+ public List getConfigProperties() {
+ return ProviderConfigurationBuilder.create()
+ .property().name(DeclarativeUserProfileProvider.UP_COMPONENT_CONFIG_KEY)
+ .type(ProviderConfigProperty.STRING_TYPE)
+ .add()
+ .build();
+ }
+
+ @Override
+ public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException {
+ String upConfigJson = model == null ? null : model.get(DeclarativeUserProfileProvider.UP_COMPONENT_CONFIG_KEY);
+
+ if (!isBlank(upConfigJson)) {
+ try {
+ UPConfig upc = UPConfigUtils.parseConfig(upConfigJson);
+ List errors = UPConfigUtils.validate(session, upc);
+
+ if (!errors.isEmpty()) {
+ throw new ComponentValidationException(errors.toString());
+ }
+ } catch (IOException e) {
+ throw new ComponentValidationException(e.getMessage(), e);
+ }
+ }
+
+ // delete cache so new config is parsed and applied next time it is required
+ // throught #configureUserProfile(metadata, session)
+ if (model != null) {
+ model.removeNote(DeclarativeUserProfileProvider.PARSED_CONFIG_COMPONENT_KEY);
+ }
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+ @Override
+ public int order() {
+ return PROVIDER_PRIORITY;
+ }
+
+ @Override
+ public String getHelpText() {
+ return null;
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public DeclarativeUserProfileProvider create(KeycloakSession session) {
+ return new DeclarativeUserProfileProvider(session, this);
+ }
+
+ /**
+ * Specifies how contextual profile metadata is configured at init time.
+ *
+ * @param metadata the profile metadata
+ * @return the metadata
+ */
+ protected UserProfileMetadata configureUserProfile(UserProfileMetadata metadata) {
+ if (isDeclarativeConfigurationEnabled) {
+ // default metadata for each context is based on the default realm configuration
+ return new DeclarativeUserProfileProvider(null, this).decorateUserProfileForCache(metadata, parsedDefaultRawConfig);
+ }
+
+ return metadata;
+ }
+
+ private AttributeValidatorMetadata createReadOnlyAttributeUnchangedValidator(Pattern pattern) {
+ return new AttributeValidatorMetadata(ReadOnlyAttributeUnchangedValidator.ID,
+ ValidatorConfig.builder().config(ReadOnlyAttributeUnchangedValidator.CFG_PATTERN, pattern)
+ .build());
+ }
+
+ private void addContextualProfileMetadata(UserProfileMetadata metadata) {
+ if (contextualMetadataRegistry.putIfAbsent(metadata.getContext(), metadata) != null) {
+ throw new IllegalStateException("Multiple profile metadata found for context " + metadata.getContext());
+ }
+ }
+
+ private UserProfileMetadata createBrokeringProfile(AttributeValidatorMetadata readOnlyValidator) {
+ UserProfileMetadata metadata = new UserProfileMetadata(IDP_REVIEW);
+
+ metadata.addAttribute(UserModel.USERNAME, -2, DeclarativeUserProfileProviderFactory::editUsernameCondition,
+ DeclarativeUserProfileProviderFactory::readUsernameCondition, new AttributeValidatorMetadata(BrokeringFederatedUsernameHasValueValidator.ID)).setAttributeDisplayName("${username}");
+
+ metadata.addAttribute(UserModel.EMAIL, -1,
+ new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_EMAIL, true)))
+ .setAttributeDisplayName("${email}");
+
+ List readonlyValidators = new ArrayList<>();
+
+ readonlyValidators.add(createReadOnlyAttributeUnchangedValidator(readOnlyAttributesPattern));
+
+ if (readOnlyValidator != null) {
+ readonlyValidators.add(readOnlyValidator);
+ }
+
+ metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, 1000, readonlyValidators);
+
+ return metadata;
+ }
+
+ private UserProfileMetadata createRegistrationUserCreationProfile(AttributeValidatorMetadata readOnlyValidator) {
+ UserProfileMetadata metadata = createDefaultProfile(REGISTRATION, readOnlyValidator);
+
+ metadata.getAttribute(UserModel.USERNAME).get(0).addValidators(Arrays.asList(
+ new AttributeValidatorMetadata(RegistrationEmailAsUsernameUsernameValueValidator.ID), new AttributeValidatorMetadata(RegistrationUsernameExistsValidator.ID), new AttributeValidatorMetadata(UsernameHasValueValidator.ID)));
+
+ metadata.getAttribute(UserModel.EMAIL).get(0).addValidators(Collections.singletonList(
+ new AttributeValidatorMetadata(RegistrationEmailAsUsernameEmailValueValidator.ID)));
+
+ return metadata;
+ }
+
+ private UserProfileMetadata createDefaultProfile(UserProfileContext context, AttributeValidatorMetadata readOnlyValidator) {
+ UserProfileMetadata metadata = new UserProfileMetadata(context);
+
+ metadata.addAttribute(UserModel.USERNAME, -2,
+ DeclarativeUserProfileProviderFactory::editUsernameCondition,
+ DeclarativeUserProfileProviderFactory::readUsernameCondition,
+ new AttributeValidatorMetadata(UsernameHasValueValidator.ID),
+ new AttributeValidatorMetadata(DuplicateUsernameValidator.ID),
+ new AttributeValidatorMetadata(UsernameMutationValidator.ID)).setAttributeDisplayName("${username}");
+
+ metadata.addAttribute(UserModel.EMAIL, -1,
+ DeclarativeUserProfileProviderFactory::editEmailCondition,
+ DeclarativeUserProfileProviderFactory::readEmailCondition,
+ new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_EMAIL, false)),
+ new AttributeValidatorMetadata(DuplicateEmailValidator.ID),
+ new AttributeValidatorMetadata(EmailExistsAsUsernameValidator.ID),
+ new AttributeValidatorMetadata(EmailValidator.ID, ValidatorConfig.builder().config(EmailValidator.IGNORE_EMPTY_VALUE, true).build()))
+ .setAttributeDisplayName("${email}");
+
+ List readonlyValidators = new ArrayList<>();
+
+ readonlyValidators.add(createReadOnlyAttributeUnchangedValidator(readOnlyAttributesPattern));
+
+ if (readOnlyValidator != null) {
+ readonlyValidators.add(readOnlyValidator);
+ }
+
+ metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, 1000, readonlyValidators);
+
+ return metadata;
+ }
+
+ private UserProfileMetadata createUserResourceValidation(Config.Scope config) {
+ Pattern p = getRegexPatternString(config.getArray(CONFIG_ADMIN_READ_ONLY_ATTRIBUTES));
+ UserProfileMetadata metadata = new UserProfileMetadata(USER_API);
+
+
+ metadata.addAttribute(UserModel.USERNAME, -2, new AttributeValidatorMetadata(UsernameHasValueValidator.ID))
+ .addWriteCondition(DeclarativeUserProfileProviderFactory::editUsernameCondition);
+ metadata.addAttribute(UserModel.EMAIL, -1, new AttributeValidatorMetadata(EmailValidator.ID, ValidatorConfig.builder().config(EmailValidator.IGNORE_EMPTY_VALUE, true).build()))
+ .addWriteCondition(DeclarativeUserProfileProviderFactory::editEmailCondition);
+
+ List readonlyValidators = new ArrayList<>();
+
+ if (p != null) {
+ readonlyValidators.add(createReadOnlyAttributeUnchangedValidator(p));
+ }
+
+ readonlyValidators.add(createReadOnlyAttributeUnchangedValidator(adminReadOnlyAttributesPattern));
+ metadata.addAttribute(READ_ONLY_ATTRIBUTE_KEY, 1000, readonlyValidators);
+
+ metadata.addAttribute(UserModel.LOCALE, -1, DeclarativeUserProfileProviderFactory::isInternationalizationEnabled, DeclarativeUserProfileProviderFactory::isInternationalizationEnabled)
+ .setRequired(AttributeMetadata.ALWAYS_FALSE);
+
+ return metadata;
+ }
+
private UserProfileMetadata createAccountProfile(UserProfileContext context, AttributeValidatorMetadata readOnlyValidator) {
UserProfileMetadata defaultProfile = createDefaultProfile(context, readOnlyValidator);
- defaultProfile.addAttribute(UserModel.LOCALE, -1, AbstractUserProfileProvider::isInternationalizationEnabled, AbstractUserProfileProvider::isInternationalizationEnabled)
+ defaultProfile.addAttribute(UserModel.LOCALE, -1, DeclarativeUserProfileProviderFactory::isInternationalizationEnabled, DeclarativeUserProfileProviderFactory::isInternationalizationEnabled)
.setRequired(AttributeMetadata.ALWAYS_FALSE);
return defaultProfile;
}
+
+ // GETTER METHODS FOR INTERNAL FIELDS
+
+ protected boolean isDeclarativeConfigurationEnabled() {
+ return isDeclarativeConfigurationEnabled;
+ }
+
+ protected String getDefaultRawConfig() {
+ return defaultRawConfig;
+ }
+
+ protected UPConfig getParsedDefaultRawConfig() {
+ return parsedDefaultRawConfig;
+ }
+
+ protected Map getContextualMetadataRegistry() {
+ return contextualMetadataRegistry;
+ }
+
}
diff --git a/services/src/main/java/org/keycloak/userprofile/config/UPConfigUtils.java b/services/src/main/java/org/keycloak/userprofile/config/UPConfigUtils.java
index 1acf3a3acf..aeb76368e8 100644
--- a/services/src/main/java/org/keycloak/userprofile/config/UPConfigUtils.java
+++ b/services/src/main/java/org/keycloak/userprofile/config/UPConfigUtils.java
@@ -18,6 +18,7 @@ package org.keycloak.userprofile.config;
import static org.keycloak.common.util.ObjectUtil.isBlank;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
@@ -76,6 +77,17 @@ public class UPConfigUtils {
return JsonSerialization.readValue(is, UPConfig.class);
}
+ /**
+ * Parse configuration of user-profile from String
+ *
+ * @param rawConfig Configuration in String format
+ * @return object representation of the configuration
+ * @throws IOException if JSON configuration can't be loaded (eg due to JSON format errors etc)
+ */
+ public static UPConfig parseConfig(String rawConfig) throws IOException {
+ return readConfig(new ByteArrayInputStream(rawConfig.getBytes("UTF-8")));
+ }
+
/**
* Validate object representation of the configuration. Validations:
*
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.userprofile.UserProfileProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.userprofile.UserProfileProviderFactory
index 45956ba080..c22f651de7 100644
--- a/services/src/main/resources/META-INF/services/org.keycloak.userprofile.UserProfileProviderFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.userprofile.UserProfileProviderFactory
@@ -16,4 +16,4 @@
# * limitations under the License.
# */
#
-org.keycloak.userprofile.DeclarativeUserProfileProvider
\ No newline at end of file
+org.keycloak.userprofile.DeclarativeUserProfileProviderFactory
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/user/profile/CustomUserProfileProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/user/profile/CustomUserProfileProvider.java
index ed09247c59..635edf74bf 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/user/profile/CustomUserProfileProvider.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/user/profile/CustomUserProfileProvider.java
@@ -5,34 +5,13 @@ import org.keycloak.models.UserModel;
import org.keycloak.userprofile.DeclarativeUserProfileProvider;
import org.keycloak.userprofile.UserProfile;
import org.keycloak.userprofile.UserProfileContext;
-import org.keycloak.userprofile.UserProfileMetadata;
-import org.keycloak.userprofile.UserProfileProvider;
-import org.keycloak.representations.userprofile.config.UPConfig;
import java.util.Map;
public class CustomUserProfileProvider extends DeclarativeUserProfileProvider {
- public static final String ID = "custom-user-profile";
-
- public CustomUserProfileProvider() {
- super();
- }
-
- public CustomUserProfileProvider(KeycloakSession session,
- Map metadataRegistry, String defaultRawConfig, UPConfig parsedDefaultRawConfig) {
- super(session, metadataRegistry, defaultRawConfig, parsedDefaultRawConfig);
- }
-
- @Override
- protected UserProfileProvider create(KeycloakSession session,
- Map metadataRegistry) {
- return new CustomUserProfileProvider(session, metadataRegistry, defaultRawConfig, parsedDefaultRawConfig);
- }
-
- @Override
- public String getId() {
- return ID;
+ public CustomUserProfileProvider(KeycloakSession session, CustomUserProfileProviderFactory factory) {
+ super(session, factory);
}
@Override
@@ -50,8 +29,4 @@ public class CustomUserProfileProvider extends DeclarativeUserProfileProvider {
return this.create(context, attributes, (UserModel) null);
}
- @Override
- public int order() {
- return super.order() - 1;
- }
}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/user/profile/CustomUserProfileProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/user/profile/CustomUserProfileProviderFactory.java
new file mode 100644
index 0000000000..7599188f60
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/user/profile/CustomUserProfileProviderFactory.java
@@ -0,0 +1,46 @@
+/*
+ * 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.testsuite.user.profile;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.userprofile.DeclarativeUserProfileProviderFactory;
+
+/**
+ * @author Marek Posolda
+ */
+public class CustomUserProfileProviderFactory extends DeclarativeUserProfileProviderFactory {
+
+ public static final String ID = "custom-user-profile";
+
+ @Override
+ public CustomUserProfileProvider create(KeycloakSession session) {
+ return new CustomUserProfileProvider(session, this);
+ }
+
+ @Override
+ public int order() {
+ return super.order() - 1;
+ }
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.userprofile.UserProfileProviderFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.userprofile.UserProfileProviderFactory
index 6d25842823..afd1e3c8ca 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.userprofile.UserProfileProviderFactory
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.userprofile.UserProfileProviderFactory
@@ -16,4 +16,4 @@
# * limitations under the License.
# */
#
-org.keycloak.testsuite.user.profile.CustomUserProfileProvider
\ No newline at end of file
+org.keycloak.testsuite.user.profile.CustomUserProfileProviderFactory
\ No newline at end of file