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>
|
* @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.common.util.ObjectUtil.isBlank;
|
||||||
import static org.keycloak.protocol.oidc.TokenManager.getRequestedClientScopes;
|
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.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -37,19 +34,13 @@ import java.util.function.Function;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
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.ComponentModel;
|
||||||
import org.keycloak.component.ComponentValidationException;
|
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserProvider;
|
import org.keycloak.models.UserProvider;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
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.services.messages.Messages;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.userprofile.config.DeclarativeUserProfileModel;
|
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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
* @author Vlastimil Elias <velias@redhat.com>
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
*/
|
*/
|
||||||
public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<UserProfileProvider>
|
public class DeclarativeUserProfileProvider implements UserProfileProvider {
|
||||||
implements AmphibianProviderFactory<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 UP_COMPONENT_CONFIG_KEY = "kc.user.profile.config";
|
||||||
public static final String REALM_USER_PROFILE_ENABLED = "userProfileEnabled";
|
public static final String REALM_USER_PROFILE_ENABLED = "userProfileEnabled";
|
||||||
private static final String PARSED_CONFIG_COMPONENT_KEY = "kc.user.profile.metadata";
|
protected static final String PARSED_CONFIG_COMPONENT_KEY = "kc.user.profile.metadata"; // TODO:mposolda should it be here or rather on factory?
|
||||||
|
|
||||||
private static boolean isDeclarativeConfigurationEnabled;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method used for predicate which returns true if any of the configuredScopes is requested in current auth flow.
|
* 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);
|
return getRequestedClientScopes(requestedScopesString, client).map((csm) -> csm.getName()).anyMatch(configuredScopes::contains);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String defaultRawConfig;
|
private final KeycloakSession session;
|
||||||
protected UPConfig parsedDefaultRawConfig;
|
private final boolean isDeclarativeConfigurationEnabled;
|
||||||
|
private final String providerId;
|
||||||
|
private final Map<UserProfileContext, UserProfileMetadata> contextualMetadataRegistry;
|
||||||
|
private final String defaultRawConfig;
|
||||||
|
protected final UPConfig parsedDefaultRawConfig;
|
||||||
|
|
||||||
public DeclarativeUserProfileProvider() {
|
public DeclarativeUserProfileProvider(KeycloakSession session, DeclarativeUserProfileProviderFactory factory) {
|
||||||
// factory create
|
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,
|
protected Attributes createAttributes(UserProfileContext context, Map<String, ?> attributes,
|
||||||
UserModel user, UserProfileMetadata metadata) {
|
UserModel user, UserProfileMetadata metadata) {
|
||||||
RealmModel realm = session.getContext().getRealm();
|
RealmModel realm = session.getContext().getRealm();
|
||||||
|
@ -143,16 +121,59 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected UserProfileMetadata configureUserProfile(UserProfileMetadata metadata) {
|
public UserProfile create(UserProfileContext context, UserModel user) {
|
||||||
if (isDeclarativeConfigurationEnabled) {
|
return createUserProfile(context, user.getAttributes(), user);
|
||||||
// default metadata for each context is based on the default realm configuration
|
|
||||||
return decorateUserProfileForCache(metadata, parsedDefaultRawConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadata;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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) {
|
protected UserProfileMetadata configureUserProfile(UserProfileMetadata metadata, KeycloakSession session) {
|
||||||
UserProfileContext context = metadata.getContext();
|
UserProfileContext context = metadata.getContext();
|
||||||
UserProfileMetadata decoratedMetadata = metadata.clone();
|
UserProfileMetadata decoratedMetadata = metadata.clone();
|
||||||
|
@ -187,35 +208,6 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
return metadataMap.computeIfAbsent(context, createUserDefinedProfileDecorator(session, decoratedMetadata, component));
|
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
|
@Override
|
||||||
public UPConfig getConfiguration() {
|
public UPConfig getConfiguration() {
|
||||||
RealmModel realm = session.getContext().getRealm();
|
RealmModel realm = session.getContext().getRealm();
|
||||||
|
@ -261,39 +253,13 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
realm.updateComponent(component);
|
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() {
|
private Optional<ComponentModel> getComponentModel() {
|
||||||
RealmModel realm = session.getContext().getRealm();
|
RealmModel realm = session.getContext().getRealm();
|
||||||
return realm.getComponentsStream(realm.getId(), UserProfileProvider.class.getName()).findAny();
|
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
|
* 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.
|
* method is called again only if configuration changes.
|
||||||
*/
|
*/
|
||||||
|
@ -479,7 +445,7 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
protected UPConfig getParsedConfig(String rawConfig) {
|
protected UPConfig getParsedConfig(String rawConfig) {
|
||||||
if (!isBlank(rawConfig)) {
|
if (!isBlank(rawConfig)) {
|
||||||
try {
|
try {
|
||||||
return parseConfig(rawConfig);
|
return UPConfigUtils.parseConfig(rawConfig);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException("UserProfile configuration for realm '" + session.getContext().getRealm().getName() + "' is invalid:" + e.getMessage(), 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private UPConfig parseConfig(String rawConfig) throws IOException {
|
|
||||||
return readConfig(new ByteArrayInputStream(rawConfig.getBytes("UTF-8")));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the component model to store configuration
|
* Create the component model to store configuration
|
||||||
* @return component model
|
* @return component model
|
||||||
*/
|
*/
|
||||||
protected ComponentModel createComponentModel() {
|
protected ComponentModel createComponentModel() {
|
||||||
RealmModel realm = session.getContext().getRealm();
|
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);
|
return isDeclarativeConfigurationEnabled && realm.getAttribute(REALM_USER_PROFILE_ENABLED, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
}
|
||||||
|
|
||||||
private Function<UserProfileContext, UserProfileMetadata> createUserDefinedProfileDecorator(KeycloakSession session, UserProfileMetadata decoratedMetadata, ComponentModel component) {
|
private Function<UserProfileContext, UserProfileMetadata> createUserDefinedProfileDecorator(KeycloakSession session, UserProfileMetadata decoratedMetadata, ComponentModel component) {
|
||||||
return (c) -> {
|
return (c) -> {
|
||||||
UPConfig parsedConfig = getParsedConfig(getConfigJsonFromComponentModel(component));
|
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
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* * and other contributors as indicated by the @author tags.
|
* you may not use this file except in compliance with the License.
|
||||||
* *
|
* You may obtain a copy of the License at
|
||||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
*
|
||||||
* * you may not use this file except in compliance with the License.
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
* * You may obtain a copy of the License at
|
*
|
||||||
* *
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* * http://www.apache.org/licenses/LICENSE-2.0
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* *
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
* * Unless required by applicable law or agreed to in writing, software
|
*
|
||||||
* * distributed under the License is distributed on an "AS IS" BASIS,
|
* See the License for the specific language governing permissions and
|
||||||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* limitations under the License.
|
||||||
* * See the License for the specific language governing permissions and
|
|
||||||
* * limitations under the License.
|
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.keycloak.userprofile;
|
package org.keycloak.userprofile;
|
||||||
|
|
||||||
import static org.keycloak.userprofile.DefaultAttributes.READ_ONLY_ATTRIBUTE_KEY;
|
import java.io.IOException;
|
||||||
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.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.common.Profile;
|
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.KeycloakContext;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
@ -48,6 +43,7 @@ import org.keycloak.provider.ProviderConfigProperty;
|
||||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.userprofile.config.UPConfigUtils;
|
||||||
import org.keycloak.userprofile.validator.BlankAttributeValidator;
|
import org.keycloak.userprofile.validator.BlankAttributeValidator;
|
||||||
import org.keycloak.userprofile.validator.BrokeringFederatedUsernameHasValueValidator;
|
import org.keycloak.userprofile.validator.BrokeringFederatedUsernameHasValueValidator;
|
||||||
import org.keycloak.userprofile.validator.DuplicateEmailValidator;
|
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.ValidatorConfig;
|
||||||
import org.keycloak.validate.validators.EmailValidator;
|
import org.keycloak.validate.validators.EmailValidator;
|
||||||
|
|
||||||
/**
|
import static org.keycloak.common.util.ObjectUtil.isBlank;
|
||||||
* <p>A base class for {@link UserProfileProvider} implementations providing the main hooks for customizations.
|
import static org.keycloak.userprofile.DefaultAttributes.READ_ONLY_ATTRIBUTE_KEY;
|
||||||
*
|
import static org.keycloak.userprofile.UserProfileContext.ACCOUNT;
|
||||||
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
|
import static org.keycloak.userprofile.UserProfileContext.IDP_REVIEW;
|
||||||
*/
|
import static org.keycloak.userprofile.UserProfileContext.REGISTRATION;
|
||||||
public abstract class AbstractUserProfileProvider<U extends UserProfileProvider> implements UserProfileProvider, UserProfileProviderFactory<U> {
|
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_ADMIN_READ_ONLY_ATTRIBUTES = "admin-read-only-attributes";
|
||||||
public static final String CONFIG_READ_ONLY_ATTRIBUTES = "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 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) {
|
private static boolean editUsernameCondition(AttributeContext c) {
|
||||||
KeycloakSession session = c.getSession();
|
KeycloakSession session = c.getSession();
|
||||||
KeycloakContext context = session.getContext();
|
KeycloakContext context = session.getContext();
|
||||||
|
@ -119,7 +138,7 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
||||||
return true;
|
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()));
|
return !(UPDATE_PROFILE.equals(c.getContext()) || ACCOUNT.equals(c.getContext()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,7 +156,7 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Profile.isFeatureEnabled(Feature.UPDATE_EMAIL)) {
|
if (Profile.isFeatureEnabled(Profile.Feature.UPDATE_EMAIL)) {
|
||||||
return !UPDATE_PROFILE.equals(context);
|
return !UPDATE_PROFILE.equals(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,6 +171,15 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
||||||
return true;
|
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) {
|
public static Pattern getRegexPatternString(String[] builtinReadOnlyAttributes) {
|
||||||
if (builtinReadOnlyAttributes != null) {
|
if (builtinReadOnlyAttributes != null) {
|
||||||
List<String> readOnlyAttributes = new ArrayList<>(Arrays.asList(builtinReadOnlyAttributes));
|
List<String> readOnlyAttributes = new ArrayList<>(Arrays.asList(builtinReadOnlyAttributes));
|
||||||
|
@ -169,59 +197,16 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
||||||
return null;
|
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
|
@Override
|
||||||
public void init(Config.Scope config) {
|
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
|
// make sure registry is clear in case of re-deploy
|
||||||
contextualMetadataRegistry.clear();
|
contextualMetadataRegistry.clear();
|
||||||
Pattern pattern = getRegexPatternString(config.getArray(CONFIG_READ_ONLY_ATTRIBUTES));
|
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(createRegistrationUserCreationProfile(readOnlyValidator)));
|
||||||
addContextualProfileMetadata(configureUserProfile(createUserResourceValidation(config)));
|
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
|
@Override
|
||||||
public List<ProviderConfigProperty> getConfigMetadata() {
|
public List<ProviderConfigProperty> getConfigMetadata() {
|
||||||
|
@ -457,12 +250,212 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
||||||
.build();
|
.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) {
|
private UserProfileMetadata createAccountProfile(UserProfileContext context, AttributeValidatorMetadata readOnlyValidator) {
|
||||||
UserProfileMetadata defaultProfile = createDefaultProfile(context, 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);
|
.setRequired(AttributeMetadata.ALWAYS_FALSE);
|
||||||
|
|
||||||
return defaultProfile;
|
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 static org.keycloak.common.util.ObjectUtil.isBlank;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
|
@ -76,6 +77,17 @@ public class UPConfigUtils {
|
||||||
return JsonSerialization.readValue(is, UPConfig.class);
|
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:
|
* Validate object representation of the configuration. Validations:
|
||||||
* <ul>
|
* <ul>
|
||||||
|
|
|
@ -16,4 +16,4 @@
|
||||||
# * limitations under the License.
|
# * 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.DeclarativeUserProfileProvider;
|
||||||
import org.keycloak.userprofile.UserProfile;
|
import org.keycloak.userprofile.UserProfile;
|
||||||
import org.keycloak.userprofile.UserProfileContext;
|
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;
|
import java.util.Map;
|
||||||
|
|
||||||
public class CustomUserProfileProvider extends DeclarativeUserProfileProvider {
|
public class CustomUserProfileProvider extends DeclarativeUserProfileProvider {
|
||||||
|
|
||||||
public static final String ID = "custom-user-profile";
|
public CustomUserProfileProvider(KeycloakSession session, CustomUserProfileProviderFactory factory) {
|
||||||
|
super(session, factory);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -50,8 +29,4 @@ public class CustomUserProfileProvider extends DeclarativeUserProfileProvider {
|
||||||
return this.create(context, attributes, (UserModel) null);
|
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.
|
# * limitations under the License.
|
||||||
# */
|
# */
|
||||||
#
|
#
|
||||||
org.keycloak.testsuite.user.profile.CustomUserProfileProvider
|
org.keycloak.testsuite.user.profile.CustomUserProfileProviderFactory
|
Loading…
Reference in a new issue