Decouple factory methods from the provider methods on UserProfileProvider implementation

closes #25146

Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
mposolda 2023-12-01 10:06:54 +01:00 committed by Pedro Igor
parent ba790f1bca
commit 3fa2d155ca
8 changed files with 404 additions and 412 deletions

View file

@ -22,6 +22,6 @@ import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public interface UserProfileProviderFactory<U extends UserProfileProvider> extends ProviderFactory<U> {
public interface UserProfileProviderFactory extends ProviderFactory<UserProfileProvider> {
}

View file

@ -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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
* @author Vlastimil Elias <velias@redhat.com>
*/
public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<UserProfileProvider>
implements AmphibianProviderFactory<UserProfileProvider> {
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<UserProfileContext, UserProfileMetadata> 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<UserProfileContext, UserProfileMetadata> 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<UserProfileContext, UserProfileMetadata> metadataRegistry) {
return new DeclarativeUserProfileProvider(session, metadataRegistry, defaultRawConfig, parsedDefaultRawConfig);
}
@Override
protected Attributes createAttributes(UserProfileContext context, Map<String, ?> 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<String, ?> attributes, UserModel user) {
return createUserProfile(context, attributes, user);
}
@Override
public UserProfile create(UserProfileContext context, Map<String, ?> attributes) {
return createUserProfile(context, attributes, null);
}
private UserProfile createUserProfile(UserProfileContext context, Map<String, ?> 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<Attributes, UserModel> createUserFactory() {
return new Function<Attributes, UserModel>() {
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<String> 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<ProviderConfigProperty> 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<ComponentModel> 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<UserProfileContext, UserProfileMetadata> createUserDefinedProfileDecorator(KeycloakSession session, UserProfileMetadata decoratedMetadata, ComponentModel component) {
return (c) -> {
UPConfig parsedConfig = getParsedConfig(getConfigJsonFromComponentModel(component));

View file

@ -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;
/**
* <p>A base class for {@link UserProfileProvider} implementations providing the main hooks for customizations.
*
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public abstract class AbstractUserProfileProvider<U extends UserProfileProvider> implements UserProfileProvider, UserProfileProviderFactory<U> {
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<UserProfileProvider> {
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<UserProfileContext, UserProfileMetadata> 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<U extends UserProfileProvider>
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<U extends UserProfileProvider>
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<U extends UserProfileProvider>
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<String> readOnlyAttributes = new ArrayList<>(Arrays.asList(builtinReadOnlyAttributes));
@ -169,59 +197,16 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
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<UserProfileContext, UserProfileMetadata> contextualMetadataRegistry;
protected final KeycloakSession session;
public AbstractUserProfileProvider() {
// for reflection
this(null, new HashMap<>());
}
public AbstractUserProfileProvider(KeycloakSession session, Map<UserProfileContext, UserProfileMetadata> 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<String, ?> attributes, UserModel user) {
return createUserProfile(context, attributes, user);
}
@Override
public UserProfile create(UserProfileContext context, Map<String, ?> 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));
@ -241,198 +226,6 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
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<UserProfileContext, UserProfileMetadata> 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<Attributes, UserModel> createUserFactory() {
return new Function<Attributes, UserModel>() {
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<String, ?> 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<String, ?> 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<AttributeValidatorMetadata> 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<AttributeValidatorMetadata> 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<AttributeValidatorMetadata> 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<ProviderConfigProperty> getConfigMetadata() {
return ProviderConfigurationBuilder.create()
@ -457,12 +250,212 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
.build();
}
@Override
public List<ProviderConfigProperty> 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<String> 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<AttributeValidatorMetadata> 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<AttributeValidatorMetadata> 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<AttributeValidatorMetadata> 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<UserProfileContext, UserProfileMetadata> getContextualMetadataRegistry() {
return contextualMetadataRegistry;
}
}

View file

@ -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:
* <ul>

View file

@ -16,4 +16,4 @@
# * limitations under the License.
# */
#
org.keycloak.userprofile.DeclarativeUserProfileProvider
org.keycloak.userprofile.DeclarativeUserProfileProviderFactory

View file

@ -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<UserProfileContext, UserProfileMetadata> metadataRegistry, String defaultRawConfig, UPConfig parsedDefaultRawConfig) {
super(session, metadataRegistry, defaultRawConfig, parsedDefaultRawConfig);
}
@Override
protected UserProfileProvider create(KeycloakSession session,
Map<UserProfileContext, UserProfileMetadata> 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;
}
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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;
}
}

View file

@ -16,4 +16,4 @@
# * limitations under the License.
# */
#
org.keycloak.testsuite.user.profile.CustomUserProfileProvider
org.keycloak.testsuite.user.profile.CustomUserProfileProviderFactory