Decouple factory methods from the provider methods on UserProfileProvider implementation
closes #25146 Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
parent
ba790f1bca
commit
3fa2d155ca
8 changed files with 404 additions and 412 deletions
|
@ -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> {
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
@ -240,198 +225,6 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
|||
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<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() {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -16,4 +16,4 @@
|
|||
# * limitations under the License.
|
||||
# */
|
||||
#
|
||||
org.keycloak.userprofile.DeclarativeUserProfileProvider
|
||||
org.keycloak.userprofile.DeclarativeUserProfileProviderFactory
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -16,4 +16,4 @@
|
|||
# * limitations under the License.
|
||||
# */
|
||||
#
|
||||
org.keycloak.testsuite.user.profile.CustomUserProfileProvider
|
||||
org.keycloak.testsuite.user.profile.CustomUserProfileProviderFactory
|
Loading…
Reference in a new issue