[KEYCLOAK-18427] - Allowing switching to declarative provider

This commit is contained in:
Pedro Igor 2021-06-25 10:07:16 -03:00
parent 512bcd14f7
commit 948f453e2d
27 changed files with 247 additions and 252 deletions

View file

@ -61,8 +61,7 @@ public class Profile {
WEB_AUTHN(Type.DEFAULT, Type.PREVIEW),
CLIENT_POLICIES(Type.DEFAULT),
CIBA(Type.PREVIEW),
MAP_STORAGE(Type.EXPERIMENTAL),
DECLARATIVE_USER_PROFILE(Type.PREVIEW);
MAP_STORAGE(Type.EXPERIMENTAL);
private final Type typeProject;
private final Type typeProduct;

View file

@ -21,8 +21,8 @@ public class ProfileTest {
@Test
public void checkDefaultsKeycloak() {
Assert.assertEquals("community", Profile.getName());
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS, Profile.Feature.CIBA, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE);
assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.CIBA, Profile.Feature.DECLARATIVE_USER_PROFILE);
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS, Profile.Feature.CIBA, Profile.Feature.MAP_STORAGE);
assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.CIBA);
assertEquals(Profile.getDeprecatedFeatures(), Profile.Feature.UPLOAD_SCRIPTS);
Assert.assertTrue(Profile.Feature.WEB_AUTHN.hasDifferentProductType());
@ -37,8 +37,8 @@ public class ProfileTest {
Profile.init();
Assert.assertEquals("product", Profile.getName());
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS, Profile.Feature.WEB_AUTHN, Profile.Feature.CIBA, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE);
assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.WEB_AUTHN, Profile.Feature.CIBA, Profile.Feature.DECLARATIVE_USER_PROFILE);
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS, Profile.Feature.WEB_AUTHN, Profile.Feature.CIBA, Profile.Feature.MAP_STORAGE);
assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.WEB_AUTHN, Profile.Feature.CIBA);
assertEquals(Profile.getDeprecatedFeatures(), Profile.Feature.UPLOAD_SCRIPTS);
Assert.assertTrue(Profile.Feature.WEB_AUTHN.hasDifferentProductType());

View file

@ -138,6 +138,7 @@ import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.federated.UserFederatedStorageProvider;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.util.JsonSerialization;
import org.keycloak.validation.ValidationUtil;
@ -1077,6 +1078,11 @@ public class RepresentationToModel {
renameRealm(realm, rep.getRealm());
}
if (!Boolean.parseBoolean(rep.getAttributesOrEmpty().get("userProfileEnabled"))) {
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
provider.setConfiguration(null);
}
// Import attributes first, so the stuff saved directly on representation (displayName, bruteForce etc) has bigger priority
if (rep.getAttributes() != null) {
Set<String> attrsToRemove = new HashSet<>(realm.getAttributes().keySet());

View file

@ -275,11 +275,8 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
List<String> values = EMPTY_VALUE;
AttributeMetadata metadata = metadataByAttribute.get(attributeName);
// if the attribute is not provided and does not have view permission, use the current values
// this check makes possible to decide whether or not validation should happen for read-only attributes
// when the context does not have access to such attributes
if (user != null && !metadata.canView(createAttributeContext(metadata))) {
values = user.getAttributes().get(attributeName);
if (user != null && isIncludeAttributeIfNotProvided(metadata)) {
values = user.getAttributes().getOrDefault(attributeName, EMPTY_VALUE);
}
newAttributes.put(attributeName, values);
@ -302,6 +299,11 @@ public class DefaultAttributes extends HashMap<String, List<String>> implements
return newAttributes;
}
protected boolean isIncludeAttributeIfNotProvided(AttributeMetadata metadata) {
// user api expects that attributes are not updated if not provided when in legacy mode
return UserProfileContext.USER_API.equals(context);
}
/**
* <p>Checks whether an attribute is support by the profile configuration and the current context.
*

View file

@ -286,10 +286,14 @@ public class UserResource {
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
UserProfile profile = provider.create(USER_API, user);
Map<String, List<String>> attributes = profile.getAttributes().getReadable(false);
if (rep.getAttributes() != null) {
Map<String, List<String>> allowedAttributes = profile.getAttributes().getReadable(false);
if (!attributes.isEmpty()) {
rep.setAttributes(attributes);
for (String attributeName : rep.getAttributes().keySet()) {
if (!allowedAttributes.containsKey(attributeName)) {
rep.getAttributes().remove(attributeName);
}
}
}
return rep;

View file

@ -17,7 +17,7 @@
*
*/
package org.keycloak.userprofile.legacy;
package org.keycloak.userprofile;
import static org.keycloak.userprofile.DefaultAttributes.READ_ONLY_ATTRIBUTE_KEY;
import static org.keycloak.userprofile.UserProfileContext.ACCOUNT;
@ -44,16 +44,6 @@ import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.messages.Messages;
import org.keycloak.userprofile.AttributeContext;
import org.keycloak.userprofile.AttributeValidatorMetadata;
import org.keycloak.userprofile.Attributes;
import org.keycloak.userprofile.DefaultAttributes;
import org.keycloak.userprofile.DefaultUserProfile;
import org.keycloak.userprofile.UserProfile;
import org.keycloak.userprofile.UserProfileContext;
import org.keycloak.userprofile.UserProfileMetadata;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.UserProfileProviderFactory;
import org.keycloak.userprofile.validator.BlankAttributeValidator;
import org.keycloak.userprofile.validator.BrokeringFederatedUsernameHasValueValidator;
import org.keycloak.userprofile.validator.DuplicateEmailValidator;
@ -79,7 +69,20 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
KeycloakSession session = c.getSession();
KeycloakContext context = session.getContext();
RealmModel realm = context.getRealm();
return ((c.getContext() == REGISTRATION_PROFILE || c.getContext() == IDP_REVIEW) && !realm.isRegistrationEmailAsUsername()) || realm.isEditUsernameAllowed();
switch (c.getContext()) {
case REGISTRATION_PROFILE:
case IDP_REVIEW:
return !realm.isRegistrationEmailAsUsername();
case ACCOUNT_OLD:
case ACCOUNT:
case UPDATE_PROFILE:
return realm.isEditUsernameAllowed();
case USER_API:
return true;
default:
return false;
}
}
public static Pattern getRegexPatternString(String[] builtinReadOnlyAttributes) {

View file

@ -1,4 +1,4 @@
package org.keycloak.userprofile.config;
package org.keycloak.userprofile;
import java.util.HashMap;
import java.util.List;
@ -7,6 +7,7 @@ import java.util.Map;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel;
import org.keycloak.userprofile.AttributeMetadata;
import org.keycloak.userprofile.DeclarativeUserProfileProvider;
import org.keycloak.userprofile.DefaultAttributes;
import org.keycloak.userprofile.UserProfileContext;
import org.keycloak.userprofile.UserProfileMetadata;
@ -39,4 +40,9 @@ public class DeclarativeAttributes extends DefaultAttributes {
return attributes;
}
@Override
protected boolean isIncludeAttributeIfNotProvided(AttributeMetadata metadata) {
return !metadata.canView(createAttributeContext(metadata));
}
}

View file

@ -17,7 +17,7 @@
*
*/
package org.keycloak.userprofile.config;
package org.keycloak.userprofile;
import static org.keycloak.common.util.ObjectUtil.isBlank;
import static org.keycloak.protocol.oidc.TokenManager.getRequestedClientScopes;
@ -25,8 +25,6 @@ import static org.keycloak.userprofile.config.UPConfigUtils.readConfig;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@ -35,33 +33,28 @@ import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import org.keycloak.common.Profile;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.StreamUtil;
import org.keycloak.component.AmphibianProviderFactory;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientScopeModel.ClientScopeRemovedEvent;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderEvent;
import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.userprofile.AttributeContext;
import org.keycloak.userprofile.AttributeMetadata;
import org.keycloak.userprofile.AttributeValidatorMetadata;
import org.keycloak.userprofile.Attributes;
import org.keycloak.userprofile.UserProfileContext;
import org.keycloak.userprofile.UserProfileMetadata;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.userprofile.legacy.AbstractUserProfileProvider;
import org.keycloak.userprofile.config.DeclarativeUserProfileModel;
import org.keycloak.userprofile.config.UPAttribute;
import org.keycloak.userprofile.config.UPAttributePermissions;
import org.keycloak.userprofile.config.UPAttributeRequired;
import org.keycloak.userprofile.config.UPAttributeSelector;
import org.keycloak.userprofile.config.UPConfig;
import org.keycloak.userprofile.config.UPConfigUtils;
import org.keycloak.userprofile.validator.AttributeRequiredByMetadataValidator;
import org.keycloak.userprofile.validator.BlankAttributeValidator;
import org.keycloak.userprofile.validator.ImmutableAttributeValidator;
import org.keycloak.validate.AbstractSimpleValidator;
import org.keycloak.validate.ValidatorConfig;
@ -73,12 +66,12 @@ 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<DeclarativeUserProfileProvider>
implements AmphibianProviderFactory<DeclarativeUserProfileProvider>, EnvironmentDependentProviderFactory {
public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<UserProfileProvider>
implements AmphibianProviderFactory<UserProfileProvider> {
public static final String SYSTEM_DEFAULT_CONFIG_RESOURCE = "keycloak-default-user-profile.json";
public static final String ID = "declarative-user-profile";
public static final String UP_PIECES_COUNT_COMPONENT_CONFIG_KEY = "config-pieces-count";
public static final String REALM_USER_PROFILE_ENABLED = "userProfileEnabled";
private static final String PARSED_CONFIG_COMPONENT_KEY = "kc.user.profile.metadata";
private static final String UP_PIECE_COMPONENT_CONFIG_KEY_BASE = "config-piece-";
@ -106,7 +99,7 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
private String defaultRawConfig;
public DeclarativeUserProfileProvider() {
// for reflection
defaultRawConfig = UPConfigUtils.readDefaultConfig();
}
public DeclarativeUserProfileProvider(KeycloakSession session, Map<UserProfileContext, UserProfileMetadata> metadataRegistry, String defaultRawConfig) {
@ -120,18 +113,33 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
}
@Override
protected DeclarativeUserProfileProvider create(KeycloakSession session, Map<UserProfileContext, UserProfileMetadata> metadataRegistry) {
protected UserProfileProvider create(KeycloakSession session, Map<UserProfileContext, UserProfileMetadata> metadataRegistry) {
return new DeclarativeUserProfileProvider(session, metadataRegistry, defaultRawConfig);
}
@Override
protected Attributes createAttributes(UserProfileContext context, Map<String, ?> attributes,
UserModel user, UserProfileMetadata metadata) {
if (!isEnabled(session)) {
return new DefaultAttributes(context, attributes, user, metadata, session);
}
return new DeclarativeAttributes(context, attributes, user, metadata, session);
}
@Override
protected UserProfileMetadata configureUserProfile(UserProfileMetadata metadata, KeycloakSession session) {
UserProfileContext context = metadata.getContext();
UserProfileMetadata decoratedMetadata = metadata.clone();
if (!isEnabled(session)) {
if(!context.equals(UserProfileContext.USER_API) && !context.equals(UserProfileContext.REGISTRATION_USER_CREATION)) {
decoratedMetadata.addAttribute(UserModel.FIRST_NAME, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(
Messages.MISSING_FIRST_NAME))).setAttributeDisplayName("${firstName}");
decoratedMetadata.addAttribute(UserModel.LAST_NAME, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_LAST_NAME))).setAttributeDisplayName("${lastName}");
return decoratedMetadata;
}
}
ComponentModel model = getComponentModelOrCreate(session);
Map<UserProfileContext, UserProfileMetadata> metadataMap = model.getNote(PARSED_CONFIG_COMPONENT_KEY);
@ -141,7 +149,7 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
model.setNote(PARSED_CONFIG_COMPONENT_KEY, metadataMap);
}
return metadataMap.computeIfAbsent(metadata.getContext(), (context) -> decorateUserProfileForCache(metadata, model));
return metadataMap.computeIfAbsent(context, (c) -> decorateUserProfileForCache(decoratedMetadata, model));
}
@Override
@ -175,6 +183,10 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
@Override
public String getConfiguration() {
if (!isEnabled(session)) {
return null;
}
String cfg = getConfigJsonFromComponentModel(getComponentModel());
if (isBlank(cfg)) {
@ -190,6 +202,8 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
removeConfigJsonFromComponentModel(component);
RealmModel realm = session.getContext().getRealm();
if (!isBlank(configuration)) {
// store new parts
List<String> parts = UPConfigUtils.getChunks(configuration, 3800);
@ -202,19 +216,15 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
for (String part : parts) {
config.putSingle(UP_PIECE_COMPONENT_CONFIG_KEY_BASE + (i++), part);
}
}
session.getContext().getRealm().updateComponent(component);
realm.updateComponent(component);
} else {
realm.removeComponent(component);
}
}
@Override
public void postInit(KeycloakSessionFactory factory) {
// TODO: We should avoid blocking operations during startup. Need to review this.
try (InputStream is = getClass().getResourceAsStream(SYSTEM_DEFAULT_CONFIG_RESOURCE)) {
defaultRawConfig = StreamUtil.readString(is, Charset.defaultCharset());
} catch (IOException cause) {
throw new RuntimeException("Failed to load default user profile config file", cause);
}
}
@Override
@ -231,23 +241,19 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
* 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.
*
* @param metadata base to be decorated based on configuration loaded from component model
* @param decoratedMetadata base to be decorated based on configuration loaded from component model
* @param model component model to get "per realm" configuration from
* @return decorated metadata
*/
protected UserProfileMetadata decorateUserProfileForCache(UserProfileMetadata metadata, ComponentModel model) {
UserProfileContext context = metadata.getContext();
protected UserProfileMetadata decorateUserProfileForCache(UserProfileMetadata decoratedMetadata, ComponentModel model) {
UserProfileContext context = decoratedMetadata.getContext();
UPConfig parsedConfig = getParsedConfig(model);
// do not change config for REGISTRATION_USER_CREATION context, everything important is covered thanks to REGISTRATION_PROFILE
if (parsedConfig == null || context == UserProfileContext.REGISTRATION_USER_CREATION) {
return metadata;
return decoratedMetadata;
}
// need to clone otherwise changes to profile config are going to be reflected
// in the default config
UserProfileMetadata decoratedMetadata = metadata.clone();
for (UPAttribute attrConfig : parsedConfig.getAttributes()) {
String attributeName = attrConfig.getName();
List<AttributeValidatorMetadata> validators = new ArrayList<>();
@ -425,8 +431,14 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
model.getConfig().remove(UP_PIECES_COUNT_COMPONENT_CONFIG_KEY);
}
@Override
public boolean isSupported() {
return Profile.isFeatureEnabled(Profile.Feature.DECLARATIVE_USER_PROFILE);
/**
* Returns whether the declarative provider is enabled to a realm
*
* @deprecated should be removed once {@link DeclarativeUserProfileProvider} becomes the default.
* @param session the session
* @return {@code true} if the declarative provider is enabled. Otherwise, {@code false}.
*/
private Boolean isEnabled(KeycloakSession session) {
return session.getContext().getRealm().getAttribute(REALM_USER_PROFILE_ENABLED, false);
}
}

View file

@ -20,6 +20,7 @@
package org.keycloak.userprofile.config;
import org.keycloak.component.ComponentModel;
import org.keycloak.userprofile.DeclarativeUserProfileProvider;
import org.keycloak.userprofile.UserProfileProvider;
/**

View file

@ -20,6 +20,7 @@ import static org.keycloak.common.util.ObjectUtil.isBlank;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@ -28,6 +29,7 @@ import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.keycloak.common.util.StreamUtil;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
@ -45,6 +47,7 @@ import org.keycloak.validate.Validators;
*/
public class UPConfigUtils {
private static final String SYSTEM_DEFAULT_CONFIG_RESOURCE = "keycloak-default-user-profile.json";
public static final String ROLE_USER = "user";
public static final String ROLE_ADMIN = "admin";
@ -260,4 +263,11 @@ public class UPConfigUtils {
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
public static String readDefaultConfig() {
try (InputStream is = UPConfigUtils.class.getResourceAsStream(SYSTEM_DEFAULT_CONFIG_RESOURCE)) {
return StreamUtil.readString(is, Charset.defaultCharset());
} catch (IOException cause) {
throw new RuntimeException("Failed to load default user profile config file", cause);
}
}
}

View file

@ -1,70 +0,0 @@
/*
*
* * 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.
*
*/
package org.keycloak.userprofile.legacy;
import java.util.Map;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserModel;
import org.keycloak.services.messages.Messages;
import org.keycloak.userprofile.AttributeValidatorMetadata;
import org.keycloak.userprofile.UserProfileContext;
import org.keycloak.userprofile.UserProfileMetadata;
import org.keycloak.userprofile.validator.BlankAttributeValidator;
/**
* @author <a href="mailto:markus.till@bosch.io">Markus Till</a>
*/
public class DefaultUserProfileProvider extends AbstractUserProfileProvider<DefaultUserProfileProvider> {
private static final String PROVIDER_ID = "legacy-user-profile";
public DefaultUserProfileProvider() {
// for reflection
}
public DefaultUserProfileProvider(KeycloakSession session, Map<UserProfileContext, UserProfileMetadata> validators) {
super(session, validators);
}
@Override
protected DefaultUserProfileProvider create(KeycloakSession session, Map<UserProfileContext, UserProfileMetadata> metadataRegistry) {
return new DefaultUserProfileProvider(session, metadataRegistry);
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public int order() {
return 1;
}
protected UserProfileMetadata configureUserProfile(UserProfileMetadata metadata) {
UserProfileContext ctx = metadata.getContext();
if(ctx != UserProfileContext.USER_API && ctx != UserProfileContext.REGISTRATION_USER_CREATION) {
metadata.addAttribute(UserModel.FIRST_NAME, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_FIRST_NAME))).setAttributeDisplayName("${firstName}");
metadata.addAttribute(UserModel.LAST_NAME, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_LAST_NAME))).setAttributeDisplayName("${lastName}");
}
return metadata;
}
}

View file

@ -16,5 +16,4 @@
# * limitations under the License.
# */
#
org.keycloak.userprofile.legacy.DefaultUserProfileProvider
org.keycloak.userprofile.config.DeclarativeUserProfileProvider
org.keycloak.userprofile.DeclarativeUserProfileProvider

View file

@ -19,9 +19,6 @@ echo ** Adding max-detail-length to eventsStore spi **
echo ** Adding spi=userProfile with legacy-user-profile configuration of read-only attributes **
/subsystem=keycloak-server/spi=userProfile/:add
/subsystem=keycloak-server/spi=userProfile/provider=legacy-user-profile/:add(properties={},enabled=true)
/subsystem=keycloak-server/spi=userProfile/provider=legacy-user-profile/:map-put(name=properties,key=read-only-attributes,value=[deniedFoo,deniedBar*,deniedSome/thing,deniedsome*thing])
/subsystem=keycloak-server/spi=userProfile/provider=legacy-user-profile/:map-put(name=properties,key=admin-read-only-attributes,value=[deniedSomeAdmin])
/subsystem=keycloak-server/spi=userProfile/provider=declarative-user-profile/:add(properties={},enabled=true)
/subsystem=keycloak-server/spi=userProfile/provider=declarative-user-profile/:map-put(name=properties,key=read-only-attributes,value=[deniedFoo,deniedBar*,deniedSome/thing,deniedsome*thing])
/subsystem=keycloak-server/spi=userProfile/provider=declarative-user-profile/:map-put(name=properties,key=admin-read-only-attributes,value=[deniedSomeAdmin])

View file

@ -25,6 +25,3 @@ spi.truststore.file.password=secret
# http client connection reuse settings
spi.connections-http-client.default.reuse-connections=false
# user profile provider settings
spi.user-profile.provider=${keycloak.userProfile.provider:legacy-user-profile}

View file

@ -20,50 +20,40 @@
package org.keycloak.testsuite.admin.userprofile;
import static org.junit.Assert.assertEquals;
import static org.keycloak.userprofile.DeclarativeUserProfileProvider.REALM_USER_PROFILE_ENABLED;
import static org.keycloak.userprofile.config.UPConfigUtils.readDefaultConfig;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import org.junit.Test;
import org.keycloak.admin.client.resource.UserProfileResource;
import org.keycloak.common.Profile;
import org.keycloak.common.util.StreamUtil;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.admin.AbstractAdminTest;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
import org.keycloak.userprofile.UserProfileSpi;
import org.keycloak.userprofile.config.DeclarativeUserProfileProvider;
import org.keycloak.userprofile.DeclarativeUserProfileProvider;
import org.keycloak.userprofile.config.UPConfigUtils;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@EnableFeature(value = Profile.Feature.DECLARATIVE_USER_PROFILE, skipRestart = false)
@SetDefaultProvider(spi = UserProfileSpi.ID, providerId = DeclarativeUserProfileProvider.ID,
beforeEnableFeature = false,
onlyUpdateDefault = true
)
@AuthServerContainerExclude(AuthServerContainerExclude.AuthServer.REMOTE)
public class UserProfileAdminTest extends AbstractAdminTest {
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
if (testRealm.getAttributes() == null) {
testRealm.setAttributes(new HashMap<>());
}
testRealm.getAttributes().put(REALM_USER_PROFILE_ENABLED, Boolean.TRUE.toString());
}
@Test
public void testDefaultConfigIfNoneSet() {
String defaultRawConfig;
try (InputStream is = DeclarativeUserProfileProvider.class.getResourceAsStream(DeclarativeUserProfileProvider.SYSTEM_DEFAULT_CONFIG_RESOURCE)) {
defaultRawConfig = StreamUtil.readString(is, Charset.defaultCharset());
} catch (IOException cause) {
throw new RuntimeException("Failed to load default user profile config file", cause);
}
assertEquals(defaultRawConfig, testRealm().users().userProfile().getConfiguration());
assertEquals(readDefaultConfig(), testRealm().users().userProfile().getConfiguration());
}
@Test

View file

@ -39,6 +39,10 @@ import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.util.*;
import javax.mail.internet.MimeMessage;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.jgroups.util.Util.assertTrue;
import static org.junit.Assert.assertEquals;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
@ -261,10 +265,20 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest {
registerPage.assertCurrent();
assertEquals("Please specify username.", registerPage.getInputAccountErrors().getUsernameError());
assertEquals("Please specify first name.", registerPage.getInputAccountErrors().getFirstNameError());
assertEquals("Please specify last name.", registerPage.getInputAccountErrors().getLastNameError());
assertEquals("Please specify email.", registerPage.getInputAccountErrors().getEmailError());
assertEquals("Please specify password.", registerPage.getInputPasswordErrors().getPasswordError());
assertThat(registerPage.getInputAccountErrors().getFirstNameError(), anyOf(
containsString("Please specify first name"),
containsString("Please specify this field")
));
assertThat(registerPage.getInputAccountErrors().getLastNameError(), anyOf(
containsString("Please specify last name"),
containsString("Please specify this field")
));
assertThat(registerPage.getInputAccountErrors().getEmailError(), anyOf(
containsString("Please specify email"),
containsString("Please specify this field")
));
assertThat(registerPage.getInputPasswordErrors().getPasswordError(), is("Please specify password."));
events.expectRegister(null, "registerUserMissingUsername@email")
.removeDetail(Details.USERNAME)

View file

@ -17,9 +17,11 @@
package org.keycloak.testsuite.forms;
import static org.junit.Assert.assertEquals;
import static org.keycloak.userprofile.DeclarativeUserProfileProvider.REALM_USER_PROFILE_ENABLED;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import javax.ws.rs.core.Response;
@ -29,15 +31,12 @@ import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.Profile;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
@ -47,19 +46,12 @@ import org.keycloak.testsuite.pages.VerifyEmailPage;
import org.keycloak.testsuite.util.ClientScopeBuilder;
import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.KeycloakModelUtils;
import org.keycloak.userprofile.UserProfileSpi;
import org.keycloak.userprofile.config.DeclarativeUserProfileProvider;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
* @author Vlastimil Elias <velias@redhat.com>
*/
@EnableFeature(value = Profile.Feature.DECLARATIVE_USER_PROFILE, skipRestart = false)
@SetDefaultProvider(spi = UserProfileSpi.ID, providerId = DeclarativeUserProfileProvider.ID,
beforeEnableFeature = false,
onlyUpdateDefault = true
)
@AuthServerContainerExclude(AuthServerContainerExclude.AuthServer.REMOTE)
public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest {
@ -110,11 +102,14 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest {
client_scope_optional = KeycloakModelUtils.createClient(testRealm, "client-b");
client_scope_optional.setOptionalClientScopes(scopes);
client_scope_optional.setRedirectUris(Collections.singletonList("*"));
if (testRealm.getAttributes() == null) {
testRealm.setAttributes(new HashMap<>());
}
testRealm.getAttributes().put(REALM_USER_PROFILE_ENABLED, Boolean.TRUE.toString());
}
@Test
public void testRregisterUserSuccess_lastNameOptional() {
public void testRegisterUserSuccess_lastNameOptional() {
setUserProfileConfiguration("{\"attributes\": ["
+ UP_CONFIG_BASIC_ATTRIBUTES
+ "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},"

View file

@ -0,0 +1,24 @@
package org.keycloak.testsuite.forms;
import static org.keycloak.userprofile.DeclarativeUserProfileProvider.REALM_USER_PROFILE_ENABLED;
import java.util.HashMap;
import org.keycloak.representations.idm.RealmRepresentation;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class UserProfileRegisterTest extends RegisterTest {
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
super.configureTestRealm(testRealm);
if (testRealm.getAttributes() == null) {
testRealm.setAttributes(new HashMap<>());
}
testRealm.getAttributes().put(REALM_USER_PROFILE_ENABLED, Boolean.TRUE.toString());
}
}

View file

@ -19,9 +19,11 @@ package org.keycloak.testsuite.forms;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.keycloak.userprofile.DeclarativeUserProfileProvider.REALM_USER_PROFILE_ENABLED;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
@ -33,7 +35,6 @@ import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.Profile;
import org.keycloak.events.EventType;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.ClientRepresentation;
@ -43,8 +44,6 @@ import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage;
@ -54,16 +53,10 @@ import org.keycloak.testsuite.util.KeycloakModelUtils;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.userprofile.UserProfileSpi;
import org.keycloak.userprofile.config.DeclarativeUserProfileProvider;
/**
* @author Vlastimil Elias <velias@redhat.com>
*/
@EnableFeature(value = Profile.Feature.DECLARATIVE_USER_PROFILE, skipRestart = false)
@SetDefaultProvider(spi = UserProfileSpi.ID, providerId = DeclarativeUserProfileProvider.ID,
beforeEnableFeature = false,
onlyUpdateDefault = true)
@AuthServerContainerExclude(AuthServerContainerExclude.AuthServer.REMOTE)
public class VerifyProfileTest extends AbstractTestRealmKeycloakTest {
@ -135,6 +128,10 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest {
client_scope_optional = KeycloakModelUtils.createClient(testRealm, "client-b");
client_scope_optional.setOptionalClientScopes(Collections.singletonList(SCOPE_DEPARTMENT));
client_scope_optional.setRedirectUris(Collections.singletonList("*"));
if (testRealm.getAttributes() == null) {
testRealm.setAttributes(new HashMap<>());
}
testRealm.getAttributes().put(REALM_USER_PROFILE_ENABLED, Boolean.TRUE.toString());
}
@Rule

View file

@ -19,6 +19,9 @@
package org.keycloak.testsuite.user.profile;
import static org.keycloak.userprofile.DeclarativeUserProfileProvider.REALM_USER_PROFILE_ENABLED;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@ -27,10 +30,11 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.userprofile.config.DeclarativeUserProfileProvider;
import org.keycloak.userprofile.DeclarativeUserProfileProvider;
import org.keycloak.userprofile.UserProfileProvider;
/**
@ -233,4 +237,12 @@ public abstract class AbstractUserProfileTest extends AbstractTestRealmKeycloakT
}
};
}
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
if (testRealm.getAttributes() == null) {
testRealm.setAttributes(new HashMap<>());
}
testRealm.getAttributes().put(REALM_USER_PROFILE_ENABLED, Boolean.TRUE.toString());
}
}

View file

@ -43,10 +43,8 @@ import java.util.function.Consumer;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.common.Profile;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@ -54,11 +52,8 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.messages.Messages;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
import org.keycloak.testsuite.runonserver.RunOnServer;
import org.keycloak.userprofile.UserProfileSpi;
import org.keycloak.userprofile.config.DeclarativeUserProfileProvider;
import org.keycloak.userprofile.DeclarativeUserProfileProvider;
import org.keycloak.userprofile.config.UPAttribute;
import org.keycloak.userprofile.config.UPAttributePermissions;
import org.keycloak.userprofile.config.UPAttributeRequired;
@ -80,15 +75,12 @@ import org.keycloak.validate.validators.LengthValidator;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@EnableFeature(Profile.Feature.DECLARATIVE_USER_PROFILE)
@SetDefaultProvider(spi = UserProfileSpi.ID, providerId = DeclarativeUserProfileProvider.ID,
beforeEnableFeature = false,
onlyUpdateDefault = true)
@AuthServerContainerExclude(AuthServerContainerExclude.AuthServer.REMOTE)
public class UserProfileTest extends AbstractUserProfileTest {
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
super.configureTestRealm(testRealm);
testRealm.setClientScopes(new ArrayList<>());
testRealm.getClientScopes().add(ClientScopeBuilder.create().name("customer").protocol("openid-connect").build());
testRealm.getClientScopes().add(ClientScopeBuilder.create().name("client-a").protocol("openid-connect").build());

View file

@ -220,10 +220,6 @@
"userProfile": {
"provider": "${keycloak.userProfile.provider:}",
"legacy-user-profile": {
"read-only-attributes": [ "deniedFoo", "deniedBar*", "deniedSome/thing", "deniedsome*thing" ],
"admin-read-only-attributes": [ "deniedSomeAdmin" ]
},
"declarative-user-profile": {
"read-only-attributes": [ "deniedFoo", "deniedBar*", "deniedSome/thing", "deniedsome*thing" ],
"admin-read-only-attributes": [ "deniedSomeAdmin" ]

View file

@ -140,10 +140,6 @@
"userProfile": {
"provider": "${keycloak.userProfile.provider:}",
"legacy-user-profile": {
"read-only-attributes": [ "deniedFoo", "deniedBar*", "deniedSome/thing", "deniedsome*thing" ],
"admin-read-only-attributes": [ "deniedSomeAdmin" ]
},
"declarative-user-profile": {
"read-only-attributes": [ "deniedFoo", "deniedBar*", "deniedSome/thing", "deniedsome*thing" ],
"admin-read-only-attributes": [ "deniedSomeAdmin" ]

View file

@ -32,6 +32,8 @@ realm-detail.protocol-endpoints.tooltip=Shows the configuration of the protocol
realm-detail.protocol-endpoints.oidc=OpenID Endpoint Configuration
realm-detail.protocol-endpoints.saml=SAML 2.0 Identity Provider Metadata
realm-detail.userManagedAccess.tooltip=If enabled, users are allowed to manage their resources and permissions using the Account Management Console.
userProfileEnabled=User Profile Enabled
userProfileEnabled.tooltip=If enabled, allows managing user profiles.
userManagedAccess=User-Managed Access
registrationAllowed=User registration
registrationAllowed.tooltip=Enable/disable the registration page. A link for registration will show on login page too.

View file

@ -280,8 +280,10 @@ module.controller('RealmDetailCtrl', function($scope, Current, Realm, realm, ser
}
}
$scope.realm = angular.copy(realm);
$scope.realm.attributes['userProfileEnabled'] = $scope.realm.attributes['userProfileEnabled'] == 'true';
var oldCopy = angular.copy($scope.realm);
$scope.realmCopy = oldCopy;
$scope.changed = $scope.create;
@ -309,6 +311,7 @@ module.controller('RealmDetailCtrl', function($scope, Current, Realm, realm, ser
if (Current.realms[i].realm == realmCopy.realm) {
Current.realm = Current.realms[i];
oldCopy = angular.copy($scope.realm);
$scope.realmCopy = oldCopy;
}
}
});

View file

@ -55,6 +55,14 @@
<kc-tooltip>{{:: 'realm-detail.userManagedAccess.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="userProfileEnabled">{{:: 'userProfileEnabled' | translate}}</label>
<div class="col-md-6">
<input ng-model="realm.attributes['userProfileEnabled']" name="userProfileEnabled" id="userProfileEnabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'userProfileEnabled.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
<label class="col-md-2 control-label">{{:: 'endpoints' | translate}}</label>
<div class="col-md-6">

View file

@ -19,6 +19,6 @@
<a href="#/realms/{{realm.realm}}/client-policies/profiles">{{:: 'realm-tab-client-policies' | translate}}</a>
</li>
<li ng-class="{active: path[2] == 'defense'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/defense/headers">{{:: 'realm-tab-security-defenses' | translate}}</a></li>
<li ng-class="{active: path[2] == 'user-profile'}" data-ng-show="access.viewRealm && serverInfo.featureEnabled('DECLARATIVE_USER_PROFILE')"><a href="#/realms/{{realm.realm}}/user-profile">{{:: 'realm-tab-user-profile' | translate}}</a></li>
<li ng-class="{active: path[2] == 'user-profile'}" data-ng-show="access.viewRealm && (realm.attributes['userProfileEnabled'] == true || realm.attributes['userProfileEnabled'] == 'true')"><a href="#/realms/{{realm.realm}}/user-profile">{{:: 'realm-tab-user-profile' | translate}}</a></li>
</ul>
</div>