[KEYCLOAK-18591] - Support a dynamic IDP user review form

This commit is contained in:
Vlastimil Elias 2021-07-02 10:48:57 +02:00 committed by Pedro Igor
parent 333f77a039
commit 6686482ba5
15 changed files with 567 additions and 41 deletions

View file

@ -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, UPDATE_USER_PROFILE;
LOGIN_OAUTH2_DEVICE_VERIFY_USER_CODE, UPDATE_USER_PROFILE, IDP_REVIEW_USER_PROFILE;
}

View file

@ -91,9 +91,17 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
updateProfileFirstLogin = authenticatorConfig.getConfig().get(IdpReviewProfileAuthenticatorFactory.UPDATE_PROFILE_ON_FIRST_LOGIN);
}
RealmModel realm = context.getRealm();
return IdentityProviderRepresentation.UPFLM_ON.equals(updateProfileFirstLogin)
|| (IdentityProviderRepresentation.UPFLM_MISSING.equals(updateProfileFirstLogin) && !Validation.validateUserMandatoryFields(realm, userCtx));
if(IdentityProviderRepresentation.UPFLM_MISSING.equals(updateProfileFirstLogin)) {
try {
UserProfileProvider profileProvider = context.getSession().getProvider(UserProfileProvider.class);
profileProvider.create(UserProfileContext.IDP_REVIEW, userCtx.getAttributes()).validate();
return false;
} catch (ValidationException pve) {
return true;
}
} else {
return IdentityProviderRepresentation.UPFLM_ON.equals(updateProfileFirstLogin);
}
}
@Override

View file

@ -30,6 +30,7 @@ import org.keycloak.forms.login.freemarker.model.AuthenticationContextBean;
import org.keycloak.forms.login.freemarker.model.ClientBean;
import org.keycloak.forms.login.freemarker.model.CodeBean;
import org.keycloak.forms.login.freemarker.model.IdentityProviderBean;
import org.keycloak.forms.login.freemarker.model.IdpReviewProfileBean;
import org.keycloak.forms.login.freemarker.model.LoginBean;
import org.keycloak.forms.login.freemarker.model.OAuthGrantBean;
import org.keycloak.forms.login.freemarker.model.ProfileBean;
@ -63,6 +64,7 @@ import org.keycloak.theme.beans.MessageBean;
import org.keycloak.theme.beans.MessageFormatterMethod;
import org.keycloak.theme.beans.MessageType;
import org.keycloak.theme.beans.MessagesPerFieldBean;
import org.keycloak.userprofile.UserProfileContext;
import org.keycloak.userprofile.UserProfileProvider;
import org.keycloak.utils.MediaType;
@ -261,6 +263,10 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
case UPDATE_USER_PROFILE:
attributes.put("profile", new VerifyProfileBean(user, formData, session));
break;
case IDP_REVIEW_USER_PROFILE:
UpdateProfileContext idpCtx = (UpdateProfileContext) attributes.get(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR);
attributes.put("profile", new IdpReviewProfileBean(idpCtx, formData, session));
break;
}
return processTemplate(theme, Templates.getTemplate(page), locale);
@ -557,8 +563,16 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
setMessage(MessageType.WARNING, Messages.UPDATE_PROFILE);
}
if(isDynamicUserProfile()) {
UpdateProfileContext userCtx = (UpdateProfileContext) attributes.get(LoginFormsProvider.UPDATE_PROFILE_CONTEXT_ATTR);
if(userCtx != null && userCtx.getUserProfileContext() == UserProfileContext.IDP_REVIEW)
return createResponse(LoginFormsPages.IDP_REVIEW_USER_PROFILE);
else
return createResponse(LoginFormsPages.UPDATE_USER_PROFILE);
} else {
return createResponse(LoginFormsPages.LOGIN_UPDATE_PROFILE);
}
}
@Override
public Response createIdpLinkConfirmLinkPage() {

View file

@ -75,6 +75,7 @@ public class Templates {
case SAML_POST_FORM:
return "saml-post-form.ftl";
case UPDATE_USER_PROFILE:
case IDP_REVIEW_USER_PROFILE:
return "update-user-profile.ftl";
default:
throw new IllegalArgumentException();

View file

@ -0,0 +1,55 @@
/*
* Copyright 2016 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.forms.login.freemarker.model;
import javax.ws.rs.core.MultivaluedMap;
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.userprofile.UserProfile;
import org.keycloak.userprofile.UserProfileContext;
import org.keycloak.userprofile.UserProfileProvider;
/**
* @author Vlastimil Elias <velias@redhat.com>
*/
public class IdpReviewProfileBean extends AbstractUserProfileBean {
private UpdateProfileContext idpCtx;
public IdpReviewProfileBean(UpdateProfileContext idpCtx, MultivaluedMap<String, String> formData, KeycloakSession session) {
super(formData);
this.idpCtx = idpCtx;
init(session, true);
}
@Override
protected UserProfile createUserProfile(UserProfileProvider provider) {
return provider.create(UserProfileContext.IDP_REVIEW, null, null);
}
@Override
protected String getAttributeDefaultValue(String name) {
return idpCtx.getFirstAttribute(name);
}
@Override
public String getContext() {
return UserProfileContext.IDP_REVIEW.name();
}
}

View file

@ -17,8 +17,6 @@
package org.keycloak.services.validation;
import org.keycloak.authentication.requiredactions.util.UpdateProfileContext;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.userprofile.ValidationException;
@ -42,17 +40,6 @@ public class Validation {
errors.add(new FormMessage(field, message, parameters));
}
/**
* Validate if user object contains all mandatory fields.
*
* @param realm user is for
* @param user to validate
* @return true if user object contains all mandatory values, false if some mandatory value is missing
*/
public static boolean validateUserMandatoryFields(RealmModel realm, UpdateProfileContext user){
return!(isBlank(user.getFirstName()) || isBlank(user.getLastName()) || isBlank(user.getEmail()));
}
/**
* Check if string is empty (null or lenght is 0)
*

View file

@ -285,7 +285,7 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
new AttributeValidatorMetadata(DuplicateUsernameValidator.ID),
new AttributeValidatorMetadata(UsernameMutationValidator.ID)).setAttributeDisplayName("${username}");
metadata.addAttribute(UserModel.EMAIL, -1, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_EMAIL)),
metadata.addAttribute(UserModel.EMAIL, -1, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_EMAIL, false)),
new AttributeValidatorMetadata(EmailValidator.ID, ValidatorConfig.builder().config(EmailValidator.IGNORE_EMPTY_VALUE, true).build()),
new AttributeValidatorMetadata(DuplicateEmailValidator.ID),
new AttributeValidatorMetadata(EmailExistsAsUsernameValidator.ID)).setAttributeDisplayName("${email}");
@ -306,9 +306,10 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
private UserProfileMetadata createBrokeringProfile(AttributeValidatorMetadata readOnlyValidator) {
UserProfileMetadata metadata = new UserProfileMetadata(IDP_REVIEW);
metadata.addAttribute(UserModel.USERNAME, -2, new AttributeValidatorMetadata(BrokeringFederatedUsernameHasValueValidator.ID)).setAttributeDisplayName("${username}");
metadata.addAttribute(UserModel.USERNAME, -2, AbstractUserProfileProvider::editUsernameCondition,
AbstractUserProfileProvider::editUsernameCondition, new AttributeValidatorMetadata(BrokeringFederatedUsernameHasValueValidator.ID)).setAttributeDisplayName("${username}");
metadata.addAttribute(UserModel.EMAIL, -1, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_EMAIL)),
metadata.addAttribute(UserModel.EMAIL, -1, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_EMAIL, true)),
new AttributeValidatorMetadata(EmailValidator.ID)).setAttributeDisplayName("${email}");
List<AttributeValidatorMetadata> readonlyValidators = new ArrayList<>();

View file

@ -134,8 +134,8 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
if (!isEnabled(session)) {
if(!context.equals(UserProfileContext.USER_API) && !context.equals(UserProfileContext.REGISTRATION_USER_CREATION)) {
decoratedMetadata.addAttribute(UserModel.FIRST_NAME, 1, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(
Messages.MISSING_FIRST_NAME))).setAttributeDisplayName("${firstName}");
decoratedMetadata.addAttribute(UserModel.LAST_NAME, 2, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_LAST_NAME))).setAttributeDisplayName("${lastName}");
Messages.MISSING_FIRST_NAME, metadata.getContext() == UserProfileContext.IDP_REVIEW))).setAttributeDisplayName("${firstName}");
decoratedMetadata.addAttribute(UserModel.LAST_NAME, 2, new AttributeValidatorMetadata(BlankAttributeValidator.ID, BlankAttributeValidator.createConfig(Messages.MISSING_LAST_NAME, metadata.getContext() == UserProfileContext.IDP_REVIEW))).setAttributeDisplayName("${lastName}");
return decoratedMetadata;
}
return decoratedMetadata;

View file

@ -23,6 +23,7 @@ import org.keycloak.validate.SimpleValidator;
import org.keycloak.validate.ValidationContext;
import org.keycloak.validate.ValidationError;
import org.keycloak.validate.ValidatorConfig;
import org.keycloak.validate.ValidatorConfig.ValidatorConfigBuilder;
/**
* Validator to check that User Profile attribute value is not blank (null value is OK!). Expects List of Strings as
@ -37,6 +38,8 @@ public class BlankAttributeValidator implements SimpleValidator {
public static final String CFG_ERROR_MESSAGE = "error-message";
public static final String CFG_FAIL_ON_NULL = "fail-on-null";
@Override
public String getId() {
return ID;
@ -47,13 +50,15 @@ public class BlankAttributeValidator implements SimpleValidator {
@SuppressWarnings("unchecked")
List<String> values = (List<String>) input;
if (values.isEmpty()) {
boolean failOnNull = config.getBooleanOrDefault(CFG_FAIL_ON_NULL, false);
if (values.isEmpty() && !failOnNull) {
return context;
}
String value = values.get(0);
String value = values.isEmpty() ? null: values.get(0);
if (value != null && Validation.isBlank(value)) {
if ((failOnNull || value != null) && Validation.isBlank(value)) {
context.addError(new ValidationError(ID, inputHint, config.getStringOrDefault(CFG_ERROR_MESSAGE, AttributeRequiredByMetadataValidator.ERROR_USER_ATTRIBUTE_REQUIRED)));
}
@ -64,13 +69,16 @@ public class BlankAttributeValidator implements SimpleValidator {
* Create config for this validator to get customized error message
*
* @param errorMessage to be used if validation fails
* @param failOnNull makes validator fail on null values also (not on empty string only as is the default behavior)
* @return config
*/
public static ValidatorConfig createConfig(String errorMessage) {
public static ValidatorConfig createConfig(String errorMessage, boolean failOnNull) {
ValidatorConfigBuilder builder = ValidatorConfig.builder();
builder.config(CFG_FAIL_ON_NULL, failOnNull);
if (errorMessage != null) {
return ValidatorConfig.builder().config(CFG_ERROR_MESSAGE, errorMessage).build();
builder.config(CFG_ERROR_MESSAGE, errorMessage);
}
return ValidatorConfig.EMPTY;
return builder.build();
}
}

View file

@ -1,5 +1,7 @@
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;
@ -19,6 +21,9 @@ public class UpdateAccountInformationPage extends LanguageComboboxAwarePage {
@FindBy(id = "lastName")
private WebElement lastNameInput;
@FindBy(id = "department")
private WebElement departmentInput;
@FindBy(css = "input[type=\"submit\"]")
private WebElement submitButton;
@ -41,6 +46,29 @@ public class UpdateAccountInformationPage extends LanguageComboboxAwarePage {
clickLink(submitButton);
}
public void updateAccountInformation(String userName,
String email,
String firstName,
String lastName,
String department) {
usernameInput.clear();
usernameInput.sendKeys(userName);
emailInput.clear();
emailInput.sendKeys(email);
firstNameInput.clear();
firstNameInput.sendKeys(firstName);
lastNameInput.clear();
lastNameInput.sendKeys(lastName);
departmentInput.clear();
departmentInput.sendKeys(department);
clickLink(submitButton);
}
public void updateAccountInformation(String email,
String firstName,
String lastName) {
@ -72,6 +100,18 @@ public class UpdateAccountInformationPage extends LanguageComboboxAwarePage {
return PageUtils.getPageTitle(driver).equalsIgnoreCase("update account information");
}
public String getLabelForField(String fieldId) {
return driver.findElement(By.cssSelector("label[for="+fieldId+"]")).getText();
}
public boolean isDepartmentPresent() {
try {
return driver.findElement(By.id("department")).isDisplayed();
} catch (NoSuchElementException nse) {
return false;
}
}
@Override
public void open() throws Exception {

View file

@ -22,6 +22,7 @@ import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.forms.VerifyProfileTest;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.util.MailServer;
import org.keycloak.testsuite.util.MailServerConfiguration;
@ -55,6 +56,17 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa
@SecondBrowser
protected WebDriver driver2;
protected void enableDynamicUserProfile() {
RealmResource rr = adminClient.realm(bc.consumerRealmName());
RealmRepresentation testRealm = rr.toRepresentation();
VerifyProfileTest.enableDynamicUserProfile(testRealm);
rr.update(testRealm);
}
/**
* Refers to in old test suite: org.keycloak.testsuite.broker.AbstractFirstBrokerLoginTest#testErrorPageWhenDuplicationNotAllowed_updateProfileOn
@ -452,18 +464,6 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa
}
@Test
public void testFirstBrokerLoginFlowUpdateProfileOff() {
updateExecutions(AbstractBrokerTest::disableUpdateProfileOnFirstLogin);
driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName()));
logInWithBroker(bc);
waitForAccountManagementTitle();
accountUpdateProfilePage.assertCurrent();
}
/**
* Refers to in old test suite: org.keycloak.testsuite.broker.AbstractFirstBrokerLoginTest#testErrorPageWhenDuplicationNotAllowed_updateProfileOff
*/
@ -572,6 +572,10 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa
updateAccountInformationPage.updateAccountInformation("test", "test@localhost.com", "FirstName", "LastName");
waitForAccountManagementTitle();
accountUpdateProfilePage.assertCurrent();
Assert.assertEquals("FirstName", accountUpdateProfilePage.getFirstName());
Assert.assertEquals("LastName", accountUpdateProfilePage.getLastName());
Assert.assertEquals("test@localhost.com", accountUpdateProfilePage.getEmail());
Assert.assertEquals("test", accountUpdateProfilePage.getUsername());
}
@ -991,6 +995,11 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa
updateAccountInformationPage.updateAccountInformation("FirstName", "LastName");
waitForAccountManagementTitle();
accountUpdateProfilePage.assertCurrent();
Assert.assertEquals("FirstName", accountUpdateProfilePage.getFirstName());
Assert.assertEquals("LastName", accountUpdateProfilePage.getLastName());
Assert.assertEquals("no-first-name@localhost.com", accountUpdateProfilePage.getEmail());
Assert.assertEquals("no-first-name", accountUpdateProfilePage.getUsername());
logoutFromRealm(getProviderRoot(), bc.providerRealmName());
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
@ -1009,6 +1018,10 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa
updateAccountInformationPage.updateAccountInformation("FirstName", "LastName");
waitForAccountManagementTitle();
accountUpdateProfilePage.assertCurrent();
Assert.assertEquals("FirstName", accountUpdateProfilePage.getFirstName());
Assert.assertEquals("LastName", accountUpdateProfilePage.getLastName());
Assert.assertEquals("no-last-name@localhost.com", accountUpdateProfilePage.getEmail());
Assert.assertEquals("no-last-name", accountUpdateProfilePage.getUsername());
logoutFromRealm(getProviderRoot(), bc.providerRealmName());
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
@ -1028,6 +1041,10 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa
waitForAccountManagementTitle();
accountUpdateProfilePage.assertCurrent();
Assert.assertEquals("FirstName", accountUpdateProfilePage.getFirstName());
Assert.assertEquals("LastName", accountUpdateProfilePage.getLastName());
Assert.assertEquals("no-email@localhost.com", accountUpdateProfilePage.getEmail());
Assert.assertEquals("no-email", accountUpdateProfilePage.getUsername());
}
@ -1050,6 +1067,10 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa
waitForAccountManagementTitle();
accountUpdateProfilePage.assertCurrent();
Assert.assertEquals("FirstName", accountUpdateProfilePage.getFirstName());
Assert.assertEquals("LastName", accountUpdateProfilePage.getLastName());
Assert.assertEquals("all-info-set@localhost.com", accountUpdateProfilePage.getEmail());
Assert.assertEquals("all-info-set", accountUpdateProfilePage.getUsername());
}
@ -1064,6 +1085,10 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa
logInWithBroker(bc);
waitForAccountManagementTitle();
accountUpdateProfilePage.assertCurrent();
Assert.assertEquals("", accountUpdateProfilePage.getFirstName());
Assert.assertEquals("", accountUpdateProfilePage.getLastName());
Assert.assertEquals(bc.getUserEmail(), accountUpdateProfilePage.getEmail());
Assert.assertEquals(bc.getUserLogin(), accountUpdateProfilePage.getUsername());
}

View file

@ -267,5 +267,14 @@ public class KcOidcFirstBrokerLoginTest extends AbstractFirstBrokerLoginTest {
updateAccountInformationPage.assertCurrent();
assertEquals("Please specify username.", loginUpdateProfilePage.getInputErrors().getUsernameError());
updateAccountInformationPage.updateAccountInformation("new-username", "no-first-name@localhost.com", "First Name", "Last Name");
waitForAccountManagementTitle();
accountUpdateProfilePage.assertCurrent();
Assert.assertEquals("First Name", accountUpdateProfilePage.getFirstName());
Assert.assertEquals("Last Name", accountUpdateProfilePage.getLastName());
Assert.assertEquals("no-first-name@localhost.com", accountUpdateProfilePage.getEmail());
Assert.assertEquals("new-username", accountUpdateProfilePage.getUsername());
}
}

View file

@ -0,0 +1,342 @@
/*
* 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.broker;
import static org.junit.Assert.assertEquals;
import static org.keycloak.testsuite.broker.BrokerTestTools.getConsumerRoot;
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
import static org.keycloak.testsuite.forms.VerifyProfileTest.ATTRIBUTE_DEPARTMENT;
import static org.keycloak.testsuite.forms.VerifyProfileTest.PERMISSIONS_ADMIN_EDITABLE;
import static org.keycloak.testsuite.forms.VerifyProfileTest.PERMISSIONS_ALL;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.forms.VerifyProfileTest;
import org.keycloak.testsuite.util.ClientScopeBuilder;
import org.openqa.selenium.By;
/**
*
* @author Vlastimil Elias <velias@redhat.com>
*
*/
public class KcOidcFirstBrokerLoginWithUserProfileTest extends KcOidcFirstBrokerLoginTest {
@Override
@Before
public void beforeBrokerTest() {
super.beforeBrokerTest();
enableDynamicUserProfile();
}
@Test
public void testDisplayName() {
updateExecutions(AbstractBrokerTest::enableUpdateProfileOnFirstLogin);
setUserProfileConfiguration("{\"attributes\": ["
+ "{\"name\": \"firstName\",\"displayName\":\"${firstName}\"," + PERMISSIONS_ALL + ", \"required\": {}},"
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
+ "{\"name\": \"department\", \"displayName\" : \"Department\", " + PERMISSIONS_ALL + ", \"required\":{}}"
+ "]}");
driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName()));
logInWithBroker(bc);
waitForPage(driver, "update account information", false);
updateAccountInformationPage.assertCurrent();
//assert field names
// i18n replaced
Assert.assertEquals("First name", updateAccountInformationPage.getLabelForField("firstName"));
// attribute name used if no display name set
Assert.assertEquals("lastName", updateAccountInformationPage.getLabelForField("lastName"));
// direct value in display name
Assert.assertEquals("Department", updateAccountInformationPage.getLabelForField("department"));
}
@Test
public void testAttributeGuiOrder() {
updateExecutions(AbstractBrokerTest::enableUpdateProfileOnFirstLogin);
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 + "}"
+ "]}");
driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName()));
logInWithBroker(bc);
waitForPage(driver, "update account information", false);
updateAccountInformationPage.assertCurrent();
//assert fields location in form
String htmlFormId = "kc-update-profile-form";
Assert.assertTrue(
driver.findElement(
By.cssSelector("form#"+htmlFormId+" > div:nth-child(1) > div:nth-child(2) > input#lastName")
).isDisplayed()
);
Assert.assertTrue(
driver.findElement(
By.cssSelector("form#"+htmlFormId+" > div:nth-child(2) > div:nth-child(2) > input#department")
).isDisplayed()
);
Assert.assertTrue(
driver.findElement(
By.cssSelector("form#"+htmlFormId+" > div:nth-child(3) > div:nth-child(2) > input#username")
).isDisplayed()
);
Assert.assertTrue(
driver.findElement(
By.cssSelector("form#"+htmlFormId+" > div:nth-child(4) > div:nth-child(2) > input#firstName")
).isDisplayed()
);
Assert.assertTrue(
driver.findElement(
By.cssSelector("form#"+htmlFormId+" > div:nth-child(5) > div:nth-child(2) > input#email")
).isDisplayed()
);
}
@Test
public void testDynamicUserProfileReviewWhenMissing_requiredReadOnlyAttributeDoesnotForceUpdate() {
updateExecutions(AbstractBrokerTest::setUpMissingUpdateProfileOnFirstLogin);
setUserProfileConfiguration("{\"attributes\": ["
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + "},"
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
+ "{\"name\": \"department\", " + PERMISSIONS_ADMIN_EDITABLE + ", \"required\":{}}"
+ "]}");
driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName()));
logInWithBroker(bc);
waitForAccountManagementTitle();
accountUpdateProfilePage.assertCurrent();
}
@Test
public void testDynamicUserProfileReviewWhenMissing_requiredButNotSelectedByScopeAttributeDoesnotForceUpdate() {
addDepartmentScopeIntoRealm();
updateExecutions(AbstractBrokerTest::setUpMissingUpdateProfileOnFirstLogin);
setUserProfileConfiguration("{\"attributes\": ["
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + "},"
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
+ "{\"name\": \"department\", " + PERMISSIONS_ALL + ", \"required\":{}, \"selector\":{\"scopes\":[\"department\"]}}"
+ "]}");
driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName()));
logInWithBroker(bc);
waitForAccountManagementTitle();
accountUpdateProfilePage.assertCurrent();
}
@Test
public void testDynamicUserProfileReviewWhenMissing_requiredAndSelectedByScopeAttributeForcesUpdate() {
updateExecutions(AbstractBrokerTest::setUpMissingUpdateProfileOnFirstLogin);
//we use 'profile' scope which is requested by default
setUserProfileConfiguration("{\"attributes\": ["
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + "},"
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
+ "{\"name\": \"department\", " + PERMISSIONS_ALL + ", \"required\":{}, \"selector\":{\"scopes\":[\"profile\"]}}"
+ "]}");
driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName()));
logInWithBroker(bc);
waitForPage(driver, "update account information", false);
updateAccountInformationPage.assertCurrent();
}
@Test
public void testDynamicUserProfileReview_requiredReadOnlyAttributeNotRenderedAndNotBlockingProcess() {
updateExecutions(AbstractBrokerTest::enableUpdateProfileOnFirstLogin);
setUserProfileConfiguration("{\"attributes\": ["
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
+ "{\"name\": \"department\", " + PERMISSIONS_ADMIN_EDITABLE + ", \"required\":{}}"
+ "]}");
driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName()));
logInWithBroker(bc);
waitForPage(driver, "update account information", false);
updateAccountInformationPage.assertCurrent();
Assert.assertFalse(updateAccountInformationPage.isDepartmentPresent());
updateAccountInformationPage.updateAccountInformation( "requiredReadOnlyAttributeNotRenderedAndNotBlockingRegistration", "requiredReadOnlyAttributeNotRenderedAndNotBlockingRegistration@email", "FirstAA", "LastAA");
waitForAccountManagementTitle();
accountUpdateProfilePage.assertCurrent();
}
@Test
public void testDynamicUserProfileReview_attributeRequiredAndSelectedByScopeMustBeSet() {
updateExecutions(AbstractBrokerTest::enableUpdateProfileOnFirstLogin);
//we use 'profile' scope which is requested by default
setUserProfileConfiguration("{\"attributes\": ["
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"required\":{}, \"selector\":{\"scopes\":[\"profile\"]}}"
+ "]}");
driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName()));
logInWithBroker(bc);
waitForPage(driver, "update account information", false);
updateAccountInformationPage.assertCurrent();
//check required validation works
updateAccountInformationPage.updateAccountInformation( "attributeRequiredAndSelectedByScopeMustBeSet", "attributeRequiredAndSelectedByScopeMustBeSet@email", "FirstAA", "LastAA", "");
updateAccountInformationPage.assertCurrent();
updateAccountInformationPage.updateAccountInformation( "attributeRequiredAndSelectedByScopeMustBeSet", "attributeRequiredAndSelectedByScopeMustBeSet@email", "FirstAA", "LastAA", "DepartmentAA");
waitForAccountManagementTitle();
accountUpdateProfilePage.assertCurrent();
UserRepresentation user = VerifyProfileTest.getUserByUsername(testRealm(),"attributeRequiredAndSelectedByScopeMustBeSet");
assertEquals("FirstAA", user.getFirstName());
assertEquals("LastAA", user.getLastName());
assertEquals("DepartmentAA", user.firstAttribute(ATTRIBUTE_DEPARTMENT));
}
@Test
public void testDynamicUserProfileReview_attributeNotRequiredAndSelectedByScopeCanBeIgnored() {
updateExecutions(AbstractBrokerTest::enableUpdateProfileOnFirstLogin);
//we use 'profile' scope which is requested by default
setUserProfileConfiguration("{\"attributes\": ["
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"selector\":{\"scopes\":[\"profile\"]}}"
+ "]}");
driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName()));
logInWithBroker(bc);
waitForPage(driver, "update account information", false);
updateAccountInformationPage.assertCurrent();
Assert.assertTrue(updateAccountInformationPage.isDepartmentPresent());
updateAccountInformationPage.updateAccountInformation( "attributeNotRequiredAndSelectedByScopeCanBeIgnored", "attributeNotRequiredAndSelectedByScopeCanBeIgnored@email", "FirstAA", "LastAA");
waitForAccountManagementTitle();
accountUpdateProfilePage.assertCurrent();
UserRepresentation user = VerifyProfileTest.getUserByUsername(testRealm(),"attributeNotRequiredAndSelectedByScopeCanBeIgnored");
assertEquals("FirstAA", user.getFirstName());
assertEquals("LastAA", user.getLastName());
assertEquals("", user.firstAttribute(ATTRIBUTE_DEPARTMENT));
}
@Test
public void testDynamicUserProfileReview_attributeNotRequiredAndSelectedByScopeCanBeSet() {
updateExecutions(AbstractBrokerTest::enableUpdateProfileOnFirstLogin);
//we use 'profile' scope which is requested by default
setUserProfileConfiguration("{\"attributes\": ["
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"selector\":{\"scopes\":[\"profile\"]}}"
+ "]}");
driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName()));
logInWithBroker(bc);
waitForPage(driver, "update account information", false);
updateAccountInformationPage.assertCurrent();
Assert.assertTrue(updateAccountInformationPage.isDepartmentPresent());
updateAccountInformationPage.updateAccountInformation( "attributeNotRequiredAndSelectedByScopeCanBeSet", "attributeNotRequiredAndSelectedByScopeCanBeSet@email", "FirstAA", "LastAA","Department AA");
waitForAccountManagementTitle();
accountUpdateProfilePage.assertCurrent();
UserRepresentation user = VerifyProfileTest.getUserByUsername(testRealm(),"attributeNotRequiredAndSelectedByScopeCanBeSet");
assertEquals("FirstAA", user.getFirstName());
assertEquals("LastAA", user.getLastName());
assertEquals("Department AA", user.firstAttribute(ATTRIBUTE_DEPARTMENT));
}
@Test
public void testDynamicUserProfileReview_attributeRequiredButNotSelectedByScopeIsNotRenderedAndNotBlockingProcess() {
addDepartmentScopeIntoRealm();
updateExecutions(AbstractBrokerTest::enableUpdateProfileOnFirstLogin);
setUserProfileConfiguration("{\"attributes\": ["
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"required\":{}, \"selector\":{\"scopes\":[\"department\"]}}"
+ "]}");
driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName()));
logInWithBroker(bc);
waitForPage(driver, "update account information", false);
updateAccountInformationPage.assertCurrent();
Assert.assertFalse(updateAccountInformationPage.isDepartmentPresent());
updateAccountInformationPage.updateAccountInformation( "attributeRequiredButNotSelectedByScopeIsNotRenderedAndNotBlockingRegistration", "attributeRequiredButNotSelectedByScopeIsNotRenderedAndNotBlockingRegistration@email", "FirstAA", "LastAA");
waitForAccountManagementTitle();
accountUpdateProfilePage.assertCurrent();
UserRepresentation user = VerifyProfileTest.getUserByUsername(testRealm(),"attributeRequiredButNotSelectedByScopeIsNotRenderedAndNotBlockingRegistration");
assertEquals("FirstAA", user.getFirstName());
assertEquals("LastAA", user.getLastName());
assertEquals(null, user.firstAttribute(ATTRIBUTE_DEPARTMENT));
}
public void addDepartmentScopeIntoRealm() {
testRealm().clientScopes().create(ClientScopeBuilder.create().name("department").protocol("openid-connect").build());
}
protected void setUserProfileConfiguration(String configuration) {
VerifyProfileTest.setUserProfileConfiguration(testRealm(), configuration);
}
private RealmResource testRealm() {
return adminClient.realm(bc.consumerRealmName());
}
}

View file

@ -0,0 +1,35 @@
/*
* 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.broker;
import org.junit.Before;
/**
*
* @author Vlastimil Elias <velias@redhat.com>
*
*/
public class KcSamlFirstBrokerLoginWithUserProfileTest extends KcSamlFirstBrokerLoginTest {
@Override
@Before
public void beforeBrokerTest() {
super.beforeBrokerTest();
enableDynamicUserProfile();
}
}

View file

@ -1046,6 +1046,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
Map<String, Object> attributes = new HashMap<>();
attributes.put(UserModel.USERNAME, "user");
attributes.put(UserModel.EMAIL, "user@email.test");
// client with default scopes for which is attribute NOT configured as required
configureAuthenticationSession(session, "client-b", null);