diff --git a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java index abf053e796..52eb85700e 100755 --- a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java +++ b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java @@ -26,6 +26,6 @@ public enum LoginFormsPages { LOGIN_IDP_LINK_CONFIRM, LOGIN_IDP_LINK_EMAIL, OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, LOGIN_SELECT_AUTHENTICATOR, REGISTER, REGISTER_USER_PROFILE, INFO, ERROR, ERROR_WEBAUTHN, LOGIN_UPDATE_PROFILE, LOGIN_PAGE_EXPIRED, CODE, X509_CONFIRM, SAML_POST_FORM, - LOGIN_OAUTH2_DEVICE_VERIFY_USER_CODE, VERIFY_PROFILE; + LOGIN_OAUTH2_DEVICE_VERIFY_USER_CODE, UPDATE_USER_PROFILE; } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java index 80c190e065..1749bd7a95 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java @@ -32,6 +32,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.services.resources.IdentityBrokerService; import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.userprofile.UserProfileContext; import org.keycloak.util.JsonSerialization; import java.io.IOException; @@ -66,6 +67,12 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext { return !emailAsUsername; } + @JsonIgnore + @Override + public UserProfileContext getUserProfileContext() { + return UserProfileContext.IDP_REVIEW; + } + public String getId() { return id; } diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java index 9d8330e329..ee43b06cea 100644 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java @@ -27,6 +27,7 @@ import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.events.Details; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; +import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.UserModel; @@ -57,9 +58,7 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact @Override public void requiredActionChallenge(RequiredActionContext context) { - Response challenge = context.form() - .createResponse(UserModel.RequiredAction.UPDATE_PROFILE); - context.challenge(challenge); + context.challenge(createResponse(context, null, null)); } @Override @@ -94,14 +93,28 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact } catch (ValidationException pve) { List errors = Validation.getFormErrorsFromValidation(pve.getErrors()); - Response challenge = context.form() - .setErrors(errors) - .setFormData(formData) - .createResponse(UserModel.RequiredAction.UPDATE_PROFILE); - context.challenge(challenge); + context.challenge(createResponse(context, formData, errors)); } } + protected UserModel.RequiredAction getResponseAction(){ + return UserModel.RequiredAction.UPDATE_PROFILE; + } + + protected Response createResponse(RequiredActionContext context, MultivaluedMap formData, List errors) { + LoginFormsProvider form = context.form(); + + if (errors != null && !errors.isEmpty()) { + form.setErrors(errors); + } + + if(formData != null) { + form = form.setFormData(formData); + } + + return form.createResponse(getResponseAction()); + } + @Override public void close() { diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyUserProfile.java b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyUserProfile.java index ecc340f3a0..4f661744b9 100644 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyUserProfile.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyUserProfile.java @@ -17,24 +17,19 @@ package org.keycloak.authentication.requiredactions; +import java.util.List; +import java.util.stream.Collectors; + import javax.ws.rs.HttpMethod; import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; -import java.util.List; -import org.keycloak.Config; -import org.keycloak.OAuth2Constants; -import org.keycloak.authentication.DisplayTypeRequiredActionFactory; import org.keycloak.authentication.InitiatedActionSupport; import org.keycloak.authentication.RequiredActionContext; -import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; -import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.UserModel; import org.keycloak.models.utils.FormMessage; import org.keycloak.services.validation.Validation; @@ -46,11 +41,16 @@ import org.keycloak.userprofile.ValidationException; /** * @author Pedro Igor */ -public class VerifyUserProfile implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory { +public class VerifyUserProfile extends UpdateProfile { @Override public InitiatedActionSupport initiatedActionSupport() { - return InitiatedActionSupport.SUPPORTED; + return InitiatedActionSupport.NOT_SUPPORTED; + } + + @Override + protected UserModel.RequiredAction getResponseAction(){ + return UserModel.RequiredAction.VERIFY_PROFILE; } @Override @@ -72,7 +72,7 @@ public class VerifyUserProfile implements RequiredActionProvider, RequiredAction public void requiredActionChallenge(RequiredActionContext context) { UserProfileProvider provider = context.getSession().getProvider(UserProfileProvider.class); UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, context.getUser()); - + try { profile.validate(); context.success(); @@ -86,31 +86,17 @@ public class VerifyUserProfile implements RequiredActionProvider, RequiredAction parameters = context.getHttpRequest().getDecodedFormParameters(); } - context.challenge(createResponse(context, profile, parameters, errors)); + context.challenge(createResponse(context, parameters, errors)); + + EventBuilder event = context.getEvent().clone(); + event.event(EventType.VERIFY_PROFILE); + event.detail("fields_to_update", collectFields(errors)); + event.success(); } } - @Override - public void processAction(RequiredActionContext context) { - EventBuilder event = context.getEvent(); - event.event(EventType.VERIFY_PROFILE); - MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); - UserProfileProvider provider = context.getSession().getProvider(UserProfileProvider.class); - UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, formData, context.getUser()); - - try { - profile.update(); - context.success(); - } catch (ValidationException ve) { - List errors = Validation.getFormErrorsFromValidation(ve.getErrors()); - context.challenge(createResponse(context, profile, formData, errors)); - } - } - - - @Override - public void close() { - + private String collectFields(List errors) { + return errors.stream().map(FormMessage::getField).distinct().collect(Collectors.joining(",")); } @Override @@ -118,23 +104,6 @@ public class VerifyUserProfile implements RequiredActionProvider, RequiredAction return this; } - @Override - public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) { - if (displayType == null) return this; - if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null; - return ConsoleUpdateProfile.SINGLETON; - } - - @Override - public void init(Config.Scope config) { - - } - - @Override - public void postInit(KeycloakSessionFactory factory) { - - } - @Override public String getDisplayText() { return "Verify Profile"; @@ -146,15 +115,4 @@ public class VerifyUserProfile implements RequiredActionProvider, RequiredAction return UserModel.RequiredAction.VERIFY_PROFILE.name(); } - private Response createResponse(RequiredActionContext context, UserProfile profile, - MultivaluedMap formData, List errors) { - LoginFormsProvider form = context.form(); - - if (!errors.isEmpty()) { - form.setErrors(errors); - } - - return form.setFormData(formData) - .createResponse(UserModel.RequiredAction.VERIFY_PROFILE); - } } diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/util/UpdateProfileContext.java b/services/src/main/java/org/keycloak/authentication/requiredactions/util/UpdateProfileContext.java index 9b0d453c66..f6fb87a21d 100644 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/util/UpdateProfileContext.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/util/UpdateProfileContext.java @@ -22,6 +22,8 @@ import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.keycloak.userprofile.UserProfileContext; + /** * Abstraction, which allows to display updateProfile page in various contexts (Required action of already existing user, or first identity provider * login when user doesn't yet exists in Keycloak DB) @@ -29,6 +31,8 @@ import java.util.stream.Stream; * @author Marek Posolda */ public interface UpdateProfileContext { + + UserProfileContext getUserProfileContext(); boolean isEditUsernameAllowed(); diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/util/UserUpdateProfileContext.java b/services/src/main/java/org/keycloak/authentication/requiredactions/util/UserUpdateProfileContext.java index fc52597144..b2d24e35c0 100644 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/util/UserUpdateProfileContext.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/util/UserUpdateProfileContext.java @@ -19,10 +19,10 @@ package org.keycloak.authentication.requiredactions.util; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; +import org.keycloak.userprofile.UserProfileContext; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -37,11 +37,16 @@ public class UserUpdateProfileContext implements UpdateProfileContext { this.realm = realm; this.user = user; } - + @Override public boolean isEditUsernameAllowed() { return realm.isEditUsernameAllowed(); } + + @Override + public UserProfileContext getUserProfileContext() { + return UserProfileContext.UPDATE_PROFILE; + } @Override public String getUsername() { diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java index 88b39e01da..e75cfa8bdd 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java @@ -150,7 +150,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { this.attributes.put(UPDATE_PROFILE_CONTEXT_ATTR, userBasedContext); actionMessage = Messages.UPDATE_PROFILE; - page = LoginFormsPages.LOGIN_UPDATE_PROFILE; + if(isDynamicUserProfile()) { + page = LoginFormsPages.UPDATE_USER_PROFILE; + } else { + page = LoginFormsPages.LOGIN_UPDATE_PROFILE; + } break; case UPDATE_PASSWORD: boolean isRequestedByAdmin = user.getRequiredActionsStream().filter(Objects::nonNull).anyMatch(UPDATE_PASSWORD.toString()::contains); @@ -166,7 +170,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { this.attributes.put(UPDATE_PROFILE_CONTEXT_ATTR, verifyProfile); actionMessage = Messages.UPDATE_PROFILE; - page = LoginFormsPages.VERIFY_PROFILE; + page = LoginFormsPages.UPDATE_USER_PROFILE; break; default: return Response.serverError().build(); @@ -254,7 +258,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { case SAML_POST_FORM: attributes.put("samlPost", new SAMLPostFormBean(formData)); break; - case VERIFY_PROFILE: + case UPDATE_USER_PROFILE: attributes.put("profile", new VerifyProfileBean(user, formData, session)); break; } diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java b/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java index 08ce21462f..01c87542ff 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java @@ -74,8 +74,8 @@ public class Templates { return "login-x509-info.ftl"; case SAML_POST_FORM: return "saml-post-form.ftl"; - case VERIFY_PROFILE: - return "verify-profile.ftl"; + case UPDATE_USER_PROFILE: + return "update-user-profile.ftl"; default: throw new IllegalArgumentException(); } diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/AbstractUserProfileBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/AbstractUserProfileBean.java index 9a6a040ca9..3ebacf573a 100644 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/AbstractUserProfileBean.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/AbstractUserProfileBean.java @@ -55,9 +55,9 @@ public abstract class AbstractUserProfileBean { * Get attribute default value to be pre-filled into the form on first show. * * @param name of the attribute - * @return attribute default value (can be null or empty list) + * @return attribute default value (can be null) */ - protected abstract List getAttributeDefaultValue(String name); + protected abstract String getAttributeDefaultValue(String name); /** * Get context the template is used for, so view can be customized for distinct contexts. @@ -110,11 +110,12 @@ public abstract class AbstractUserProfileBean { } public String getValue() { - List v = formData!=null ? formData.getOrDefault(getName(), getAttributeDefaultValue(getName())): getAttributeDefaultValue(getName()); + List v = formData != null ? formData.get(getName()) : null; if (v == null || v.isEmpty()) { - return null; + return getAttributeDefaultValue(getName()); + } else { + return v.get(0); } - return v.get(0); } public boolean isRequired() { @@ -143,7 +144,7 @@ public abstract class AbstractUserProfileBean { return annotations; } - + /** * Get info about validators applied to attribute. * diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/RegisterBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/RegisterBean.java index 6554bd0132..4ed1855260 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/RegisterBean.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/RegisterBean.java @@ -17,7 +17,6 @@ package org.keycloak.forms.login.freemarker.model; import java.util.HashMap; -import java.util.List; import java.util.Map; import javax.ws.rs.core.MultivaluedMap; @@ -53,7 +52,7 @@ public class RegisterBean extends AbstractUserProfileBean { } @Override - protected List getAttributeDefaultValue(String name) { + protected String getAttributeDefaultValue(String name) { return null; } diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/VerifyProfileBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/VerifyProfileBean.java index 5e371238f7..022441d7d9 100644 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/VerifyProfileBean.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/VerifyProfileBean.java @@ -1,9 +1,5 @@ package org.keycloak.forms.login.freemarker.model; -import static java.util.Collections.singletonList; - -import java.util.List; - import javax.ws.rs.core.MultivaluedMap; import org.keycloak.models.KeycloakSession; @@ -31,8 +27,8 @@ public class VerifyProfileBean extends AbstractUserProfileBean { } @Override - protected List getAttributeDefaultValue(String name) { - return singletonList(user.getFirstAttribute(name)); + protected String getAttributeDefaultValue(String name) { + return user.getFirstAttribute(name); } @Override diff --git a/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java index 2b83d72c90..e0146cfcba 100644 --- a/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java +++ b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java @@ -138,6 +138,7 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider< decoratedMetadata.addAttribute(UserModel.LAST_NAME, 2, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_LAST_NAME))).setAttributeDisplayName("${lastName}"); return decoratedMetadata; } + return decoratedMetadata; } ComponentModel model = getComponentModelOrCreate(session); diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginUpdateProfileEditUsernameAllowedPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginUpdateProfileEditUsernameAllowedPage.java index c97ed46528..ed675668b2 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginUpdateProfileEditUsernameAllowedPage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginUpdateProfileEditUsernameAllowedPage.java @@ -16,6 +16,8 @@ */ package org.keycloak.testsuite.pages; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; @@ -30,6 +32,12 @@ public class LoginUpdateProfileEditUsernameAllowedPage extends LoginUpdateProfil update(firstName, lastName, email); } + public void updateWithDepartment(String firstName, String lastName, String department, String email, String username) { + usernameInput.clear(); + usernameInput.sendKeys(username); + super.updateWithDepartment(firstName, lastName, department, email); + } + public String getUsername() { return usernameInput.getAttribute("value"); } @@ -37,6 +45,14 @@ public class LoginUpdateProfileEditUsernameAllowedPage extends LoginUpdateProfil public boolean isCurrent() { return PageUtils.getPageTitle(driver).equals("Update Account Information"); } + + public boolean isUsernamePresent() { + try { + return driver.findElement(By.id("username")).isDisplayed(); + } catch (NoSuchElementException nse) { + return false; + } + } @Override public void open() { diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java index e935ff3582..37bf9134e2 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java @@ -19,6 +19,7 @@ package org.keycloak.testsuite.pages; import org.jboss.arquillian.graphene.page.Page; import org.keycloak.testsuite.util.UIUtils; +import org.openqa.selenium.By; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; @@ -42,6 +43,9 @@ public class LoginUpdateProfilePage extends AbstractPage { @FindBy(id = "email") private WebElement emailInput; + + @FindBy(id = "department") + private WebElement departmentInput; @FindBy(css = "input[type=\"submit\"]") private WebElement submitButton; @@ -53,6 +57,10 @@ public class LoginUpdateProfilePage extends AbstractPage { private WebElement loginAlertErrorMessage; public void update(String firstName, String lastName, String email) { + updateWithDepartment(firstName, lastName, null, email); + } + + public void updateWithDepartment(String firstName, String lastName, String department, String email) { if (firstName != null) { firstNameInput.clear(); firstNameInput.sendKeys(firstName); @@ -66,6 +74,11 @@ public class LoginUpdateProfilePage extends AbstractPage { emailInput.sendKeys(email); } + if(department != null) { + departmentInput.clear(); + departmentInput.sendKeys(department); + } + clickLink(submitButton); } @@ -92,6 +105,14 @@ public class LoginUpdateProfilePage extends AbstractPage { public String getEmail() { return emailInput.getAttribute("value"); } + + public String getDepartment() { + return departmentInput.getAttribute("value"); + } + + public boolean isDepartmentEnabled() { + return departmentInput.isEnabled(); + } public boolean isCurrent() { return PageUtils.getPageTitle(driver).equals("Update Account Information"); @@ -100,6 +121,19 @@ public class LoginUpdateProfilePage extends AbstractPage { public UpdateProfileErrors getInputErrors() { return errorsPage; } + + public String getLabelForField(String fieldId) { + return driver.findElement(By.cssSelector("label[for="+fieldId+"]")).getText(); + } + + public boolean isDepartmentPresent() { + try { + isDepartmentEnabled(); + return true; + } catch (NoSuchElementException e) { + return false; + } + } @Override public void open() { @@ -120,8 +154,14 @@ public class LoginUpdateProfilePage extends AbstractPage { @FindBy(id = "input-error-firstname") private WebElement inputErrorFirstName; + @FindBy(id = "input-error-firstName") + private WebElement inputErrorFirstNameDynamic; + @FindBy(id = "input-error-lastname") private WebElement inputErrorLastName; + + @FindBy(id = "input-error-lastName") + private WebElement inputErrorLastNameDynamic; @FindBy(id = "input-error-email") private WebElement inputErrorEmail; @@ -133,7 +173,11 @@ public class LoginUpdateProfilePage extends AbstractPage { try { return getTextFromElement(inputErrorFirstName); } catch (NoSuchElementException e) { - return null; + try { + return getTextFromElement(inputErrorFirstNameDynamic); + } catch (NoSuchElementException ex) { + return null; + } } } @@ -141,7 +185,11 @@ public class LoginUpdateProfilePage extends AbstractPage { try { return getTextFromElement(inputErrorLastName); } catch (NoSuchElementException e) { - return null; + try { + return getTextFromElement(inputErrorLastNameDynamic); + } catch (NoSuchElementException ex) { + return null; + } } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionUpdateProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionUpdateProfileTest.java index c8fb1865c6..ebc9754158 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionUpdateProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionUpdateProfileTest.java @@ -47,6 +47,10 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct @Page protected ErrorPage errorPage; + protected boolean isDynamicForm() { + return false; + } + @Override public void configureTestRealm(RealmRepresentation testRealm) { } @@ -203,7 +207,10 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct Assert.assertEquals("New last", updateProfilePage.getLastName()); Assert.assertEquals("new@email.com", updateProfilePage.getEmail()); - Assert.assertEquals("Please specify first name.", updateProfilePage.getInputErrors().getFirstNameError()); + if(isDynamicForm()) + Assert.assertEquals("Please specify this field.", updateProfilePage.getInputErrors().getFirstNameError()); + else + Assert.assertEquals("Please specify first name.", updateProfilePage.getInputErrors().getFirstNameError()); events.assertEmpty(); } @@ -225,7 +232,10 @@ public class AppInitiatedActionUpdateProfileTest extends AbstractAppInitiatedAct Assert.assertEquals("", updateProfilePage.getLastName()); Assert.assertEquals("new@email.com", updateProfilePage.getEmail()); - Assert.assertEquals("Please specify last name.", updateProfilePage.getInputErrors().getLastNameError()); + if(isDynamicForm()) + Assert.assertEquals("Please specify this field.", updateProfilePage.getInputErrors().getLastNameError()); + else + Assert.assertEquals("Please specify last name.", updateProfilePage.getInputErrors().getLastNameError()); events.assertEmpty(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionUpdateProfileWithUserProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionUpdateProfileWithUserProfileTest.java new file mode 100644 index 0000000000..aabbe67f77 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionUpdateProfileWithUserProfileTest.java @@ -0,0 +1,48 @@ +/* + * 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.testsuite.actions; + +import org.junit.Before; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.forms.VerifyProfileTest; + +/** + * Only covers basic use cases for App Initialized actions. Complete dynamic user profile behavior is tested in {@link RequiredActionUpdateProfileWithUserProfileTest} as it shares same code as the App initialized action. + * + * @author Vlastimil Elias + * + */ +public class AppInitiatedActionUpdateProfileWithUserProfileTest extends AppInitiatedActionUpdateProfileTest { + + @Override + protected boolean isDynamicForm() { + return true; + } + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + super.configureTestRealm(testRealm); + VerifyProfileTest.enableDynamicUserProfile(testRealm); + } + + @Before + public void beforeTest() { + VerifyProfileTest.setUserProfileConfiguration(testRealm(),null); + super.beforeTest(); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java index f763dab418..092a16a667 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java @@ -63,6 +63,10 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe @Page protected ErrorPage errorPage; + + protected boolean isDynamicForm() { + return false; + } @Override public void configureTestRealm(RealmRepresentation testRealm) { @@ -166,7 +170,10 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe Assert.assertEquals("New last", updateProfilePage.getLastName()); Assert.assertEquals("new@email.com", updateProfilePage.getEmail()); - Assert.assertEquals("Please specify first name.", updateProfilePage.getInputErrors().getFirstNameError()); + if(isDynamicForm()) + Assert.assertEquals("Please specify this field.", updateProfilePage.getInputErrors().getFirstNameError()); + else + Assert.assertEquals("Please specify first name.", updateProfilePage.getInputErrors().getFirstNameError()); events.assertEmpty(); } @@ -188,7 +195,10 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe Assert.assertEquals("", updateProfilePage.getLastName()); Assert.assertEquals("new@email.com", updateProfilePage.getEmail()); - Assert.assertEquals("Please specify last name.", updateProfilePage.getInputErrors().getLastNameError()); + if(isDynamicForm()) + Assert.assertEquals("Please specify this field.", updateProfilePage.getInputErrors().getLastNameError()); + else + Assert.assertEquals("Please specify last name.", updateProfilePage.getInputErrors().getLastNameError()); events.assertEmpty(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileWithUserProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileWithUserProfileTest.java new file mode 100644 index 0000000000..70f452869b --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileWithUserProfileTest.java @@ -0,0 +1,502 @@ +/* + * 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.testsuite.actions; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import static org.keycloak.testsuite.forms.VerifyProfileTest.PERMISSIONS_ALL; +import static org.keycloak.testsuite.forms.VerifyProfileTest.PERMISSIONS_ADMIN_EDITABLE; +import static org.keycloak.testsuite.forms.VerifyProfileTest.PERMISSIONS_ADMIN_ONLY; +import static org.keycloak.testsuite.forms.VerifyProfileTest.SCOPE_DEPARTMENT; +import static org.keycloak.testsuite.forms.VerifyProfileTest.VALIDATIONS_LENGTH; +import static org.keycloak.testsuite.forms.VerifyProfileTest.ATTRIBUTE_DEPARTMENT; +import static org.keycloak.testsuite.forms.VerifyProfileTest.CONFIGURATION_FOR_USER_EDIT; + +import java.util.ArrayList; +import java.util.Collections; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.events.Details; +import org.keycloak.events.EventType; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.forms.VerifyProfileTest; +import org.keycloak.testsuite.pages.AppPage.RequestType; +import org.keycloak.testsuite.util.ClientScopeBuilder; +import org.keycloak.testsuite.util.KeycloakModelUtils; +import org.openqa.selenium.By; + +/** + * + * @author Vlastimil Elias + * + */ +public class RequiredActionUpdateProfileWithUserProfileTest extends RequiredActionUpdateProfileTest { + + protected static final String PASSWORD = "password"; + protected static final String USERNAME1 = "test-user@localhost"; + + private static ClientRepresentation client_scope_default; + private static ClientRepresentation client_scope_optional; + + @Override + protected boolean isDynamicForm() { + return true; + } + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + super.configureTestRealm(testRealm); + + VerifyProfileTest.enableDynamicUserProfile(testRealm); + + testRealm.setClientScopes(new ArrayList<>()); + testRealm.getClientScopes().add(ClientScopeBuilder.create().name(SCOPE_DEPARTMENT).protocol("openid-connect").build()); + testRealm.getClientScopes().add(ClientScopeBuilder.create().name("profile").protocol("openid-connect").build()); + + client_scope_default = KeycloakModelUtils.createClient(testRealm, "client-a"); + client_scope_default.setDefaultClientScopes(Collections.singletonList(SCOPE_DEPARTMENT)); + client_scope_default.setRedirectUris(Collections.singletonList("*")); + client_scope_optional = KeycloakModelUtils.createClient(testRealm, "client-b"); + client_scope_optional.setOptionalClientScopes(Collections.singletonList(SCOPE_DEPARTMENT)); + client_scope_optional.setRedirectUris(Collections.singletonList("*")); + + } + + @Before + public void beforeTest() { + VerifyProfileTest.setUserProfileConfiguration(testRealm(),null); + super.beforeTest(); + } + + @Test + public void testDisplayName() { + + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\",\"displayName\":\"${firstName}\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "}," + + "{\"name\": \"department\", \"displayName\" : \"Department\", " + PERMISSIONS_ALL + ", \"required\":{}}" + + "]}"); + + loginPage.open(); + loginPage.login(USERNAME1, PASSWORD); + + updateProfilePage.assertCurrent(); + + //assert field names + // i18n replaced + Assert.assertEquals("First name",updateProfilePage.getLabelForField("firstName")); + // attribute name used if no display name set + Assert.assertEquals("lastName",updateProfilePage.getLabelForField("lastName")); + // direct value in display name + Assert.assertEquals("Department",updateProfilePage.getLabelForField("department")); + + } + + @Test + public void testAttributeGuiOrder() { + + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + "}," + + "{\"name\": \"department\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{}}," + + "{\"name\": \"username\", " + VerifyProfileTest.PERMISSIONS_ALL + "}," + + "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"email\", " + VerifyProfileTest.PERMISSIONS_ALL + "}" + + "]}"); + + loginPage.open(); + loginPage.login(USERNAME1, PASSWORD); + + updateProfilePage.assertCurrent(); + + //assert fields location in form + Assert.assertTrue( + driver.findElement( + By.cssSelector("form#kc-update-profile-form > div:nth-child(1) > div:nth-child(2) > input#lastName") + ).isDisplayed() + ); + Assert.assertTrue( + driver.findElement( + By.cssSelector("form#kc-update-profile-form > div:nth-child(2) > div:nth-child(2) > input#department") + ).isDisplayed() + ); + Assert.assertTrue( + driver.findElement( + By.cssSelector("form#kc-update-profile-form > div:nth-child(3) > div:nth-child(2) > input#username") + ).isDisplayed() + ); + Assert.assertTrue( + driver.findElement( + By.cssSelector("form#kc-update-profile-form > div:nth-child(4) > div:nth-child(2) > input#firstName") + ).isDisplayed() + ); + Assert.assertTrue( + driver.findElement( + By.cssSelector("form#kc-update-profile-form > div:nth-child(5) > div:nth-child(2) > input#email") + ).isDisplayed() + ); + } + + @Test + public void testUsernameOnlyIfEditAllowed() { + RealmRepresentation realm = testRealm().toRepresentation(); + + boolean r = realm.isEditUsernameAllowed(); + try { + realm.setEditUsernameAllowed(false); + testRealm().update(realm); + + loginPage.open(); + loginPage.login(USERNAME1, PASSWORD); + + assertFalse(updateProfilePage.isUsernamePresent()); + + realm.setEditUsernameAllowed(true); + testRealm().update(realm); + + driver.navigate().refresh(); + assertTrue(updateProfilePage.isUsernamePresent()); + } finally { + realm.setEditUsernameAllowed(r); + testRealm().update(realm); + } + } + + @Test + public void testOptionalAttribute() { + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "}" + + "]}"); + + loginPage.open(); + + loginPage.login(USERNAME1, PASSWORD); + + updateProfilePage.assertCurrent(); + assertFalse(updateProfilePage.isCancelDisplayed()); + + updateProfilePage.update("New first", "", "new@email.com", USERNAME1); + + events.expectRequiredAction(EventType.UPDATE_PROFILE).detail(Details.PREVIOUS_FIRST_NAME, "Tom").detail(Details.UPDATED_FIRST_NAME, "New first") + .detail(Details.PREVIOUS_LAST_NAME, "Brady") + .detail(Details.PREVIOUS_EMAIL, USERNAME1).detail(Details.UPDATED_EMAIL, "new@email.com") + .assertEvent(); + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + events.expectLogin().assertEvent(); + + // assert user is really updated in persistent store + UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, USERNAME1); + Assert.assertEquals("New first", user.getFirstName()); + Assert.assertEquals("", user.getLastName()); + Assert.assertEquals("new@email.com", user.getEmail()); + Assert.assertEquals(USERNAME1, user.getUsername()); + } + + @Test + public void testCustomValidationLastName() { + + setUserProfileConfiguration(CONFIGURATION_FOR_USER_EDIT); + updateUserByUsername(USERNAME1, "ExistingFirst", "La", "Department"); + + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL +","+VALIDATIONS_LENGTH + "}," + + "{\"name\": \"department\"," + PERMISSIONS_ADMIN_ONLY + "}" + + "]}"); + + loginPage.open(); + loginPage.login(USERNAME1, PASSWORD); + + updateProfilePage.assertCurrent(); + //submit with error + updateProfilePage.update("First", "L", USERNAME1, USERNAME1); + + updateProfilePage.assertCurrent(); + //submit OK + updateProfilePage.update("First", "Last", USERNAME1, USERNAME1); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); + + UserRepresentation user = getUserByUsername(USERNAME1); + assertEquals("First", user.getFirstName()); + assertEquals("Last", user.getLastName()); + //check that not configured attribute is unchanged + assertEquals("Department", user.firstAttribute(ATTRIBUTE_DEPARTMENT)); + } + + @Test + public void testRequiredReadOnlyAttribute() { + + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "}," + + "{\"name\": \"department\"," + PERMISSIONS_ADMIN_EDITABLE + ", \"required\":{}}" + + "]}"); + + loginPage.open(); + loginPage.login(USERNAME1, PASSWORD); + + updateProfilePage.assertCurrent(); + Assert.assertEquals("Brady", updateProfilePage.getLastName()); + Assert.assertFalse(updateProfilePage.isDepartmentEnabled()); + + //update of the other attributes must be successful in this case + updateProfilePage.update("First", "Last", USERNAME1, USERNAME1); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); + + UserRepresentation user = getUserByUsername(USERNAME1); + assertEquals("First", user.getFirstName()); + assertEquals("Last", user.getLastName()); + } + + @Test + public void testAttributeNotVisible() { + + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "}," + + "{\"name\": \"department\"," + PERMISSIONS_ADMIN_ONLY + ", \"required\":{}}" + + "]}"); + + loginPage.open(); + loginPage.login(USERNAME1, PASSWORD); + + updateProfilePage.assertCurrent(); + Assert.assertEquals("Brady", updateProfilePage.getLastName()); + Assert.assertFalse("'department' field is visible" , updateProfilePage.isDepartmentPresent()); + + //update of the other attributes must be successful in this case + updateProfilePage.update("First", "Last", USERNAME1, USERNAME1); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); + + UserRepresentation user = getUserByUsername(USERNAME1); + assertEquals("First", user.getFirstName()); + assertEquals("Last", user.getLastName()); + } + + @Test + public void testRequiredAttribute() { + + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "}," + + "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"required\":{}}" + + "]}"); + + loginPage.open(); + loginPage.login(USERNAME1, PASSWORD); + + updateProfilePage.assertCurrent(); + + //submit with error + updateProfilePage.updateWithDepartment("FirstCC", "LastCC", "", USERNAME1, USERNAME1); + updateProfilePage.assertCurrent(); + + //submit OK + updateProfilePage.updateWithDepartment("FirstCC", "LastCC", "DepartmentCC", USERNAME1, USERNAME1); + + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); + + UserRepresentation user = getUserByUsername(USERNAME1); + assertEquals("FirstCC", user.getFirstName()); + assertEquals("LastCC", user.getLastName()); + assertEquals("DepartmentCC", user.firstAttribute(ATTRIBUTE_DEPARTMENT)); + } + + @Test + public void testAttributeRequiredForScope() { + + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "}," + + "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\""+SCOPE_DEPARTMENT+"\"]}}" + + "]}"); + + oauth.scope(SCOPE_DEPARTMENT).clientId(client_scope_optional.getClientId()).openLoginForm(); + + loginPage.assertCurrent(); + loginPage.login(USERNAME1, PASSWORD); + + updateProfilePage.assertCurrent(); + + //submit with error + updateProfilePage.updateWithDepartment("FirstCC", "LastCC", "", USERNAME1, USERNAME1); + updateProfilePage.assertCurrent(); + + //submit OK + updateProfilePage.updateWithDepartment("FirstCC", "LastCC", "DepartmentCC", USERNAME1, USERNAME1); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); + + UserRepresentation user = getUserByUsername(USERNAME1); + assertEquals("FirstCC", user.getFirstName()); + assertEquals("LastCC", user.getLastName()); + assertEquals("DepartmentCC", user.firstAttribute(ATTRIBUTE_DEPARTMENT)); + } + + @Test + public void testAttributeRequiredForDefaultScope() { + + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "}," + + "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\""+SCOPE_DEPARTMENT+"\"]}}" + + "]}"); + + oauth.clientId(client_scope_default.getClientId()).openLoginForm(); + + loginPage.assertCurrent(); + loginPage.login(USERNAME1, PASSWORD); + + updateProfilePage.assertCurrent(); + + //submit with error + updateProfilePage.updateWithDepartment("FirstCC", "LastCC", "", USERNAME1, USERNAME1); + updateProfilePage.assertCurrent(); + + //submit OK + updateProfilePage.updateWithDepartment("FirstCC", "LastCC", "DepartmentCC", USERNAME1, USERNAME1); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); + + UserRepresentation user = getUserByUsername(USERNAME1); + assertEquals("FirstCC", user.getFirstName()); + assertEquals("LastCC", user.getLastName()); + assertEquals("DepartmentCC", user.firstAttribute(ATTRIBUTE_DEPARTMENT)); + } + + @Test + public void testAttributeRequiredAndSelectedByScope() { + + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "}," + + "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"required\":{}, \"selector\":{\"scopes\":[\""+SCOPE_DEPARTMENT+"\"]}}" + + "]}"); + + oauth.scope(SCOPE_DEPARTMENT).clientId(client_scope_optional.getClientId()).openLoginForm(); + + loginPage.assertCurrent(); + loginPage.login(USERNAME1, PASSWORD); + + updateProfilePage.assertCurrent(); + + //submit with error + updateProfilePage.updateWithDepartment("FirstCC", "LastCC", "", USERNAME1, USERNAME1); + updateProfilePage.assertCurrent(); + + //submit OK + updateProfilePage.updateWithDepartment("FirstCC", "LastCC", "DepartmentCC", USERNAME1, USERNAME1); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); + + UserRepresentation user = getUserByUsername(USERNAME1); + assertEquals("FirstCC", user.getFirstName()); + assertEquals("LastCC", user.getLastName()); + assertEquals("DepartmentCC", user.firstAttribute(ATTRIBUTE_DEPARTMENT)); + } + + @Test + public void testAttributeNotRequiredAndSelectedByScopeCanBeUpdated() { + + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"selector\":{\"scopes\":[\""+SCOPE_DEPARTMENT+"\"]}}" + + "]}"); + + oauth.scope(SCOPE_DEPARTMENT).clientId(client_scope_optional.getClientId()).openLoginForm(); + + loginPage.assertCurrent(); + loginPage.login(USERNAME1, PASSWORD); + + updateProfilePage.assertCurrent(); + + Assert.assertTrue(updateProfilePage.isDepartmentPresent()); + updateProfilePage.updateWithDepartment("FirstCC", "LastCC", "DepartmentCC", USERNAME1, USERNAME1); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); + + UserRepresentation user = getUserByUsername(USERNAME1); + assertEquals("FirstCC", user.getFirstName()); + assertEquals("LastCC", user.getLastName()); + assertEquals("DepartmentCC", user.firstAttribute(ATTRIBUTE_DEPARTMENT)); + } + + @Test + public void testAttributeRequiredButNotSelectedByScopeIsNotRendered() { + + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"required\":{}, \"selector\":{\"scopes\":[\""+SCOPE_DEPARTMENT+"\"]}}" + + "]}"); + + oauth.clientId(client_scope_optional.getClientId()).openLoginForm(); + + loginPage.assertCurrent(); + loginPage.login(USERNAME1, PASSWORD); + + updateProfilePage.assertCurrent(); + + Assert.assertFalse(updateProfilePage.isDepartmentPresent()); + updateProfilePage.update("FirstCC", "LastCC", USERNAME1, USERNAME1); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); + + UserRepresentation user = getUserByUsername(USERNAME1); + assertEquals("FirstCC", user.getFirstName()); + assertEquals("LastCC", user.getLastName()); + } + + protected void setUserProfileConfiguration(String configuration) { + VerifyProfileTest.setUserProfileConfiguration(testRealm(), configuration); + } + + protected UserRepresentation getUserByUsername(String username) { + return VerifyProfileTest.getUserByUsername(testRealm(), username); + } + + protected void updateUserByUsername(String username, String firstName, String lastName, String department) { + UserRepresentation ur = getUserByUsername(username); + ur.setFirstName(firstName); + ur.setLastName(lastName); + ur.singleAttribute(ATTRIBUTE_DEPARTMENT, department); + testRealm().users().get(ur.getId()).update(ur); + } + +} 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 891529db45..979733758c 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,104 +17,76 @@ package org.keycloak.testsuite.forms; import static org.junit.Assert.assertEquals; -import static org.keycloak.userprofile.DeclarativeUserProfileProvider.REALM_USER_PROFILE_ENABLED; + +import static org.keycloak.testsuite.forms.VerifyProfileTest.PERMISSIONS_ALL; +import static org.keycloak.testsuite.forms.VerifyProfileTest.PERMISSIONS_ADMIN_EDITABLE; +import static org.keycloak.testsuite.forms.VerifyProfileTest.SCOPE_DEPARTMENT; +import static org.keycloak.testsuite.forms.VerifyProfileTest.VALIDATIONS_LENGTH; +import static org.keycloak.testsuite.forms.VerifyProfileTest.ATTRIBUTE_DEPARTMENT; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import javax.ws.rs.core.Response; - -import org.jboss.arquillian.graphene.page.Page; import org.junit.Assert; -import org.junit.Rule; +import org.junit.Before; import org.junit.Test; import org.keycloak.OAuth2Constants; 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.pages.AccountUpdateProfilePage; -import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; -import org.keycloak.testsuite.pages.LoginPage; -import org.keycloak.testsuite.pages.RegisterPage; -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.openqa.selenium.By; /** - * @author Stian Thorgersen - * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. * @author Vlastimil Elias */ -@AuthServerContainerExclude(AuthServerContainerExclude.AuthServer.REMOTE) -public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { - - @Rule - public AssertEvents events = new AssertEvents(this); - - @Page - protected AppPage appPage; - - @Page - protected LoginPage loginPage; - - @Page - protected RegisterPage registerPage; - - @Page - protected VerifyEmailPage verifyEmailPage; - - @Page - protected AccountUpdateProfilePage accountPage; - - @Rule - public GreenMailRule greenMail = new GreenMailRule(); - +public class RegisterWithUserProfileTest extends RegisterTest { private static final String SCOPE_LAST_NAME = "lastName"; private static ClientRepresentation client_scope_default; private static ClientRepresentation client_scope_optional; - public static String UP_CONFIG_BASIC_ATTRIBUTES = "{\"name\": \"username\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," - + "{\"name\": \"email\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},"; + public static String UP_CONFIG_BASIC_ATTRIBUTES = "{\"name\": \"username\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"email\"," + PERMISSIONS_ALL + ", \"required\": {}},"; @Override public void configureTestRealm(RealmRepresentation testRealm) { + super.configureTestRealm(testRealm); + + VerifyProfileTest.enableDynamicUserProfile(testRealm); + testRealm.setClientScopes(new ArrayList<>()); testRealm.getClientScopes().add(ClientScopeBuilder.create().name(SCOPE_LAST_NAME).protocol("openid-connect").build()); - testRealm.getClientScopes().add(ClientScopeBuilder.create().name(VerifyProfileTest.SCOPE_DEPARTMENT).protocol("openid-connect").build()); + testRealm.getClientScopes().add(ClientScopeBuilder.create().name(SCOPE_DEPARTMENT).protocol("openid-connect").build()); List scopes = new ArrayList<>(); scopes.add(SCOPE_LAST_NAME); - scopes.add(VerifyProfileTest.SCOPE_DEPARTMENT); - + scopes.add(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()); + client_scope_optional.setRedirectUris(Collections.singletonList("*")); + } + + @Before + public void beforeTest() { + VerifyProfileTest.setUserProfileConfiguration(testRealm(),null); } @Test public void testRegisterUserSuccess_lastNameOptional() { setUserProfileConfiguration("{\"attributes\": [" + UP_CONFIG_BASIC_ATTRIBUTES - + "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," - + "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + "}" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "}" + "]}"); loginPage.open(); @@ -134,8 +106,8 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { public void testRegisterUserSuccess_lastNameRequiredForScope_notRequested() { setUserProfileConfiguration("{\"attributes\": [" + UP_CONFIG_BASIC_ATTRIBUTES - + "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," - + "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\""+SCOPE_LAST_NAME+"\"]}}" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\""+SCOPE_LAST_NAME+"\"]}}" + "]}"); loginPage.open(); @@ -155,8 +127,8 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { public void testRegisterUserSuccess_lastNameRequiredForScope_requested() { setUserProfileConfiguration("{\"attributes\": [" + UP_CONFIG_BASIC_ATTRIBUTES - + "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," - + "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\""+SCOPE_LAST_NAME+"\"]}}" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\""+SCOPE_LAST_NAME+"\"]}}" + "]}"); oauth.scope(SCOPE_LAST_NAME).clientId(client_scope_optional.getClientId()).openLoginForm(); @@ -181,8 +153,8 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { public void testRegisterUserSuccess_lastNameRequiredForScope_clientDefault() { setUserProfileConfiguration("{\"attributes\": [" + UP_CONFIG_BASIC_ATTRIBUTES - + "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," - + "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\""+SCOPE_LAST_NAME+"\"]}}" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\""+SCOPE_LAST_NAME+"\"]}}" + "]}"); oauth.clientId(client_scope_default.getClientId()).openLoginForm(); @@ -207,8 +179,8 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { public void testRegisterUserSuccess_lastNameLengthValidation() { setUserProfileConfiguration("{\"attributes\": [" + UP_CONFIG_BASIC_ATTRIBUTES - + "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," - + "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", " + VerifyProfileTest.VALIDATIONS_LENGTH + "}" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + ", " + VALIDATIONS_LENGTH + "}" + "]}"); loginPage.open(); @@ -228,8 +200,8 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { public void testRegisterUserInvalidLastNameLength() { setUserProfileConfiguration("{\"attributes\": [" + UP_CONFIG_BASIC_ATTRIBUTES - + "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," - + "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", " + VerifyProfileTest.VALIDATIONS_LENGTH + "}" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + ", " + VALIDATIONS_LENGTH + "}" + "]}"); loginPage.open(); @@ -248,10 +220,10 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { @Test public void testAttributeDisplayName() { - setUserProfileConfiguration("{\"attributes\": [" - + "{\"name\": \"firstName\",\"displayName\":\"${firstName}\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," - + "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + "}," - + "{\"name\": \"department\", \"displayName\" : \"Department\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{}}" + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\",\"displayName\":\"${firstName}\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "}," + + "{\"name\": \"department\", \"displayName\" : \"Department\", " + PERMISSIONS_ALL + ", \"required\":{}}" + "]}"); loginPage.open(); @@ -325,10 +297,10 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { @Test public void testRegisterUserSuccess_requiredReadOnlyAttributeNotRenderedAndNotBlockingRegistration() { - setUserProfileConfiguration("{\"attributes\": [" - + "{\"name\": \"firstName\",\"displayName\":\"${firstName}\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," - + "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + "}," - + "{\"name\": \"department\", \"displayName\" : \"Department\", " + VerifyProfileTest.PERMISSIONS_ADMIN_EDITABLE + ", \"required\":{}}" + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\",\"displayName\":\"${firstName}\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "}," + + "{\"name\": \"department\", \"displayName\" : \"Department\", " + PERMISSIONS_ADMIN_EDITABLE + ", \"required\":{}}" + "]}"); loginPage.open(); @@ -349,13 +321,13 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { @Test public void testRegisterUserSuccess_attributeRequiredAndSelectedByScopeMustBeSet() { - setUserProfileConfiguration("{\"attributes\": [" - + "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," - + "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + "}," - + "{\"name\": \"department\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{}, \"selector\":{\"scopes\":[\""+VerifyProfileTest.SCOPE_DEPARTMENT+"\"]}}" + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "}," + + "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"required\":{}, \"selector\":{\"scopes\":[\""+SCOPE_DEPARTMENT+"\"]}}" + "]}"); - oauth.scope(VerifyProfileTest.SCOPE_DEPARTMENT).clientId(client_scope_optional.getClientId()).openLoginForm(); + oauth.scope(SCOPE_DEPARTMENT).clientId(client_scope_optional.getClientId()).openLoginForm(); loginPage.clickRegister(); registerPage.assertCurrent(); @@ -368,22 +340,22 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); - UserRepresentation user = getUserByUsername("attributeRequiredAndSelectedByScopeMustBeSet"); + UserRepresentation user = VerifyProfileTest.getUserByUsername(testRealm(),"attributeRequiredAndSelectedByScopeMustBeSet"); assertEquals("FirstAA", user.getFirstName()); assertEquals("LastAA", user.getLastName()); - assertEquals("DepartmentAA", user.firstAttribute(VerifyProfileTest.ATTRIBUTE_DEPARTMENT)); + assertEquals("DepartmentAA", user.firstAttribute(ATTRIBUTE_DEPARTMENT)); } @Test public void testRegisterUserSuccess_attributeNotRequiredAndSelectedByScopeCanBeIgnored() { - setUserProfileConfiguration("{\"attributes\": [" - + "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," - + "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," - + "{\"name\": \"department\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"selector\":{\"scopes\":[\""+VerifyProfileTest.SCOPE_DEPARTMENT+"\"]}}" + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"selector\":{\"scopes\":[\""+SCOPE_DEPARTMENT+"\"]}}" + "]}"); - oauth.scope(VerifyProfileTest.SCOPE_DEPARTMENT).clientId(client_scope_optional.getClientId()).openLoginForm(); + oauth.scope(SCOPE_DEPARTMENT).clientId(client_scope_optional.getClientId()).openLoginForm(); loginPage.clickRegister(); registerPage.assertCurrent(); @@ -397,16 +369,16 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { UserRepresentation user = getUser(userId); assertEquals("FirstAA", user.getFirstName()); assertEquals("LastAA", user.getLastName()); - assertEquals("", user.firstAttribute(VerifyProfileTest.ATTRIBUTE_DEPARTMENT)); + assertEquals("", user.firstAttribute(ATTRIBUTE_DEPARTMENT)); } @Test public void testRegisterUserSuccess_attributeNotRequiredAndSelectedByScopeCanBeSet() { - setUserProfileConfiguration("{\"attributes\": [" - + "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," - + "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," - + "{\"name\": \"department\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"selector\":{\"scopes\":[\""+VerifyProfileTest.SCOPE_DEPARTMENT+"\"]}}" + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"selector\":{\"scopes\":[\""+SCOPE_DEPARTMENT+"\"]}}" + "]}"); oauth.clientId(client_scope_default.getClientId()).openLoginForm(); @@ -423,16 +395,16 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { UserRepresentation user = getUser(userId); assertEquals("FirstAA", user.getFirstName()); assertEquals("LastAA", user.getLastName()); - assertEquals("Department AA", user.firstAttribute(VerifyProfileTest.ATTRIBUTE_DEPARTMENT)); + assertEquals("Department AA", user.firstAttribute(ATTRIBUTE_DEPARTMENT)); } @Test public void testRegisterUserSuccess_attributeRequiredButNotSelectedByScopeIsNotRenderedAndNotBlockingRegistration() { - 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+"\"]}}" + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"required\":{}, \"selector\":{\"scopes\":[\""+SCOPE_DEPARTMENT+"\"]}}" + "]}"); oauth.clientId(client_scope_optional.getClientId()).openLoginForm(); @@ -449,7 +421,7 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { UserRepresentation user = getUser(userId); assertEquals("FirstAA", user.getFirstName()); assertEquals("LastAA", user.getLastName()); - assertEquals(null, user.firstAttribute(VerifyProfileTest.ATTRIBUTE_DEPARTMENT)); + assertEquals(null, user.firstAttribute(ATTRIBUTE_DEPARTMENT)); } @@ -468,21 +440,7 @@ public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest { assertEquals(lastName, user.getLastName()); } - 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()) - return users.get(0); - return null; - } - - private void setUserProfileConfiguration(String configuration) { - Response r = testRealm().users().userProfile().update(configuration); - if (r.getStatus() != 200) { - Assert.fail("Configuration not set due to error: " + r.readEntity(String.class)); - } + protected void setUserProfileConfiguration(String configuration) { + VerifyProfileTest.setUserProfileConfiguration(testRealm(), configuration); } } 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 deleted file mode 100644 index 0ee75c5b36..0000000000 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/UserProfileRegisterTest.java +++ /dev/null @@ -1,24 +0,0 @@ -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 1f892586b6..16da10971b 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 @@ -36,6 +36,7 @@ import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.events.Details; import org.keycloak.events.EventType; import org.keycloak.models.UserModel; import org.keycloak.representations.idm.ClientRepresentation; @@ -44,7 +45,6 @@ import org.keycloak.representations.idm.RequiredActionProviderRepresentation; 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.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.LoginPage; @@ -54,11 +54,11 @@ 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.openqa.selenium.By; /** * @author Vlastimil Elias */ -@AuthServerContainerExclude(AuthServerContainerExclude.AuthServer.REMOTE) public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { public static final String SCOPE_DEPARTMENT = "department"; @@ -70,7 +70,7 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { public static String VALIDATIONS_LENGTH = "\"validations\": {\"length\": { \"min\": 3, \"max\": 255 }}"; - private static final String CONFIGURATION_FOR_USER_EDIT = "{\"attributes\": [" + public static final String CONFIGURATION_FOR_USER_EDIT = "{\"attributes\": [" + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + "}," + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "}," + "{\"name\": \"department\"," + PERMISSIONS_ALL + "}" @@ -94,6 +94,9 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { @Override public void configureTestRealm(RealmRepresentation testRealm) { + + enableDynamicUserProfile(testRealm); + UserRepresentation user = UserBuilder.create().id(UUID.randomUUID().toString()).username("login-test").email("login@test.com").enabled(true).password("password").build(); userId = user.getId(); @@ -134,10 +137,6 @@ 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 @@ -179,9 +178,82 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { Assert.assertEquals("lastName",verifyProfilePage.getLabelForField("lastName")); // direct value in display name Assert.assertEquals("Department",verifyProfilePage.getLabelForField("department")); - } + + @Test + public void testAttributeGuiOrder() { + setUserProfileConfiguration(CONFIGURATION_FOR_USER_EDIT); + updateUser(user5Id, "ExistingFirst", "ExistingLast", null); + + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + "}," + + "{\"name\": \"department\", " + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{}}," + + "{\"name\": \"username\", " + VerifyProfileTest.PERMISSIONS_ALL + "}," + + "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"email\", " + VerifyProfileTest.PERMISSIONS_ALL + "}" + + "]}"); + + loginPage.open(); + loginPage.login("login-test5", "password"); + + verifyProfilePage.assertCurrent(); + + //assert fields location in form + Assert.assertTrue( + driver.findElement( + By.cssSelector("form#kc-update-profile-form > div:nth-child(1) > div:nth-child(2) > input#lastName") + ).isDisplayed() + ); + Assert.assertTrue( + driver.findElement( + By.cssSelector("form#kc-update-profile-form > div:nth-child(2) > div:nth-child(2) > input#department") + ).isDisplayed() + ); + Assert.assertTrue( + driver.findElement( + By.cssSelector("form#kc-update-profile-form > div:nth-child(3) > div:nth-child(2) > input#username") + ).isDisplayed() + ); + Assert.assertTrue( + driver.findElement( + By.cssSelector("form#kc-update-profile-form > div:nth-child(4) > div:nth-child(2) > input#firstName") + ).isDisplayed() + ); + Assert.assertTrue( + driver.findElement( + By.cssSelector("form#kc-update-profile-form > div:nth-child(5) > div:nth-child(2) > input#email") + ).isDisplayed() + ); + } + + @Test + public void testEvents() { + + setUserProfileConfiguration(CONFIGURATION_FOR_USER_EDIT); + updateUser(user5Id, "ExistingFirst", "ExistingLast", null); + + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "}," + + "{\"name\": \"department\", " + PERMISSIONS_ALL + ", \"required\":{}}" + + "]}"); + + loginPage.open(); + loginPage.login("login-test5", "password"); + + verifyProfilePage.assertCurrent(); + //event when form is shown + events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user5Id).detail("fields_to_update", "department").assertEvent(); + + verifyProfilePage.update("First", "Last", "Department"); + //event after profile is updated + events.expectRequiredAction(EventType.UPDATE_PROFILE).user(user5Id) + .detail(Details.PREVIOUS_FIRST_NAME, "ExistingFirst").detail(Details.UPDATED_FIRST_NAME, "First") + .detail(Details.PREVIOUS_LAST_NAME, "ExistingLast").detail(Details.UPDATED_LAST_NAME, "Last") + .assertEvent(); + } + @Test public void testDefaultProfile() { setUserProfileConfiguration(null); @@ -203,8 +275,6 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); - events.expectRequiredAction(EventType.VERIFY_PROFILE).user(userId).assertEvent(); - UserRepresentation user = getUser(userId); assertEquals("First", user.getFirstName()); assertEquals("Last", user.getLastName()); @@ -214,6 +284,7 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { public void testUsernameOnlyIfEditAllowed() { RealmRepresentation realm = testRealm().toRepresentation(); + boolean r = realm.isEditUsernameAllowed(); try { setUserProfileConfiguration(null); @@ -231,7 +302,7 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { driver.navigate().refresh(); assertTrue(verifyProfilePage.isUsernamePresent()); } finally { - realm.setEditUsernameAllowed(false); + realm.setEditUsernameAllowed(r); testRealm().update(realm); } } @@ -252,8 +323,6 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); - events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user2Id).assertEvent(); - UserRepresentation user = getUser(user2Id); assertEquals("First", user.getFirstName()); assertEquals("", user.getLastName()); @@ -285,8 +354,6 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); - events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user5Id).assertEvent(); - UserRepresentation user = getUser(user5Id); assertEquals("First", user.getFirstName()); assertEquals("Last", user.getLastName()); @@ -363,8 +430,6 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); - events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user3Id).assertEvent(); - UserRepresentation user = getUser(user3Id); assertEquals("First", user.getFirstName()); assertEquals("Last", user.getLastName()); @@ -392,8 +457,6 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); - events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user4Id).assertEvent(); - UserRepresentation user = getUser(user4Id); assertEquals("First", user.getFirstName()); assertEquals("Last", user.getLastName()); @@ -423,12 +486,9 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { //submit OK verifyProfilePage.update("FirstCC", "LastCC", "DepartmentCC"); - Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); - events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user5Id).assertEvent(); - UserRepresentation user = getUser(user5Id); assertEquals("FirstCC", user.getFirstName()); assertEquals("LastCC", user.getLastName()); @@ -463,8 +523,6 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); - events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user5Id).assertEvent(); - UserRepresentation user = getUser(user5Id); assertEquals("FirstCC", user.getFirstName()); assertEquals("LastCC", user.getLastName()); @@ -516,8 +574,6 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); - - events.expectRequiredAction(EventType.VERIFY_PROFILE).client(client_scope_optional).user(user5Id).assertEvent(); UserRepresentation user = getUser(user5Id); assertEquals("FirstAA", user.getFirstName()); @@ -552,8 +608,6 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); - - events.expectRequiredAction(EventType.VERIFY_PROFILE).client(client_scope_default).user(user5Id).assertEvent(); UserRepresentation user = getUser(user5Id); assertEquals("FirstBB", user.getFirstName()); @@ -630,8 +684,6 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); - - events.expectRequiredAction(EventType.VERIFY_PROFILE).client(client_scope_optional).user(user5Id).assertEvent(); UserRepresentation user = getUser(user5Id); assertEquals("FirstAA", user.getFirstName()); @@ -663,8 +715,6 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); - - events.expectRequiredAction(EventType.VERIFY_PROFILE).client(client_scope_optional).user(user5Id).assertEvent(); UserRepresentation user = getUser(user5Id); assertEquals("FirstAA", user.getFirstName()); @@ -696,8 +746,6 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); - - events.expectRequiredAction(EventType.VERIFY_PROFILE).client(client_scope_optional).user(user5Id).assertEvent(); UserRepresentation user = getUser(user5Id); assertEquals("FirstAA", user.getFirstName()); @@ -732,8 +780,6 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); - - events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user5Id).assertEvent(); UserRepresentation user = getUser(user5Id); assertEquals("FirstCC", user.getFirstName()); @@ -761,22 +807,48 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { } protected UserRepresentation getUser(String userId) { - return testRealm().users().get(userId).toRepresentation(); + return getUser(testRealm(), userId); } protected void updateUser(String userId, String firstName, String lastName, String department) { - UserRepresentation ur = testRealm().users().get(userId).toRepresentation(); + updateUser(testRealm(), userId, firstName, lastName, department); + } + + protected void setUserProfileConfiguration(String configuration) { + setUserProfileConfiguration(testRealm(), configuration); + } + + public static void enableDynamicUserProfile(RealmRepresentation testRealm) { + if (testRealm.getAttributes() == null) { + testRealm.setAttributes(new HashMap<>()); + } + testRealm.getAttributes().put(REALM_USER_PROFILE_ENABLED, Boolean.TRUE.toString()); + } + + public static void setUserProfileConfiguration(RealmResource testRealm, String configuration) { + Response r = testRealm.users().userProfile().update(configuration); + if (r.getStatus() != 200) { + Assert.fail("UserProfile Configuration not set due to error: " + r.readEntity(String.class)); + } + } + + public static UserRepresentation getUser(RealmResource testRealm, String userId) { + return testRealm.users().get(userId).toRepresentation(); + } + + public static UserRepresentation getUserByUsername(RealmResource testRealm, String username) { + List users = testRealm.users().search(username); + if(users!=null && !users.isEmpty()) + return users.get(0); + return null; + } + + public static void updateUser(RealmResource testRealm, String userId, String firstName, String lastName, String department) { + UserRepresentation ur = getUser(testRealm, userId); ur.setFirstName(firstName); ur.setLastName(lastName); ur.singleAttribute(ATTRIBUTE_DEPARTMENT, department); - testRealm().users().get(userId).update(ur); + testRealm.users().get(userId).update(ur); } - - protected void setUserProfileConfiguration(String configuration) { - Response r = testRealm().users().userProfile().update(configuration); - if (r.getStatus() != 200) { - Assert.fail("Configuration not set due to error: " + r.readEntity(String.class)); - } - } - + } diff --git a/themes/src/main/resources/theme/base/login/register-user-profile.ftl b/themes/src/main/resources/theme/base/login/register-user-profile.ftl index 4ec7fb80a6..e0d533b89f 100755 --- a/themes/src/main/resources/theme/base/login/register-user-profile.ftl +++ b/themes/src/main/resources/theme/base/login/register-user-profile.ftl @@ -1,72 +1,54 @@ <#import "template.ftl" as layout> +<#import "user-profile-commons.ftl" as userProfileCommons> <@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section> <#if section = "header"> ${msg("registerTitle")} <#elseif section = "form">
- <#list profile.attributes as attribute> -
-
- - <#if attribute.required>* -
-
- disabled - <#if attribute.autocomplete??>autocomplete="${attribute.autocomplete}" - /> - - <#if messagesPerField.existsError('${attribute.name}')> - - ${kcSanitize(messagesPerField.get('${attribute.name}'))?no_esc} - - -
-
- <#-- render password fields just under the username or email (if used as username) --> - <#if passwordRequired?? && (attribute.name == 'username' || (attribute.name == 'email' && realm.registrationEmailAsUsername))> -
-
- * -
-
- - - <#if messagesPerField.existsError('password')> - - ${kcSanitize(messagesPerField.get('password'))?no_esc} - - -
-
- -
-
- * -
-
- - - <#if messagesPerField.existsError('password-confirm')> - - ${kcSanitize(messagesPerField.get('password-confirm'))?no_esc} - - -
-
- - - + <@userProfileCommons.userProfileFormFields; callback, attribute> + <#if callback = "afterField"> + <#-- render password fields just under the username or email (if used as username) --> + <#if passwordRequired?? && (attribute.name == 'username' || (attribute.name == 'email' && realm.registrationEmailAsUsername))> +
+
+ * +
+
+ + + <#if messagesPerField.existsError('password')> + + ${kcSanitize(messagesPerField.get('password'))?no_esc} + + +
+
+ +
+
+ * +
+
+ + + <#if messagesPerField.existsError('password-confirm')> + + ${kcSanitize(messagesPerField.get('password-confirm'))?no_esc} + + +
+
+ + + <#if recaptchaRequired??>
diff --git a/themes/src/main/resources/theme/base/login/update-user-profile.ftl b/themes/src/main/resources/theme/base/login/update-user-profile.ftl new file mode 100755 index 0000000000..071b2f1692 --- /dev/null +++ b/themes/src/main/resources/theme/base/login/update-user-profile.ftl @@ -0,0 +1,28 @@ +<#import "template.ftl" as layout> +<#import "user-profile-commons.ftl" as userProfileCommons> +<@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section> + <#if section = "header"> + ${msg("loginProfileTitle")} + <#elseif section = "form"> + + + <@userProfileCommons.userProfileFormFields/> + +
+
+
+
+
+ +
+ <#if isAppInitiatedAction??> + + + <#else> + + +
+
+ + + \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/login/user-profile-commons.ftl b/themes/src/main/resources/theme/base/login/user-profile-commons.ftl new file mode 100644 index 0000000000..888d43a32b --- /dev/null +++ b/themes/src/main/resources/theme/base/login/user-profile-commons.ftl @@ -0,0 +1,26 @@ +<#macro userProfileFormFields> + <#list profile.attributes as attribute> + <#nested "beforeField" attribute> +
+
+ + <#if attribute.required>* +
+
+ disabled + <#if attribute.autocomplete??>autocomplete="${attribute.autocomplete}" + /> + + <#if messagesPerField.existsError('${attribute.name}')> + + ${kcSanitize(messagesPerField.get('${attribute.name}'))?no_esc} + + +
+
+ <#nested "afterField" attribute> + + \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/login/verify-profile.ftl b/themes/src/main/resources/theme/base/login/verify-profile.ftl deleted file mode 100755 index 5e29d8ffea..0000000000 --- a/themes/src/main/resources/theme/base/login/verify-profile.ftl +++ /dev/null @@ -1,47 +0,0 @@ -<#import "template.ftl" as layout> -<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','email','firstName','lastName'); section> - <#if section = "header"> - ${msg("loginProfileTitle")} - <#elseif section = "form"> -
- - <#list profile.attributes as attribute> -
-
- - <#if attribute.required>* -
-
- disabled - /> - - <#if messagesPerField.existsError('${attribute.name}')> - - ${kcSanitize(messagesPerField.get('${attribute.name}'))?no_esc} - - -
-
- - -
-
-
-
-
- -
- <#if isAppInitiatedAction??> - - - <#else> - - -
-
-
- - \ No newline at end of file