keycloak-aplcache

Merge pull request #1633 from stianst/update-username KEYCLOAK-1857

9/23/2015 3:26:03 AM

Details

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 2257508..584bea3 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">
         <form id="kc-update-profile-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
+            <#if realm.editUsernameAllowed>
+                <div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('username',properties.kcFormGroupErrorClass!)}">
+                    <div class="${properties.kcLabelWrapperClass!}">
+                        <label for="username" class="${properties.kcLabelClass!}">${msg("username")}</label>
+                    </div>
+                    <div class="${properties.kcInputWrapperClass!}">
+                        <input type="text" id="username" name="username" value="${(user.username!'')?html}" class="${properties.kcInputClass!}"/>
+                    </div>
+                </div>
+            </#if>
             <div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('email',properties.kcFormGroupErrorClass!)}">
                 <div class="${properties.kcLabelWrapperClass!}">
                     <label for="email" class="${properties.kcLabelClass!}">${msg("email")}</label>
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 6f73cc5..e730c14 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 b161ad2..e6ae21d 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 42c2e02..9630f3b 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<FormMessage> errors = Validation.validateUpdateProfileForm(formData);
+        List<FormMessage> 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 868a76c..47b4e1d 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 e3d6ac9..492cd4b 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();
 
@@ -192,6 +225,52 @@ public class RequiredActionUpdateProfileTest {
     }
 
     @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 fb2b5df..d7084c1 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 0000000..bba3f93
--- /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 8ad29cc..5344ad0 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" : [