diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java index 29e3074944..3684fea4c2 100755 --- a/common/src/main/java/org/keycloak/common/Profile.java +++ b/common/src/main/java/org/keycloak/common/Profile.java @@ -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; diff --git a/common/src/test/java/org/keycloak/common/ProfileTest.java b/common/src/test/java/org/keycloak/common/ProfileTest.java index 1d3d07c2b4..0261f15cbc 100644 --- a/common/src/test/java/org/keycloak/common/ProfileTest.java +++ b/common/src/test/java/org/keycloak/common/ProfileTest.java @@ -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()); diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 8faaf69e66..9f7f03c183 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -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 attrsToRemove = new HashSet<>(realm.getAttributes().keySet()); diff --git a/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultAttributes.java b/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultAttributes.java index b3a32b49c0..fcf6f8d364 100644 --- a/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultAttributes.java +++ b/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultAttributes.java @@ -275,11 +275,8 @@ public class DefaultAttributes extends HashMap> implements List 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> 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); + } + /** *

Checks whether an attribute is support by the profile configuration and the current context. * diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java index c803354d25..49f9c082b3 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java @@ -286,10 +286,14 @@ public class UserResource { UserProfileProvider provider = session.getProvider(UserProfileProvider.class); UserProfile profile = provider.create(USER_API, user); - Map> attributes = profile.getAttributes().getReadable(false); + if (rep.getAttributes() != null) { + Map> 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; diff --git a/services/src/main/java/org/keycloak/userprofile/legacy/AbstractUserProfileProvider.java b/services/src/main/java/org/keycloak/userprofile/AbstractUserProfileProvider.java similarity index 95% rename from services/src/main/java/org/keycloak/userprofile/legacy/AbstractUserProfileProvider.java rename to services/src/main/java/org/keycloak/userprofile/AbstractUserProfileProvider.java index 614f6889fd..6ee0ee6f9e 100644 --- a/services/src/main/java/org/keycloak/userprofile/legacy/AbstractUserProfileProvider.java +++ b/services/src/main/java/org/keycloak/userprofile/AbstractUserProfileProvider.java @@ -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 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) { diff --git a/services/src/main/java/org/keycloak/userprofile/config/DeclarativeAttributes.java b/services/src/main/java/org/keycloak/userprofile/DeclarativeAttributes.java similarity index 83% rename from services/src/main/java/org/keycloak/userprofile/config/DeclarativeAttributes.java rename to services/src/main/java/org/keycloak/userprofile/DeclarativeAttributes.java index c1acb9d00f..3638b7a020 100644 --- a/services/src/main/java/org/keycloak/userprofile/config/DeclarativeAttributes.java +++ b/services/src/main/java/org/keycloak/userprofile/DeclarativeAttributes.java @@ -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)); + } } diff --git a/services/src/main/java/org/keycloak/userprofile/config/DeclarativeUserProfileProvider.java b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java similarity index 84% rename from services/src/main/java/org/keycloak/userprofile/config/DeclarativeUserProfileProvider.java rename to services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java index 11c9e21288..ed915c9896 100644 --- a/services/src/main/java/org/keycloak/userprofile/config/DeclarativeUserProfileProvider.java +++ b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java @@ -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 Pedro Igor * @author Vlastimil Elias */ -public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider - implements AmphibianProviderFactory, EnvironmentDependentProviderFactory { +public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider + implements AmphibianProviderFactory { - 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 metadataRegistry, String defaultRawConfig) { @@ -120,18 +113,33 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider< } @Override - protected DeclarativeUserProfileProvider create(KeycloakSession session, Map metadataRegistry) { + protected UserProfileProvider create(KeycloakSession session, Map metadataRegistry) { return new DeclarativeUserProfileProvider(session, metadataRegistry, defaultRawConfig); } @Override protected Attributes createAttributes(UserProfileContext context, Map 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 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 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 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); } } diff --git a/services/src/main/java/org/keycloak/userprofile/config/DeclarativeUserProfileModel.java b/services/src/main/java/org/keycloak/userprofile/config/DeclarativeUserProfileModel.java index 0dc74d12b5..7a82292e25 100644 --- a/services/src/main/java/org/keycloak/userprofile/config/DeclarativeUserProfileModel.java +++ b/services/src/main/java/org/keycloak/userprofile/config/DeclarativeUserProfileModel.java @@ -20,6 +20,7 @@ package org.keycloak.userprofile.config; import org.keycloak.component.ComponentModel; +import org.keycloak.userprofile.DeclarativeUserProfileProvider; import org.keycloak.userprofile.UserProfileProvider; /** diff --git a/services/src/main/java/org/keycloak/userprofile/config/UPConfigUtils.java b/services/src/main/java/org/keycloak/userprofile/config/UPConfigUtils.java index ceb174c9e4..e1cf65c207 100644 --- a/services/src/main/java/org/keycloak/userprofile/config/UPConfigUtils.java +++ b/services/src/main/java/org/keycloak/userprofile/config/UPConfigUtils.java @@ -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); + } + } } diff --git a/services/src/main/java/org/keycloak/userprofile/legacy/DefaultUserProfileProvider.java b/services/src/main/java/org/keycloak/userprofile/legacy/DefaultUserProfileProvider.java deleted file mode 100644 index d270360811..0000000000 --- a/services/src/main/java/org/keycloak/userprofile/legacy/DefaultUserProfileProvider.java +++ /dev/null @@ -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 Markus Till - */ -public class DefaultUserProfileProvider extends AbstractUserProfileProvider { - - private static final String PROVIDER_ID = "legacy-user-profile"; - - public DefaultUserProfileProvider() { - // for reflection - } - - public DefaultUserProfileProvider(KeycloakSession session, Map validators) { - super(session, validators); - } - - @Override - protected DefaultUserProfileProvider create(KeycloakSession session, Map 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; - } -} diff --git a/services/src/main/resources/META-INF/services/org.keycloak.userprofile.UserProfileProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.userprofile.UserProfileProviderFactory index d04172afe7..45956ba080 100644 --- a/services/src/main/resources/META-INF/services/org.keycloak.userprofile.UserProfileProviderFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.userprofile.UserProfileProviderFactory @@ -16,5 +16,4 @@ # * limitations under the License. # */ # -org.keycloak.userprofile.legacy.DefaultUserProfileProvider -org.keycloak.userprofile.config.DeclarativeUserProfileProvider \ No newline at end of file +org.keycloak.userprofile.DeclarativeUserProfileProvider \ No newline at end of file diff --git a/testsuite/integration-arquillian/servers/auth-server/jboss/common/jboss-cli/keycloak-server-subsystem.cli b/testsuite/integration-arquillian/servers/auth-server/jboss/common/jboss-cli/keycloak-server-subsystem.cli index 8432ce8905..c78131e18a 100644 --- a/testsuite/integration-arquillian/servers/auth-server/jboss/common/jboss-cli/keycloak-server-subsystem.cli +++ b/testsuite/integration-arquillian/servers/auth-server/jboss/common/jboss-cli/keycloak-server-subsystem.cli @@ -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]) diff --git a/testsuite/integration-arquillian/servers/auth-server/quarkus/src/main/content/conf/keycloak.properties b/testsuite/integration-arquillian/servers/auth-server/quarkus/src/main/content/conf/keycloak.properties index a8cc8ea308..5e1c7f1121 100644 --- a/testsuite/integration-arquillian/servers/auth-server/quarkus/src/main/content/conf/keycloak.properties +++ b/testsuite/integration-arquillian/servers/auth-server/quarkus/src/main/content/conf/keycloak.properties @@ -24,7 +24,4 @@ spi.truststore.file.file=${kc.home.dir}/conf/keycloak.truststore 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} +spi.connections-http-client.default.reuse-connections=false \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/userprofile/UserProfileAdminTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/userprofile/UserProfileAdminTest.java index c764a8e483..1a8f756c28 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/userprofile/UserProfileAdminTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/userprofile/UserProfileAdminTest.java @@ -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 Pedro Igor */ -@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 diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java index 1180b7526c..385e143435 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java @@ -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) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterWithUserProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterWithUserProfileTest.java index ccad3b9201..717d2061b4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterWithUserProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterWithUserProfileTest.java @@ -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 Stian Thorgersen * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. * @author Vlastimil Elias */ -@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 { @@ -103,18 +95,21 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { List scopes = new ArrayList<>(); scopes.add(SCOPE_LAST_NAME); scopes.add(VerifyProfileTest.SCOPE_DEPARTMENT); - + client_scope_default = KeycloakModelUtils.createClient(testRealm, "client-a"); client_scope_default.setDefaultClientScopes(scopes); client_scope_default.setRedirectUris(Collections.singletonList("*")); 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\": {}}," @@ -248,14 +243,14 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { events.expectRegister("registeruserinvalidlastnamelength", "registerUserInvalidLastNameLength@email") .error("invalid_registration").assertEvent(); } - + @Test public void testAttributeDisplayName() { - setUserProfileConfiguration("{\"attributes\": [" - + "{\"name\": \"firstName\",\"displayName\":\"${firstName}\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\",\"displayName\":\"${firstName}\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," + "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + "}," - + "{\"name\": \"department\", \"displayName\" : \"Department\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{}}" + + "{\"name\": \"department\", \"displayName\" : \"Department\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{}}" + "]}"); loginPage.open(); @@ -271,50 +266,50 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { // direct value in display name Assert.assertEquals("Department",registerPage.getLabelForField("department")); } - + @Test public void testRegisterUserSuccess_requiredReadOnlyAttributeNotRenderedAndNotBlockingRegistration() { - setUserProfileConfiguration("{\"attributes\": [" - + "{\"name\": \"firstName\",\"displayName\":\"${firstName}\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\",\"displayName\":\"${firstName}\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," + "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + "}," - + "{\"name\": \"department\", \"displayName\" : \"Department\", " + VerifyProfileTest.PERMISSIONS_ADMIN_EDITABLE + ", \"required\":{}}" + + "{\"name\": \"department\", \"displayName\" : \"Department\", " + VerifyProfileTest.PERMISSIONS_ADMIN_EDITABLE + ", \"required\":{}}" + "]}"); loginPage.open(); loginPage.clickRegister(); registerPage.assertCurrent(); - + Assert.assertFalse(registerPage.isDepartmentPresent()); - + registerPage.register("FirstName", "LastName", "requiredReadOnlyAttributeNotRenderedAndNotBlockingRegistration@email", "requiredReadOnlyAttributeNotRenderedAndNotBlockingRegistration", "password", "password"); - + appPage.assertCurrent(); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); } - - + + @Test public void testRegisterUserSuccess_attributeRequiredAndSelectedByScopeMustBeSet() { - setUserProfileConfiguration("{\"attributes\": [" - + "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," + "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + "}," - + "{\"name\": \"department\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{}, \"selector\":{\"scopes\":[\""+VerifyProfileTest.SCOPE_DEPARTMENT+"\"]}}" + + "{\"name\": \"department\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{}, \"selector\":{\"scopes\":[\""+VerifyProfileTest.SCOPE_DEPARTMENT+"\"]}}" + "]}"); oauth.scope(VerifyProfileTest.SCOPE_DEPARTMENT).clientId(client_scope_optional.getClientId()).openLoginForm(); loginPage.clickRegister(); registerPage.assertCurrent(); - + //check required validation works registerPage.register("FirstAA", "LastAA", "attributeRequiredAndSelectedByScopeMustBeSet@email", "attributeRequiredAndSelectedByScopeMustBeSet", "password", "password", ""); registerPage.assertCurrent(); - + registerPage.register("FirstAA", "LastAA", "attributeRequiredAndSelectedByScopeMustBeSet@email", "attributeRequiredAndSelectedByScopeMustBeSet", "password", "password", "DepartmentAA"); - + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); @@ -327,23 +322,23 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { @Test public void testRegisterUserSuccess_attributeNotRequiredAndSelectedByScopeCanBeIgnored() { - setUserProfileConfiguration("{\"attributes\": [" - + "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," + "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," - + "{\"name\": \"department\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"selector\":{\"scopes\":[\""+VerifyProfileTest.SCOPE_DEPARTMENT+"\"]}}" + + "{\"name\": \"department\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"selector\":{\"scopes\":[\""+VerifyProfileTest.SCOPE_DEPARTMENT+"\"]}}" + "]}"); oauth.scope(VerifyProfileTest.SCOPE_DEPARTMENT).clientId(client_scope_optional.getClientId()).openLoginForm(); loginPage.clickRegister(); registerPage.assertCurrent(); - + Assert.assertTrue(registerPage.isDepartmentPresent()); registerPage.register("FirstAA", "LastAA", "attributeNotRequiredAndSelectedByScopeCanBeIgnored@email", "attributeNotRequiredAndSelectedByScopeCanBeIgnored", "password", "password", null); - + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); - String userId = events.expectRegister("attributeNotRequiredAndSelectedByScopeCanBeIgnored", "attributeNotRequiredAndSelectedByScopeCanBeIgnored@email",client_scope_optional.getClientId()).assertEvent().getUserId(); + String userId = events.expectRegister("attributeNotRequiredAndSelectedByScopeCanBeIgnored", "attributeNotRequiredAndSelectedByScopeCanBeIgnored@email",client_scope_optional.getClientId()).assertEvent().getUserId(); UserRepresentation user = getUser(userId); assertEquals("FirstAA", user.getFirstName()); assertEquals("LastAA", user.getLastName()); @@ -353,23 +348,23 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { @Test public void testRegisterUserSuccess_attributeNotRequiredAndSelectedByScopeCanBeSet() { - setUserProfileConfiguration("{\"attributes\": [" - + "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," + "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," - + "{\"name\": \"department\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"selector\":{\"scopes\":[\""+VerifyProfileTest.SCOPE_DEPARTMENT+"\"]}}" + + "{\"name\": \"department\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"selector\":{\"scopes\":[\""+VerifyProfileTest.SCOPE_DEPARTMENT+"\"]}}" + "]}"); oauth.clientId(client_scope_default.getClientId()).openLoginForm(); loginPage.clickRegister(); registerPage.assertCurrent(); - + Assert.assertTrue(registerPage.isDepartmentPresent()); registerPage.register("FirstAA", "LastAA", "attributeNotRequiredAndSelectedByScopeCanBeSet@email", "attributeNotRequiredAndSelectedByScopeCanBeSet", "password", "password", "Department AA"); - + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); - String userId = events.expectRegister("attributeNotRequiredAndSelectedByScopeCanBeSet", "attributeNotRequiredAndSelectedByScopeCanBeSet@email",client_scope_default.getClientId()).assertEvent().getUserId(); + String userId = events.expectRegister("attributeNotRequiredAndSelectedByScopeCanBeSet", "attributeNotRequiredAndSelectedByScopeCanBeSet@email",client_scope_default.getClientId()).assertEvent().getUserId(); UserRepresentation user = getUser(userId); assertEquals("FirstAA", user.getFirstName()); assertEquals("LastAA", user.getLastName()); @@ -379,19 +374,19 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { @Test public void testRegisterUserSuccess_attributeRequiredButNotSelectedByScopeIsNotRenderedAndNotBlockingRegistration() { - setUserProfileConfiguration("{\"attributes\": [" - + "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," + "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," - + "{\"name\": \"department\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{}, \"selector\":{\"scopes\":[\""+VerifyProfileTest.SCOPE_DEPARTMENT+"\"]}}" + + "{\"name\": \"department\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{}, \"selector\":{\"scopes\":[\""+VerifyProfileTest.SCOPE_DEPARTMENT+"\"]}}" + "]}"); oauth.clientId(client_scope_optional.getClientId()).openLoginForm(); loginPage.clickRegister(); registerPage.assertCurrent(); - + Assert.assertFalse(registerPage.isDepartmentPresent()); registerPage.register("FirstAA", "LastAA", "attributeRequiredButNotSelectedByScopeIsNotRendered@email", "attributeRequiredButNotSelectedByScopeIsNotRendered", "password", "password"); - + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); @@ -402,7 +397,7 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { assertEquals(null, user.firstAttribute(VerifyProfileTest.ATTRIBUTE_DEPARTMENT)); } - + private void assertUserRegistered(String userId, String username, String email, String firstName, String lastName) { events.expectLogin().detail("username", username.toLowerCase()).user(userId).assertEvent(); @@ -421,7 +416,7 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { protected UserRepresentation getUser(String userId) { return testRealm().users().get(userId).toRepresentation(); } - + protected UserRepresentation getUserByUsername(String username) { List users = testRealm().users().search(username); if(users!=null && !users.isEmpty()) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/UserProfileRegisterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/UserProfileRegisterTest.java new file mode 100644 index 0000000000..0ee75c5b36 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/UserProfileRegisterTest.java @@ -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 Pedro Igor + */ +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()); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/VerifyProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/VerifyProfileTest.java index d51c99c0a7..6aeb3a3ed2 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/VerifyProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/VerifyProfileTest.java @@ -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 */ -@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 diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/AbstractUserProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/AbstractUserProfileTest.java index 3fb184d63c..bcc71410cf 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/AbstractUserProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/AbstractUserProfileTest.java @@ -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()); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java index 0949727ccb..e44c4868de 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java @@ -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 Pedro Igor */ -@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()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json index c80de7b1f9..72957ed3c0 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json @@ -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" ] diff --git a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json index 6382015b05..7453b59a70 100755 --- a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json +++ b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json @@ -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" ] diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index b8c2b3690e..50125310a1 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -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. diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index f2b0dfea91..e7c193bea4 100644 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -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; } } }); diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html index 89eb413e5a..bfda569223 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-detail.html @@ -55,6 +55,14 @@ {{:: 'realm-detail.userManagedAccess.tooltip' | translate}} +

+ +
+ +
+ {{:: 'userProfileEnabled.tooltip' | translate}} +
+
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html index d9caf3ec6f..279125883f 100755 --- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html +++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-realm.html @@ -19,6 +19,6 @@ {{:: 'realm-tab-client-policies' | translate}}
  • {{:: 'realm-tab-security-defenses' | translate}}
  • -
  • {{:: 'realm-tab-user-profile' | translate}}
  • +
  • {{:: 'realm-tab-user-profile' | translate}}
  • \ No newline at end of file