diff --git a/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileProviderFactory.java index 10214d3bba..e57e9f6086 100644 --- a/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileProviderFactory.java +++ b/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileProviderFactory.java @@ -22,6 +22,6 @@ import org.keycloak.provider.ProviderFactory; /** * @author Markus Till */ -public interface UserProfileProviderFactory extends ProviderFactory { +public interface UserProfileProviderFactory extends ProviderFactory { } diff --git a/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java index 5b436476e1..8da699d60c 100644 --- a/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java +++ b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java @@ -21,9 +21,6 @@ package org.keycloak.userprofile; import static org.keycloak.common.util.ObjectUtil.isBlank; import static org.keycloak.protocol.oidc.TokenManager.getRequestedClientScopes; -import static org.keycloak.userprofile.config.UPConfigUtils.readConfig; - -import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -37,19 +34,13 @@ import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; -import org.keycloak.Config; -import org.keycloak.common.Profile; -import org.keycloak.component.AmphibianProviderFactory; import org.keycloak.component.ComponentModel; -import org.keycloak.component.ComponentValidationException; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserProvider; import org.keycloak.protocol.oidc.OIDCLoginProtocol; -import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.services.messages.Messages; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.userprofile.config.DeclarativeUserProfileModel; @@ -73,16 +64,11 @@ import org.keycloak.validate.ValidatorConfig; * @author Pedro Igor * @author Vlastimil Elias */ -public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider - implements AmphibianProviderFactory { +public class DeclarativeUserProfileProvider implements UserProfileProvider { - public static final String ID = "declarative-user-profile"; - public static final int PROVIDER_PRIORITY = 1; public static final String UP_COMPONENT_CONFIG_KEY = "kc.user.profile.config"; public static final String REALM_USER_PROFILE_ENABLED = "userProfileEnabled"; - private static final String PARSED_CONFIG_COMPONENT_KEY = "kc.user.profile.metadata"; - - private static boolean isDeclarativeConfigurationEnabled; + protected static final String PARSED_CONFIG_COMPONENT_KEY = "kc.user.profile.metadata"; // TODO:mposolda should it be here or rather on factory? /** * Method used for predicate which returns true if any of the configuredScopes is requested in current auth flow. @@ -105,30 +91,22 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider< return getRequestedClientScopes(requestedScopesString, client).map((csm) -> csm.getName()).anyMatch(configuredScopes::contains); } - protected String defaultRawConfig; - protected UPConfig parsedDefaultRawConfig; + private final KeycloakSession session; + private final boolean isDeclarativeConfigurationEnabled; + private final String providerId; + private final Map contextualMetadataRegistry; + private final String defaultRawConfig; + protected final UPConfig parsedDefaultRawConfig; - public DeclarativeUserProfileProvider() { - // factory create + public DeclarativeUserProfileProvider(KeycloakSession session, DeclarativeUserProfileProviderFactory factory) { + this.session = session; + this.providerId = factory.getId(); + this.isDeclarativeConfigurationEnabled = factory.isDeclarativeConfigurationEnabled(); + this.contextualMetadataRegistry = factory.getContextualMetadataRegistry(); + this.defaultRawConfig = factory.getDefaultRawConfig(); + this.parsedDefaultRawConfig = factory.getParsedDefaultRawConfig(); } - public DeclarativeUserProfileProvider(KeycloakSession session, Map metadataRegistry, String defaultRawConfig, UPConfig parsedDefaultRawConfig) { - super(session, metadataRegistry); - this.defaultRawConfig = defaultRawConfig; - this.parsedDefaultRawConfig = parsedDefaultRawConfig; - } - - @Override - public String getId() { - return ID; - } - - @Override - protected UserProfileProvider create(KeycloakSession session, Map metadataRegistry) { - return new DeclarativeUserProfileProvider(session, metadataRegistry, defaultRawConfig, parsedDefaultRawConfig); - } - - @Override protected Attributes createAttributes(UserProfileContext context, Map attributes, UserModel user, UserProfileMetadata metadata) { RealmModel realm = session.getContext().getRealm(); @@ -143,16 +121,59 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider< } @Override - protected UserProfileMetadata configureUserProfile(UserProfileMetadata metadata) { - if (isDeclarativeConfigurationEnabled) { - // default metadata for each context is based on the default realm configuration - return decorateUserProfileForCache(metadata, parsedDefaultRawConfig); - } - - return metadata; + 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); + } + + 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); + } + + /** + * 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; + } + }; + } + + /** + * Specifies how contextual profile metadata is configured at runtime. + * + * @param metadata the profile metadata + * @return the metadata + */ protected UserProfileMetadata configureUserProfile(UserProfileMetadata metadata, KeycloakSession session) { UserProfileContext context = metadata.getContext(); UserProfileMetadata decoratedMetadata = metadata.clone(); @@ -187,35 +208,6 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider< return metadataMap.computeIfAbsent(context, createUserDefinedProfileDecorator(session, decoratedMetadata, component)); } - @Override - public String getHelpText() { - return null; - } - - @Override - public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel model) throws ComponentValidationException { - String upConfigJson = getConfigJsonFromComponentModel(model); - - if (!isBlank(upConfigJson)) { - try { - UPConfig upc = 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(PARSED_CONFIG_COMPONENT_KEY); - } - } - @Override public UPConfig getConfiguration() { RealmModel realm = session.getContext().getRealm(); @@ -261,39 +253,13 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider< realm.updateComponent(component); } - @Override - public List getConfigProperties() { - return ProviderConfigurationBuilder.create() - .property().name(UP_COMPONENT_CONFIG_KEY) - .type(ProviderConfigProperty.STRING_TYPE) - .add() - .build(); - } - - @Override - public void init(Config.Scope config) { - isDeclarativeConfigurationEnabled = Profile.isFeatureEnabled(Profile.Feature.DECLARATIVE_USER_PROFILE); - defaultRawConfig = UPConfigUtils.readDefaultConfig(); - try { - parsedDefaultRawConfig = parseConfig(defaultRawConfig); - } catch (IOException cause) { - throw new RuntimeException("Failed to parse default user profile configuration", cause); - } - super.init(config); - } - - @Override - public int order() { - return PROVIDER_PRIORITY; - } - private Optional getComponentModel() { RealmModel realm = session.getContext().getRealm(); return realm.getComponentsStream(realm.getId(), UserProfileProvider.class.getName()).findAny(); } /** - * Decorate basic metadata provided from {@link AbstractUserProfileProvider} based on 'per realm' configuration. + * Decorate basic metadata based on 'per realm' configuration. * This method is called for each {@link UserProfileContext} in each realm, and metadata are cached then and this * method is called again only if configuration changes. */ @@ -479,7 +445,7 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider< protected UPConfig getParsedConfig(String rawConfig) { if (!isBlank(rawConfig)) { try { - return parseConfig(rawConfig); + return UPConfigUtils.parseConfig(rawConfig); } catch (IOException e) { throw new RuntimeException("UserProfile configuration for realm '" + session.getContext().getRealm().getName() + "' is invalid:" + e.getMessage(), e); } @@ -488,17 +454,13 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider< return null; } - private UPConfig parseConfig(String rawConfig) throws IOException { - return readConfig(new ByteArrayInputStream(rawConfig.getBytes("UTF-8"))); - } - /** * Create the component model to store configuration * @return component model */ protected ComponentModel createComponentModel() { RealmModel realm = session.getContext().getRealm(); - return realm.addComponentModel(new DeclarativeUserProfileModel(getId())); + return realm.addComponentModel(new DeclarativeUserProfileModel(providerId)); } /** @@ -531,6 +493,10 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider< return isDeclarativeConfigurationEnabled && realm.getAttribute(REALM_USER_PROFILE_ENABLED, false); } + @Override + public void close() { + } + private Function 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