mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
Hide read-only email attribute in update profile context with update … …email enabled (#43024)
* Hide read-only email attribute in update profile context with update email enabled Closes #42990 Signed-off-by: Martin Kanis <mkanis@redhat.com> * Simplifying conditions when checking read/write on email attribute and more tests Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com> --------- Signed-off-by: Martin Kanis <mkanis@redhat.com> Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com> Co-authored-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
8dd7437e90
commit
a493213ad4
@ -57,3 +57,9 @@ If the email attribute is set as required in the user profile configuration, the
|
||||
meaning a user won't be able to clear his/her email in update email page. The opposite is true, if the email attribute is set as optional
|
||||
in the user profile configuration.
|
||||
|
||||
If the email attribute is set as read-only in the user profile configuration, the following behavior applies:
|
||||
|
||||
* The `Update email` link will not be displayed in the account console
|
||||
* The `UPDATE_EMAIL` required action will be automatically skipped and removed from the user account
|
||||
* In the update profile page, if the user's email is initially empty, the email field will be hidden
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ import java.util.stream.Stream;
|
||||
|
||||
import jakarta.ws.rs.core.MultivaluedHashMap;
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
@ -60,6 +61,7 @@ import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.validation.Validation;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.userprofile.EventAuditingAttributeChangeListener;
|
||||
@ -157,7 +159,6 @@ public class UpdateEmail implements RequiredActionProvider, RequiredActionFactor
|
||||
if (newEmail != null) {
|
||||
// Remove VERIFY_EMAIL to ensure UPDATE_EMAIL takes precedence when both realm verification and forced verification are enabled.
|
||||
user.removeRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);
|
||||
setPendingEmailVerification(context, newEmail);
|
||||
sendEmailUpdateConfirmation(context, false);
|
||||
} else {
|
||||
// Check if email verification is pending and show message for subsequent visits
|
||||
@ -238,6 +239,10 @@ public class UpdateEmail implements RequiredActionProvider, RequiredActionFactor
|
||||
} catch (EmailException e) {
|
||||
logger.error("Failed to send email for email update", e);
|
||||
context.getEvent().error(Errors.EMAIL_SEND_FAILED);
|
||||
context.failure(Messages.EMAIL_SENT_ERROR);
|
||||
context.challenge(context.form()
|
||||
.setError(Messages.EMAIL_SENT_ERROR)
|
||||
.createErrorPage(Response.Status.INTERNAL_SERVER_ERROR));
|
||||
return;
|
||||
}
|
||||
context.getEvent().success();
|
||||
|
||||
@ -67,6 +67,7 @@ import org.keycloak.userprofile.validator.RegistrationEmailAsUsernameUsernameVal
|
||||
import org.keycloak.userprofile.validator.RegistrationUsernameExistsValidator;
|
||||
import org.keycloak.userprofile.validator.UsernameHasValueValidator;
|
||||
import org.keycloak.userprofile.validator.UsernameMutationValidator;
|
||||
import org.keycloak.utils.StringUtil;
|
||||
import org.keycloak.validate.ValidatorConfig;
|
||||
import org.keycloak.validate.validators.EmailValidator;
|
||||
|
||||
@ -155,15 +156,23 @@ public class DeclarativeUserProfileProviderFactory implements UserProfileProvide
|
||||
|
||||
if (UpdateEmail.isEnabled(realm)) {
|
||||
if (UPDATE_PROFILE.equals(c.getContext())) {
|
||||
UserModel user = c.getUser();
|
||||
|
||||
if (!isNewUser(c)) {
|
||||
if (c.getUser().getEmail() == null || c.getUser().getEmail().isEmpty()) {
|
||||
if (StringUtil.isBlank(user.getEmail())) {
|
||||
// allow to set email via UPDATE_PROFILE if the email is not set for the user
|
||||
return true;
|
||||
}
|
||||
} else if (UserModel.EMAIL.equals(c.getAttribute().getKey()) && c.getAttribute().getValue().isEmpty()) {
|
||||
return true;
|
||||
|
||||
List<String> values = c.getAttribute().getValue();
|
||||
|
||||
if (values == null || values.isEmpty()) {
|
||||
// ignore empty values if the user has an email set, email should be set via update email flow
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !(UPDATE_PROFILE.equals(c.getContext()) || ACCOUNT.equals(c.getContext()));
|
||||
}
|
||||
|
||||
@ -185,15 +194,14 @@ public class DeclarativeUserProfileProviderFactory implements UserProfileProvide
|
||||
|
||||
if (UpdateEmail.isEnabled(session.getContext().getRealm())) {
|
||||
if (UPDATE_PROFILE.equals(c.getContext())) {
|
||||
if (!isNewUser(c)) {
|
||||
if (c.getUser().getEmail() == null || c.getUser().getEmail().isEmpty()) {
|
||||
// show email field in UPDATE_PROFILE page if the email is not set for the user
|
||||
return true;
|
||||
}
|
||||
} else if (UserModel.EMAIL.equals(c.getAttribute().getKey()) && c.getAttribute().getValue().isEmpty()) {
|
||||
List<String> value = c.getAttribute().getValue();
|
||||
|
||||
if (value.isEmpty() && !c.getMetadata().isReadOnly(c)) {
|
||||
// show email field in UPDATE_PROFILE page if the email is not set for the user and is not read-only
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return !UPDATE_PROFILE.equals(context);
|
||||
}
|
||||
|
||||
|
||||
@ -131,6 +131,15 @@ public class LoginUpdateProfilePage extends AbstractPage {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEmailInputPresent() {
|
||||
try {
|
||||
emailInput.isDisplayed();
|
||||
return true;
|
||||
} catch (NoSuchElementException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isDepartmentPresent() {
|
||||
try {
|
||||
isDepartmentEnabled();
|
||||
|
||||
@ -39,9 +39,9 @@ import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.auth.page.login.UpdateEmailPage;
|
||||
import org.keycloak.testsuite.pages.AppPage;
|
||||
import org.keycloak.testsuite.pages.ErrorPage;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
|
||||
import org.keycloak.testsuite.pages.VerifyEmailPage;
|
||||
import org.keycloak.testsuite.util.SecondBrowser;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
@ -64,9 +64,12 @@ public abstract class AbstractRequiredActionUpdateEmailTest extends AbstractTest
|
||||
@Page
|
||||
protected AppPage appPage;
|
||||
|
||||
@Drone
|
||||
@SecondBrowser
|
||||
protected WebDriver driver2;
|
||||
@Page
|
||||
protected ErrorPage errorPage;
|
||||
|
||||
@Drone
|
||||
@SecondBrowser
|
||||
protected WebDriver driver2;
|
||||
|
||||
@Before
|
||||
public void beforeTest() {
|
||||
|
||||
@ -18,22 +18,30 @@ package org.keycloak.testsuite.actions;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.keycloak.userprofile.UserProfileConstants.ROLE_USER;
|
||||
import static org.keycloak.userprofile.UserProfileConstants.ROLE_ADMIN;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.resource.AuthenticationManagementResource;
|
||||
import org.keycloak.admin.client.resource.UserProfileResource;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.authentication.requiredactions.UpdateEmail;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserModel.RequiredAction;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.idm.UserSessionRepresentation;
|
||||
import org.keycloak.representations.userprofile.config.UPAttribute;
|
||||
@ -135,4 +143,109 @@ public class RequiredActionUpdateEmailTest extends AbstractRequiredActionUpdateE
|
||||
|
||||
assertEquals(0, testUser.toRepresentation().getRequiredActions().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateProfileWhenEmailIsSetAndIsWritable() {
|
||||
configureRequiredActionsToUser("test-user@localhost", RequiredAction.UPDATE_PROFILE.name());
|
||||
UserResource testUser = testRealm().users().get(findUser("test-user@localhost").getId());
|
||||
assertEquals(1, testUser.toRepresentation().getRequiredActions().size());
|
||||
|
||||
// login and update profile, email is already set and should not be visible
|
||||
loginPage.open();
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
updateProfilePage.assertCurrent();
|
||||
assertFalse(updateProfilePage.isEmailInputPresent());
|
||||
updateProfilePage.update("Tom", "Brady");
|
||||
|
||||
// successfully update the profile without providing the email
|
||||
appPage.assertCurrent();
|
||||
assertEquals(0, testUser.toRepresentation().getRequiredActions().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateProfileWhenEmailNotSetAndIsWritable() {
|
||||
configureRequiredActionsToUser("test-user@localhost", RequiredAction.UPDATE_PROFILE.name());
|
||||
UserResource testUser = testRealm().users().get(findUser("test-user@localhost").getId());
|
||||
assertEquals(1, testUser.toRepresentation().getRequiredActions().size());
|
||||
UserRepresentation rep = testUser.toRepresentation();
|
||||
rep.setEmail("");
|
||||
testUser.update(rep);
|
||||
|
||||
// login and update profile, including the email
|
||||
loginPage.open();
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
updateProfilePage.assertCurrent();
|
||||
assertTrue(updateProfilePage.isEmailInputPresent());
|
||||
updateProfilePage.update("Tom", "Brady", "test-user@localhost");
|
||||
|
||||
appPage.assertCurrent();
|
||||
rep = testUser.toRepresentation();
|
||||
assertEquals(0, rep.getRequiredActions().size());
|
||||
assertNull(Optional.ofNullable(rep.getAttributes()).orElse(Map.of()).get(UserModel.EMAIL_PENDING));
|
||||
assertEquals("test-user@localhost", rep.getEmail());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateProfileWhenEmailNotSetAndIsNotWritable() {
|
||||
UPConfig upConfig = testRealm().users().userProfile().getConfiguration();
|
||||
upConfig.getAttribute(UserModel.EMAIL).setPermissions(new UPAttributePermissions(Set.of(ROLE_USER, ROLE_ADMIN), Set.of(ROLE_ADMIN)));
|
||||
testRealm().users().userProfile().update(upConfig);
|
||||
getCleanup().addCleanup(() -> {
|
||||
upConfig.getAttribute(UserModel.EMAIL).setPermissions(new UPAttributePermissions(Set.of(ROLE_USER, ROLE_ADMIN), Set.of(ROLE_USER, ROLE_ADMIN)));
|
||||
testRealm().users().userProfile().update(upConfig);
|
||||
});
|
||||
configureRequiredActionsToUser("test-user@localhost", RequiredAction.UPDATE_PROFILE.name());
|
||||
UserResource testUser = testRealm().users().get(findUser("test-user@localhost").getId());
|
||||
assertEquals(1, testUser.toRepresentation().getRequiredActions().size());
|
||||
UserRepresentation rep = testUser.toRepresentation();
|
||||
rep.setEmail("");
|
||||
testUser.update(rep);
|
||||
|
||||
// login and update profile, email is readonly for users and should not be visible
|
||||
loginPage.open();
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
updateProfilePage.assertCurrent();
|
||||
assertFalse(updateProfilePage.isEmailInputPresent());
|
||||
updateProfilePage.update("Tom", "Brady");
|
||||
|
||||
appPage.assertCurrent();
|
||||
rep = testUser.toRepresentation();
|
||||
assertEquals(0, rep.getRequiredActions().size());
|
||||
assertNull(Optional.ofNullable(rep.getAttributes()).orElse(Map.of()).get(UserModel.EMAIL_PENDING));
|
||||
assertNull(rep.getEmail());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailWhenSendingVerificationEmail() {
|
||||
AuthenticationManagementResource authMgt = testRealm().flows();
|
||||
RequiredActionProviderRepresentation requiredAction = authMgt.getRequiredActions().stream()
|
||||
.filter(action -> RequiredAction.UPDATE_EMAIL.name().equals(action.getAlias()))
|
||||
.findAny().get();
|
||||
requiredAction.getConfig().put(UpdateEmail.CONFIG_VERIFY_EMAIL, Boolean.TRUE.toString());
|
||||
authMgt.updateRequiredAction(requiredAction.getAlias(), requiredAction);
|
||||
getCleanup().addCleanup(() -> {
|
||||
requiredAction.getConfig().remove(UpdateEmail.CONFIG_VERIFY_EMAIL);
|
||||
authMgt.updateRequiredAction(requiredAction.getAlias(), requiredAction);
|
||||
});
|
||||
|
||||
configureRequiredActionsToUser("test-user@localhost", RequiredAction.UPDATE_PROFILE.name());
|
||||
UserResource testUser = testRealm().users().get(findUser("test-user@localhost").getId());
|
||||
assertEquals(1, testUser.toRepresentation().getRequiredActions().size());
|
||||
UserRepresentation rep = testUser.toRepresentation();
|
||||
rep.setEmail("");
|
||||
testUser.update(rep);
|
||||
|
||||
loginPage.open();
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
updateProfilePage.assertCurrent();
|
||||
assertTrue(updateProfilePage.isEmailInputPresent());
|
||||
updateProfilePage.update("Tom", "Brady", "test-user@localhost");
|
||||
errorPage.assertCurrent();
|
||||
assertEquals("Failed to send email, please try again later.", errorPage.getError());
|
||||
rep = testUser.toRepresentation();
|
||||
assertEquals(1, rep.getRequiredActions().size());
|
||||
assertEquals(RequiredAction.UPDATE_EMAIL.name(), rep.getRequiredActions().get(0));
|
||||
assertNull(Optional.ofNullable(rep.getAttributes()).orElse(Map.of()).get(UserModel.EMAIL_PENDING));
|
||||
assertNull(rep.getEmail());
|
||||
}
|
||||
}
|
||||
|
||||
@ -589,4 +589,37 @@ public class RequiredActionUpdateEmailTestWithVerificationTest extends AbstractR
|
||||
ApiUtil.removeUserByUsername(testRealm(), "realmverifyuser");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateProfileWithVerificationWhenEmailIsNotSetAndIsWritable() throws MessagingException, IOException {
|
||||
configureRequiredActionsToUser("test-user@localhost", RequiredAction.UPDATE_PROFILE.name());
|
||||
UserResource testUser = testRealm().users().get(findUser("test-user@localhost").getId());
|
||||
assertEquals(1, testUser.toRepresentation().getRequiredActions().size());
|
||||
UserRepresentation rep = testUser.toRepresentation();
|
||||
rep.setEmail("");
|
||||
testUser.update(rep);
|
||||
|
||||
// login and update profile, email is empty and writable, so email input should be present
|
||||
loginPage.open();
|
||||
loginPage.login("test-user@localhost", "password");
|
||||
updateProfilePage.assertCurrent();
|
||||
assertTrue(updateProfilePage.isEmailInputPresent());
|
||||
updateProfilePage.update("Tom", "Brady", "test-user@localhost");
|
||||
|
||||
// Should send verification email and show pending verification message
|
||||
assertThat("Should show pending verification message with realm verification enabled",
|
||||
driver.getPageSource(), containsString("A confirmation email has been sent to test-user@localhost."));
|
||||
String confirmationLink = fetchEmailConfirmationLink("test-user@localhost");
|
||||
rep = testUser.toRepresentation();
|
||||
assertEquals(1, rep.getRequiredActions().size());
|
||||
assertEquals(RequiredAction.UPDATE_EMAIL.name(), rep.getRequiredActions().get(0));
|
||||
assertEquals("test-user@localhost", testUser.toRepresentation().getAttributes().get(UserModel.EMAIL_PENDING).get(0));
|
||||
assertNull(testUser.toRepresentation().getEmail());
|
||||
|
||||
// confirm the email and authenticate to the app
|
||||
driver.navigate().to(confirmationLink);
|
||||
infoPage.assertCurrent();
|
||||
infoPage.clickBackToApplicationLink();
|
||||
appPage.assertCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
@ -819,6 +819,22 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||
profile.update();
|
||||
assertNull(user.getFirstAttribute(UserModel.EMAIL_PENDING));
|
||||
|
||||
config.getAttribute(UserModel.EMAIL).getPermissions().setEdit(Set.of(ROLE_ADMIN));
|
||||
provider.setConfiguration(config);
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes, user);
|
||||
profile.update();
|
||||
assertEquals("myemail@foo.bar", user.getFirstAttribute(UserModel.EMAIL));
|
||||
assertNull(user.getFirstAttribute(UserModel.EMAIL_PENDING));
|
||||
assertFalse(profile.getAttributes().getWritable().containsKey(UserModel.EMAIL));
|
||||
user.setEmail(null);
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes, user);
|
||||
profile.update();
|
||||
assertNull(user.getFirstAttribute(UserModel.EMAIL));
|
||||
assertFalse(profile.getAttributes().getWritable().containsKey(UserModel.EMAIL));
|
||||
config.getAttribute(UserModel.EMAIL).getPermissions().setEdit(Set.of(ROLE_USER, ROLE_ADMIN));
|
||||
provider.setConfiguration(config);
|
||||
user.setEmail("myemail@foo.bar");
|
||||
|
||||
profile = provider.create(UserProfileContext.USER_API, attributes, user);
|
||||
profile.update();
|
||||
assertNotNull(user.getFirstAttribute(UserModel.EMAIL_PENDING));
|
||||
@ -2451,6 +2467,52 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmailFieldHiddenWhenEmptyAndReadOnlyWithUpdateEmailEnabled() {
|
||||
ApiUtil.enableRequiredAction(testRealm(), RequiredAction.UPDATE_EMAIL, true);
|
||||
try {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testEmailFieldHiddenWhenEmptyAndReadOnlyWithUpdateEmailEnabled);
|
||||
} finally {
|
||||
ApiUtil.enableRequiredAction(testRealm(), RequiredAction.UPDATE_EMAIL, false);
|
||||
}
|
||||
}
|
||||
|
||||
private static void testEmailFieldHiddenWhenEmptyAndReadOnlyWithUpdateEmailEnabled(KeycloakSession session) {
|
||||
UserProfileProvider provider = getUserProfileProvider(session);
|
||||
String userName = org.keycloak.models.utils.KeycloakModelUtils.generateId();
|
||||
Map<String, String> attributes = new HashMap<>();
|
||||
|
||||
// Enable UPDATE_EMAIL feature
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
realm.getRequiredActionProviderByAlias(UserModel.RequiredAction.UPDATE_EMAIL.name()).setEnabled(true);
|
||||
|
||||
// Create user without email
|
||||
attributes.put(UserModel.USERNAME, userName);
|
||||
attributes.put(UserModel.FIRST_NAME, "John");
|
||||
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||
// Deliberately not setting email to test empty email scenario
|
||||
|
||||
UserProfile profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||
UserModel user = profile.create();
|
||||
|
||||
// Configure email as read-only for users (only admins can edit)
|
||||
UPConfig upConfig = provider.getConfiguration();
|
||||
UPAttribute emailAttr = upConfig.getAttribute(UserModel.EMAIL);
|
||||
if (emailAttr == null) {
|
||||
emailAttr = new UPAttribute(UserModel.EMAIL);
|
||||
upConfig.addOrReplaceAttribute(emailAttr);
|
||||
}
|
||||
emailAttr.setPermissions(new UPAttributePermissions(Set.of(), Set.of("admin")));
|
||||
provider.setConfiguration(upConfig);
|
||||
|
||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, user);
|
||||
Map<String, List<String>> readableAttributes = profile.getAttributes().getReadable();
|
||||
|
||||
// Email should NOT be visible in UPDATE_PROFILE context when empty and read-only
|
||||
assertFalse("Email field should be hidden when empty, read-only, and UPDATE_EMAIL is enabled",
|
||||
readableAttributes.containsKey(UserModel.EMAIL));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultivalued() {
|
||||
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testMultivalued);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user