diff --git a/forms/common-themes/src/main/resources/theme/base/login/login-update-profile.ftl b/forms/common-themes/src/main/resources/theme/base/login/login-update-profile.ftl index 22575087ff..584bea322b 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/login-update-profile.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/login-update-profile.ftl @@ -6,6 +6,16 @@ ${msg("loginProfileTitle")} <#elseif section = "form">
+ <#if realm.editUsernameAllowed> +
+
+ +
+
+ +
+
+
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ProfileBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ProfileBean.java index 6f73cc5878..e730c1470c 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ProfileBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/ProfileBean.java @@ -70,6 +70,8 @@ public class ProfileBean { } + public String getUsername() { return formData != null ? formData.getFirst("username") : user.getUsername(); } + public String getFirstName() { return formData != null ? formData.getFirst("firstName") : user.getFirstName(); } diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java index b161ad21fd..e6ae21de98 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RealmBean.java @@ -66,6 +66,10 @@ public class RealmBean { return realm.isInternationalizationEnabled(); } + public boolean isEditUsernameAllowed() { + return realm.isEditUsernameAllowed(); + } + public boolean isPassword() { for (RequiredCredentialModel r : realm.getRequiredCredentials()) { if (r.getType().equals(CredentialRepresentation.PASSWORD)) { 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 42c2e02348..9630f3b10a 100755 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java @@ -52,7 +52,7 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact RealmModel realm = context.getRealm(); - List errors = Validation.validateUpdateProfileForm(formData); + List errors = Validation.validateUpdateProfileForm(realm, formData); if (errors != null && !errors.isEmpty()) { Response challenge = context.form() .setErrors(errors) @@ -62,6 +62,28 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact return; } + if (realm.isEditUsernameAllowed()) { + String username = formData.getFirst("username"); + String oldUsername = user.getUsername(); + + boolean usernameChanged = oldUsername != null ? !oldUsername.equals(username) : username != null; + + if (usernameChanged) { + + if (session.users().getUserByUsername(username, realm) != null) { + Response challenge = context.form() + .setError(Messages.USERNAME_EXISTS) + .setFormData(formData) + .createResponse(UserModel.RequiredAction.UPDATE_PROFILE); + context.challenge(challenge); + return; + } + + user.setUsername(username); + } + + } + user.setFirstName(formData.getFirst("firstName")); user.setLastName(formData.getFirst("lastName")); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java index 868a76cc50..47b4e1d866 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java @@ -33,11 +33,8 @@ import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; -import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.*; import org.keycloak.testsuite.pages.AppPage.RequestType; -import org.keycloak.testsuite.pages.LoginPage; -import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; -import org.keycloak.testsuite.pages.LoginUpdateProfilePage; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup; import org.keycloak.testsuite.rule.WebResource; @@ -83,7 +80,7 @@ public class RequiredActionMultipleActionsTest { protected LoginPasswordUpdatePage changePasswordPage; @WebResource - protected LoginUpdateProfilePage updateProfilePage; + protected LoginUpdateProfileEditUsernameAllowedPage updateProfilePage; @Test public void updateProfileAndPassword() throws Exception { @@ -121,7 +118,7 @@ public class RequiredActionMultipleActionsTest { } public String updateProfile(String sessionId) { - updateProfilePage.update("New first", "New last", "new@email.com"); + updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost"); AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com"); if (sessionId != null) { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java index e3d6ac921d..492cd4bb13 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java @@ -21,11 +21,7 @@ */ package org.keycloak.testsuite.actions; -import org.junit.Assert; -import org.junit.Before; -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.Test; +import org.junit.*; import org.keycloak.events.Details; import org.keycloak.events.EventType; import org.keycloak.models.RealmModel; @@ -36,7 +32,7 @@ import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.LoginPage; -import org.keycloak.testsuite.pages.LoginUpdateProfilePage; +import org.keycloak.testsuite.pages.LoginUpdateProfileEditUsernameAllowedPage; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebRule; @@ -66,7 +62,7 @@ public class RequiredActionUpdateProfileTest { protected LoginPage loginPage; @WebResource - protected LoginUpdateProfilePage updateProfilePage; + protected LoginUpdateProfileEditUsernameAllowedPage updateProfilePage; @Before public void before() { @@ -75,6 +71,8 @@ public class RequiredActionUpdateProfileTest { public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) { UserModel user = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm); user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE); + UserModel anotherUser = manager.getSession().users().getUserByEmail("john-doh@localhost", appRealm); + anotherUser.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE); } }); } @@ -87,7 +85,7 @@ public class RequiredActionUpdateProfileTest { updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "new@email.com"); + updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost"); String sessionId = events.expectRequiredAction(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent().getSessionId(); events.expectRequiredAction(EventType.UPDATE_PROFILE).session(sessionId).assertEvent(); @@ -101,6 +99,41 @@ public class RequiredActionUpdateProfileTest { Assert.assertEquals("New first", user.getFirstName()); Assert.assertEquals("New last", user.getLastName()); Assert.assertEquals("new@email.com", user.getEmail()); + Assert.assertEquals("test-user@localhost", user.getUsername()); + } + + @Test + public void updateUsername() { + loginPage.open(); + + loginPage.login("john-doh@localhost", "password"); + + String userId = keycloakRule.getUser("test", "john-doh@localhost").getId(); + + updateProfilePage.assertCurrent(); + + updateProfilePage.update("New first", "New last", "john-doh@localhost", "new"); + + String sessionId = events + .expectLogin() + .event(EventType.UPDATE_PROFILE) + .detail(Details.USERNAME, "john-doh@localhost") + .user(userId) + .session(AssertEvents.isUUID()) + .removeDetail(Details.CONSENT) + .assertEvent() + .getSessionId(); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + events.expectLogin().detail(Details.USERNAME, "john-doh@localhost").user(userId).session(sessionId).assertEvent(); + + // assert user is really updated in persistent store + UserRepresentation user = keycloakRule.getUser("test", "new"); + Assert.assertEquals("New first", user.getFirstName()); + Assert.assertEquals("New last", user.getLastName()); + Assert.assertEquals("john-doh@localhost", user.getEmail()); + Assert.assertEquals("new", user.getUsername()); } @Test @@ -111,7 +144,7 @@ public class RequiredActionUpdateProfileTest { updateProfilePage.assertCurrent(); - updateProfilePage.update("", "New last", "new@email.com"); + updateProfilePage.update("", "New last", "new@email.com", "new"); updateProfilePage.assertCurrent(); @@ -133,7 +166,7 @@ public class RequiredActionUpdateProfileTest { updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "", "new@email.com"); + updateProfilePage.update("New first", "", "new@email.com", "new"); updateProfilePage.assertCurrent(); @@ -155,7 +188,7 @@ public class RequiredActionUpdateProfileTest { updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", ""); + updateProfilePage.update("New first", "New last", "", "new"); updateProfilePage.assertCurrent(); @@ -177,7 +210,7 @@ public class RequiredActionUpdateProfileTest { updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "invalidemail"); + updateProfilePage.update("New first", "New last", "invalidemail", "invalid"); updateProfilePage.assertCurrent(); @@ -191,6 +224,52 @@ public class RequiredActionUpdateProfileTest { events.assertEmpty(); } + @Test + public void updateProfileMissingUsername() { + loginPage.open(); + + loginPage.login("john-doh@localhost", "password"); + + updateProfilePage.assertCurrent(); + + updateProfilePage.update("New first", "New last", "new@email.com", ""); + + updateProfilePage.assertCurrent(); + + // assert that form holds submitted values during validation error + Assert.assertEquals("New first", updateProfilePage.getFirstName()); + Assert.assertEquals("New last", updateProfilePage.getLastName()); + Assert.assertEquals("new@email.com", updateProfilePage.getEmail()); + Assert.assertEquals("", updateProfilePage.getUsername()); + + Assert.assertEquals("Please specify username.", updateProfilePage.getError()); + + events.assertEmpty(); + } + + @Test + public void updateProfileDuplicateUsername() { + loginPage.open(); + + loginPage.login("john-doh@localhost", "password"); + + updateProfilePage.assertCurrent(); + + updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost"); + + updateProfilePage.assertCurrent(); + + // assert that form holds submitted values during validation error + Assert.assertEquals("New first", updateProfilePage.getFirstName()); + Assert.assertEquals("New last", updateProfilePage.getLastName()); + Assert.assertEquals("new@email.com", updateProfilePage.getEmail()); + Assert.assertEquals("test-user@localhost", updateProfilePage.getUsername()); + + Assert.assertEquals("Username already exists.", updateProfilePage.getError()); + + events.assertEmpty(); + } + @Test public void updateProfileDuplicatedEmail() { loginPage.open(); @@ -199,7 +278,7 @@ public class RequiredActionUpdateProfileTest { updateProfilePage.assertCurrent(); - updateProfilePage.update("New first", "New last", "keycloak-user@localhost"); + updateProfilePage.update("New first", "New last", "keycloak-user@localhost", "test-user@localhost"); updateProfilePage.assertCurrent(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java index fb2b5dfd4b..d7084c15b0 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java @@ -17,7 +17,6 @@ */ package org.keycloak.testsuite.broker; -import org.codehaus.jackson.map.ObjectMapper; import org.junit.After; import org.junit.Assert; import org.junit.Before; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfileEditUsernameAllowedPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfileEditUsernameAllowedPage.java new file mode 100644 index 0000000000..bba3f93787 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfileEditUsernameAllowedPage.java @@ -0,0 +1,51 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2012, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.keycloak.testsuite.pages; + +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +public class LoginUpdateProfileEditUsernameAllowedPage extends LoginUpdateProfilePage { + + @FindBy(id = "username") + private WebElement usernameInput; + + public void update(String firstName, String lastName, String email, String username) { + usernameInput.clear(); + usernameInput.sendKeys(username); + update(firstName, lastName, email); + } + + public String getUsername() { + return usernameInput.getAttribute("value"); + } + + public boolean isCurrent() { + return driver.getTitle().equals("Update Account Information"); + } + + @Override + public void open() { + throw new UnsupportedOperationException(); + } + +} diff --git a/testsuite/integration/src/test/resources/testrealm.json b/testsuite/integration/src/test/resources/testrealm.json index 8ad29ccfcf..5344ad0e74 100755 --- a/testsuite/integration/src/test/resources/testrealm.json +++ b/testsuite/integration/src/test/resources/testrealm.json @@ -5,6 +5,7 @@ "sslRequired": "external", "registrationAllowed": true, "resetPasswordAllowed": true, + "editUsernameAllowed" : true, "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "requiredCredentials": [ "password" ], @@ -32,7 +33,23 @@ } }, { - "username" : "keycloak-user@localhost", + "username" : "john-doh@localhost", + "enabled": true, + "email" : "john-doh@localhost", + "firstName": "John", + "lastName": "Doh", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "realmRoles": ["user"], + "clientRoles": { + "test-app": [ "customer-user" ], + "account": [ "view-profile", "manage-account" ] + } + }, + { + "username" : "keycloak-user@localhost", "enabled": true, "email" : "keycloak-user@localhost", "credentials" : [