keycloak-uncached

Merge pull request #1291 from AOEpeople/KEYCLOAK-1305 KEYCLOAK-1305

6/3/2015 6:51:34 AM

Changes

Details

diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.3.0.Beta1.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.3.0.Beta1.xml
index b4332a5..9f880e5 100755
--- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.3.0.Beta1.xml
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.3.0.Beta1.xml
@@ -94,6 +94,9 @@
             <column name="ADMIN_EVENTS_DETAILS_ENABLED" type="BOOLEAN" defaultValueBoolean="false">
                 <constraints nullable="false"/>
             </column>
+            <column name="EDIT_USERNAME_ALLOWED" type="BOOLEAN" defaultValueBoolean="false">
+                <constraints nullable="false"/>
+            </column>
         </addColumn>
         <createTable tableName="CLIENT_SESSION_AUTH_STATUS">
             <column name="AUTHENTICATOR" type="VARCHAR(36)">
diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
index 6ff027c..6864854 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -24,6 +24,7 @@ public class RealmRepresentation {
     protected Boolean rememberMe;
     protected Boolean verifyEmail;
     protected Boolean resetPasswordAllowed;
+    protected Boolean editUsernameAllowed;
 
     protected Boolean userCacheEnabled;
     protected Boolean realmCacheEnabled;
@@ -328,6 +329,14 @@ public class RealmRepresentation {
         this.resetPasswordAllowed = resetPassword;
     }
 
+    public Boolean isEditUsernameAllowed() {
+        return editUsernameAllowed;
+    }
+
+    public void setEditUsernameAllowed(Boolean editUsernameAllowed) {
+        this.editUsernameAllowed = editUsernameAllowed;
+    }
+
     @Deprecated
     public Boolean isSocial() {
         return social;
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountBean.java
index 242225b..2032370 100755
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountBean.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountBean.java
@@ -38,7 +38,7 @@ public class AccountBean {
     }
 
     public String getUsername() {
-        return user.getUsername();
+        return profileFormData != null ? profileFormData.getFirst("username") : user.getUsername();
     }
 
     public String getEmail() {
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/RealmBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/RealmBean.java
index b0a5eb4..05a84c9 100755
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/RealmBean.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/RealmBean.java
@@ -46,4 +46,8 @@ public class RealmBean {
         return realm.getSupportedLocales();
     }
 
+    public boolean isEditUsernameAllowed() {
+        return realm.isEditUsernameAllowed();
+    }
+
 }
diff --git a/forms/common-themes/src/main/resources/theme/base/account/account.ftl b/forms/common-themes/src/main/resources/theme/base/account/account.ftl
index 922d9c5..d2a6af1 100755
--- a/forms/common-themes/src/main/resources/theme/base/account/account.ftl
+++ b/forms/common-themes/src/main/resources/theme/base/account/account.ftl
@@ -16,11 +16,11 @@
 
         <div class="form-group ${messagesPerField.printIfExists('username','has-error')}">
             <div class="col-sm-2 col-md-2">
-                <label for="username" class="control-label">${msg("username")}</label>
+                <label for="username" class="control-label">${msg("username")}</label> <#if realm.editUsernameAllowed><span class="required">*</span></#if>
             </div>
 
             <div class="col-sm-10 col-md-10">
-                <input type="text" class="form-control" id="username" name="username" disabled="disabled" value="${(account.username!'')?html}"/>
+                <input type="text" class="form-control" id="username" name="username" <#if !realm.editUsernameAllowed>disabled="disabled"</#if> value="${(account.username!'')?html}"/>
             </div>
         </div>
 
diff --git a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_de.properties b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_de.properties
index add9daa..ad3f459 100644
--- a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_de.properties
+++ b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_de.properties
@@ -80,7 +80,7 @@ totpStep1=Installieren Sie <a href="https://fedorahosted.org/freeotp/" target="_
 totpStep2=\u00D6ffnen Sie die Applikation und scannen Sie den Barcode oder geben sie den Code ein.
 totpStep3=Geben Sie den One-time Code welcher die Applikation generiert hat ein und klicken Sie auf Speichern.
 
-
+missingUsernameMessage=Bitte geben Sie einen Benutzernamen ein.
 missingFirstNameMessage=Bitte geben Sie einen Vornamen ein.
 missingEmailMessage=Bitte geben Sie eine E-Mail Adresse ein.
 missingLastNameMessage=Bitte geben Sie einen Nachnamen ein.
diff --git a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties
index 7eb971b..f783b49 100755
--- a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties
+++ b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties
@@ -96,6 +96,7 @@ totpStep1=Install <a href="https://fedorahosted.org/freeotp/" target="_blank">Fr
 totpStep2=Open the application and scan the barcode or enter the key.
 totpStep3=Enter the one-time code provided by the application and click Save to finish the setup.
 
+missingUsernameMessage=Please specify username.
 missingFirstNameMessage=Please specify first name.
 invalidEmailMessage=Invalid email address.
 missingLastNameMessage=Please specify last name.
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index 2561692..92e5db2 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -198,6 +198,7 @@ module.controller('UserListCtrl', function($scope, realm, User) {
 module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFederationInstances, $location, Dialog, Notifications) {
     $scope.realm = realm;
     $scope.create = !user.id;
+    $scope.editUsername = $scope.create || $scope.realm.editUsernameAllowed;
 
     if ($scope.create) {
         $scope.user = { enabled: true, attributes: {} }
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html
index 1a1b90d..bdc3610 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html
@@ -20,6 +20,13 @@
                 <kc-tooltip>If enabled then username field is hidden from registration form and email is used as username for new user.</kc-tooltip>
             </div>
             <div class="form-group">
+                <label for="editUsernameAllowed" class="col-md-2 control-label">Edit username</label>
+                <div class="col-md-6">
+                    <input ng-model="realm.editUsernameAllowed" name="editUsernameAllowed" id="editUsernameAllowed" onoffswitch />
+                </div>
+                <kc-tooltip>If enabled, the username field is editable, readonly otherwise.</kc-tooltip>
+            </div>
+            <div class="form-group">
                 <label for="resetPasswordAllowed" class="col-md-2 control-label">Forget password</label>
                 <div class="col-md-6">
                     <input ng-model="realm.resetPasswordAllowed" name="resetPasswordAllowed" id="resetPasswordAllowed" onoffswitch />
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html
index 35b5645..1dcadd4 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html
@@ -25,10 +25,11 @@
                 <div class="col-md-6">
                     <!-- Characters >,<,/,\ are forbidden in username -->
                     <input class="form-control" type="text" id="username" name="username" data-ng-model="user.username" autofocus
-                           required ng-pattern="/^[^\<\>\\\/]*$/" data-ng-readonly="!create">
+                           required ng-pattern="/^[^\<\>\\\/]*$/" data-ng-readonly="!editUsername">
                 </div>
             </div>
 
+
             <div class="form-group">
                 <label class="col-md-2 control-label" for="email">Email</label>
 
diff --git a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
index 7c393bb..46e9fa3 100755
--- a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
@@ -20,6 +20,7 @@ public class RealmEntity extends AbstractIdentifiableEntity {
     private boolean passwordCredentialGrantAllowed;
     private boolean resetPasswordAllowed;
     private String passwordPolicy;
+    private boolean editUsernameAllowed;
     //--- brute force settings
     private boolean bruteForceProtected;
     private int maxFailureWaitSeconds;
@@ -150,6 +151,14 @@ public class RealmEntity extends AbstractIdentifiableEntity {
         this.resetPasswordAllowed = resetPasswordAllowed;
     }
 
+    public boolean isEditUsernameAllowed() {
+        return editUsernameAllowed;
+    }
+
+    public void setEditUsernameAllowed(boolean editUsernameAllowed) {
+        this.editUsernameAllowed = editUsernameAllowed;
+    }
+
     public String getPasswordPolicy() {
         return passwordPolicy;
     }
diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java
index 43eaa30..b04d387 100755
--- a/model/api/src/main/java/org/keycloak/models/RealmModel.java
+++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java
@@ -59,6 +59,10 @@ public interface RealmModel extends RoleContainerModel {
 
     void setRememberMe(boolean rememberMe);
 
+    boolean isEditUsernameAllowed();
+
+    void setEditUsernameAllowed(boolean editUsernameAllowed);
+
     //--- brute force settings
     boolean isBruteForceProtected();
     void setBruteForceProtected(boolean value);
diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index e9e4b36..38b125d 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -124,6 +124,7 @@ public class ModelToRepresentation {
 
         rep.setVerifyEmail(realm.isVerifyEmail());
         rep.setResetPasswordAllowed(realm.isResetPasswordAllowed());
+        rep.setEditUsernameAllowed(realm.isEditUsernameAllowed());
         rep.setAccessTokenLifespan(realm.getAccessTokenLifespan());
         rep.setSsoSessionIdleTimeout(realm.getSsoSessionIdleTimeout());
         rep.setSsoSessionMaxLifespan(realm.getSsoSessionMaxLifespan());
diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index e2a0573..bfbfda5 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -103,6 +103,7 @@ public class RepresentationToModel {
         if (rep.isRememberMe() != null) newRealm.setRememberMe(rep.isRememberMe());
         if (rep.isVerifyEmail() != null) newRealm.setVerifyEmail(rep.isVerifyEmail());
         if (rep.isResetPasswordAllowed() != null) newRealm.setResetPasswordAllowed(rep.isResetPasswordAllowed());
+        if (rep.isEditUsernameAllowed() != null) newRealm.setEditUsernameAllowed(rep.isEditUsernameAllowed());
         if (rep.getPrivateKey() == null || rep.getPublicKey() == null) {
             KeycloakModelUtils.generateRealmKeys(newRealm);
         } else {
@@ -426,6 +427,7 @@ public class RepresentationToModel {
         if (rep.isRememberMe() != null) realm.setRememberMe(rep.isRememberMe());
         if (rep.isVerifyEmail() != null) realm.setVerifyEmail(rep.isVerifyEmail());
         if (rep.isResetPasswordAllowed() != null) realm.setResetPasswordAllowed(rep.isResetPasswordAllowed());
+        if (rep.isEditUsernameAllowed() != null) realm.setEditUsernameAllowed(rep.isEditUsernameAllowed());
         if (rep.getSslRequired() != null) realm.setSslRequired(SslRequired.valueOf(rep.getSslRequired().toUpperCase()));
         if (rep.getAccessCodeLifespan() != null) realm.setAccessCodeLifespan(rep.getAccessCodeLifespan());
         if (rep.getAccessCodeLifespanUserAction() != null) realm.setAccessCodeLifespanUserAction(rep.getAccessCodeLifespanUserAction());
diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java
index 26e09bb..184b965 100755
--- a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java
+++ b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java
@@ -271,6 +271,16 @@ public class RealmAdapter implements RealmModel {
     }
 
     @Override
+    public boolean isEditUsernameAllowed() {
+        return realm.isEditUsernameAllowed();
+    }
+
+    @Override
+    public void setEditUsernameAllowed(boolean editUsernameAllowed) {
+        realm.setEditUsernameAllowed(editUsernameAllowed);
+    }
+
+    @Override
     public PasswordPolicy getPasswordPolicy() {
         if (passwordPolicy == null) {
             passwordPolicy = new PasswordPolicy(realm.getPasswordPolicy());
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
index d93acf6..115fe17 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
@@ -42,6 +42,7 @@ public class CachedRealm {
     private boolean passwordCredentialGrantAllowed;
     private boolean resetPasswordAllowed;
     private boolean identityFederationEnabled;
+    private boolean editUsernameAllowed;
     //--- brute force settings
     private boolean bruteForceProtected;
     private int maxFailureWaitSeconds;
@@ -114,6 +115,7 @@ public class CachedRealm {
         passwordCredentialGrantAllowed = model.isPasswordCredentialGrantAllowed();
         resetPasswordAllowed = model.isResetPasswordAllowed();
         identityFederationEnabled = model.isIdentityFederationEnabled();
+        editUsernameAllowed = model.isEditUsernameAllowed();
         //--- brute force settings
         bruteForceProtected = model.isBruteForceProtected();
         maxFailureWaitSeconds = model.getMaxFailureWaitSeconds();
@@ -288,6 +290,10 @@ public class CachedRealm {
         return resetPasswordAllowed;
     }
 
+    public boolean isEditUsernameAllowed() {
+        return editUsernameAllowed;
+    }
+
     public int getSsoSessionIdleTimeout() {
         return ssoSessionIdleTimeout;
     }
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java
index afb1c2c..dc5334f 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java
@@ -8,14 +8,12 @@ import org.keycloak.models.AuthenticatorModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.IdentityProviderMapperModel;
 import org.keycloak.models.IdentityProviderModel;
-import org.keycloak.models.LDAPConstants;
 import org.keycloak.models.PasswordPolicy;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RequiredCredentialModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserFederationMapperModel;
 import org.keycloak.models.UserFederationProviderModel;
-import org.keycloak.models.UserModel;
 import org.keycloak.models.cache.entities.CachedRealm;
 import org.keycloak.models.utils.KeycloakModelUtils;
 
@@ -257,6 +255,18 @@ public class RealmAdapter implements RealmModel {
     }
 
     @Override
+    public boolean isEditUsernameAllowed() {
+        if (updated != null) return updated.isEditUsernameAllowed();
+        return cached.isEditUsernameAllowed();
+    }
+
+    @Override
+    public void setEditUsernameAllowed(boolean editUsernameAllowed) {
+        getDelegateForUpdate();
+        updated.setEditUsernameAllowed(editUsernameAllowed);
+    }
+
+    @Override
     public int getSsoSessionIdleTimeout() {
         if (updated != null) return updated.getSsoSessionIdleTimeout();
         return cached.getSsoSessionIdleTimeout();
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
index 089cd67..8f1958b 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
@@ -59,6 +59,8 @@ public class RealmEntity {
     protected boolean rememberMe;
     @Column(name="PASSWORD_POLICY")
     protected String passwordPolicy;
+    @Column(name="EDIT_USERNAME_ALLOWED")
+    protected boolean editUsernameAllowed;
 
     @Column(name="SSO_IDLE_TIMEOUT")
     private int ssoSessionIdleTimeout;
@@ -254,6 +256,14 @@ public class RealmEntity {
         this.resetPasswordAllowed = resetPasswordAllowed;
     }
 
+    public boolean isEditUsernameAllowed() {
+        return editUsernameAllowed;
+    }
+
+    public void setEditUsernameAllowed(boolean editUsernameAllowed) {
+        this.editUsernameAllowed = editUsernameAllowed;
+    }
+
     public int getSsoSessionIdleTimeout() {
         return ssoSessionIdleTimeout;
     }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 2350a7a..3f2a00d 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -321,6 +321,17 @@ public class RealmAdapter implements RealmModel {
     }
 
     @Override
+    public boolean isEditUsernameAllowed() {
+        return realm.isEditUsernameAllowed();
+    }
+
+    @Override
+    public void setEditUsernameAllowed(boolean editUsernameAllowed) {
+        realm.setEditUsernameAllowed(editUsernameAllowed);
+        em.flush();
+    }
+
+    @Override
     public int getNotBefore() {
         return realm.getNotBefore();
     }
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index e667477..673477d 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -255,6 +255,17 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
     }
 
     @Override
+    public boolean isEditUsernameAllowed() {
+        return realm.isEditUsernameAllowed();
+    }
+
+    @Override
+    public void setEditUsernameAllowed(boolean editUsernameAllowed) {
+        realm.setEditUsernameAllowed(editUsernameAllowed);
+        updateRealm();
+    }
+
+    @Override
     public PasswordPolicy getPasswordPolicy() {
         if (passwordPolicy == null) {
             passwordPolicy = new PasswordPolicy(realm.getPasswordPolicy());
diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java
index 026c877..39f6299 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -35,23 +35,35 @@ import org.keycloak.events.EventBuilder;
 import org.keycloak.events.EventStoreProvider;
 import org.keycloak.events.EventType;
 import org.keycloak.login.LoginFormsProvider;
-import org.keycloak.models.*;
+import org.keycloak.models.AccountRoles;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.Constants;
+import org.keycloak.models.FederatedIdentityModel;
+import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.ModelReadOnlyException;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserCredentialValueModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.utils.FormMessage;
 import org.keycloak.models.utils.ModelToRepresentation;
 import org.keycloak.models.utils.TimeBasedOTP;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
-import org.keycloak.protocol.oidc.TokenManager;
 import org.keycloak.protocol.oidc.utils.RedirectUtils;
 import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.services.ForbiddenException;
+import org.keycloak.services.Urls;
 import org.keycloak.services.managers.AppAuthManager;
 import org.keycloak.services.managers.Auth;
 import org.keycloak.services.managers.AuthenticationManager;
 import org.keycloak.services.managers.ClientSessionCode;
 import org.keycloak.services.messages.Messages;
-import org.keycloak.services.Urls;
 import org.keycloak.services.util.CookieHelper;
 import org.keycloak.services.util.ResolveRelative;
 import org.keycloak.services.validation.Validation;
@@ -73,7 +85,6 @@ import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriBuilder;
 import javax.ws.rs.core.UriInfo;
 import javax.ws.rs.core.Variant;
-
 import java.lang.reflect.Method;
 import java.net.URI;
 import java.util.HashSet;
@@ -414,13 +425,16 @@ public class AccountService {
 
         UserModel user = auth.getUser();
 
-        List<FormMessage> errors = Validation.validateUpdateProfileForm(formData);
+        List<FormMessage> errors = Validation.validateUpdateProfileForm(realm, formData);
         if (errors != null && !errors.isEmpty()) {
             setReferrerOnPage();
             return account.setErrors(errors).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT);
         }
 
         try {
+            if (realm.isEditUsernameAllowed()) {
+                user.setUsername(formData.getFirst("username"));
+            }
             user.setFirstName(formData.getFirst("firstName"));
             user.setLastName(formData.getFirst("lastName"));
 
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index d6dc0d3..55d97b0 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -186,6 +186,9 @@ public class UsersResource {
     }
 
     private void updateUserFromRep(UserModel user, UserRepresentation rep, Set<String> attrsToRemove) {
+        if (realm.isEditUsernameAllowed()) {
+            user.setUsername(rep.getUsername());
+        }
         user.setEmail(rep.getEmail());
         user.setFirstName(rep.getFirstName());
         user.setLastName(rep.getLastName());
diff --git a/services/src/main/java/org/keycloak/services/validation/Validation.java b/services/src/main/java/org/keycloak/services/validation/Validation.java
index 1a4392b..fefe74e 100755
--- a/services/src/main/java/org/keycloak/services/validation/Validation.java
+++ b/services/src/main/java/org/keycloak/services/validation/Validation.java
@@ -1,17 +1,16 @@
 package org.keycloak.services.validation;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.regex.Pattern;
-
-import javax.ws.rs.core.MultivaluedMap;
-
 import org.keycloak.models.PasswordPolicy;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.utils.FormMessage;
 import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.services.messages.Messages;
 
+import javax.ws.rs.core.MultivaluedMap;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
 public class Validation {
 
     public static final String FIELD_PASSWORD_CONFIRM = "password-confirm";
@@ -66,10 +65,17 @@ public class Validation {
         errors.add(new FormMessage(field, message));
     }
 
-
     public static List<FormMessage> validateUpdateProfileForm(MultivaluedMap<String, String> formData) {
+        return validateUpdateProfileForm(null, formData);
+    }
+
+    public static List<FormMessage> validateUpdateProfileForm(RealmModel realm, MultivaluedMap<String, String> formData) {
         List<FormMessage> errors = new ArrayList<>();
         
+        if (realm != null && realm.isEditUsernameAllowed() && isEmpty(formData.getFirst(FIELD_USERNAME))) {
+            addError(errors, FIELD_USERNAME, Messages.MISSING_USERNAME);
+        }
+
         if (isEmpty(formData.getFirst(FIELD_FIRST_NAME))) {
             addError(errors, FIELD_FIRST_NAME, Messages.MISSING_FIRST_NAME);
         }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
index b8cf2a8..0f1eba1 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
@@ -24,11 +24,9 @@ package org.keycloak.testsuite.account;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
-import org.keycloak.account.freemarker.model.ApplicationsBean;
 import org.keycloak.events.Details;
 import org.keycloak.events.Event;
 import org.keycloak.events.EventType;
@@ -155,6 +153,9 @@ public class AccountTest {
             @Override
             public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) {
                 UserModel user = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm);
+                user.setFirstName("Tom");
+                user.setLastName("Brady");
+                user.setEmail("test-user@localhost");
 
                 UserCredentialModel cred = new UserCredentialModel();
                 cred.setType(CredentialRepresentation.PASSWORD);
@@ -394,6 +395,61 @@ public class AccountTest {
     }
 
     @Test
+    public void changeUsername() {
+        // allow to edit the username in realm
+        keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+            @Override
+            public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                appRealm.setEditUsernameAllowed(true);
+            }
+        });
+
+        try {
+            profilePage.open();
+            loginPage.login("test-user@localhost", "password");
+
+            events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT).assertEvent();
+
+            Assert.assertEquals("test-user@localhost", profilePage.getUsername());
+            Assert.assertEquals("Tom", profilePage.getFirstName());
+            Assert.assertEquals("Brady", profilePage.getLastName());
+            Assert.assertEquals("test-user@localhost", profilePage.getEmail());
+
+            // All fields are required, so there should be an error when something is missing.
+            profilePage.updateProfile("", "New first", "New last", "new@email.com");
+
+            Assert.assertEquals("Please specify username.", profilePage.getError());
+            Assert.assertEquals("", profilePage.getUsername());
+            Assert.assertEquals("New first", profilePage.getFirstName());
+            Assert.assertEquals("New last", profilePage.getLastName());
+            Assert.assertEquals("new@email.com", profilePage.getEmail());
+
+            events.assertEmpty();
+
+            profilePage.updateProfile("test-user-new@localhost", "New first", "New last", "new@email.com");
+
+            Assert.assertEquals("Your account has been updated.", profilePage.getSuccess());
+            Assert.assertEquals("test-user-new@localhost", profilePage.getUsername());
+            Assert.assertEquals("New first", profilePage.getFirstName());
+            Assert.assertEquals("New last", profilePage.getLastName());
+            Assert.assertEquals("new@email.com", profilePage.getEmail());
+
+        } finally {
+            // reset user for other tests
+            profilePage.updateProfile("test-user@localhost", "Tom", "Brady", "test-user@localhost");
+            events.clear();
+
+            // reset realm
+            keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+                @Override
+                public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                    appRealm.setEditUsernameAllowed(false);
+                }
+            });
+        }
+    }
+
+    @Test
     public void setupTotp() {
         totpPage.open();
         loginPage.login("test-user@localhost", "password");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java
index afee212..df62362 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AdminAPITest.java
@@ -242,6 +242,7 @@ public class AdminAPITest {
         if (rep.isRememberMe() != null) Assert.assertEquals(rep.isRememberMe(), storedRealm.isRememberMe());
         if (rep.isVerifyEmail() != null) Assert.assertEquals(rep.isVerifyEmail(), storedRealm.isVerifyEmail());
         if (rep.isResetPasswordAllowed() != null) Assert.assertEquals(rep.isResetPasswordAllowed(), storedRealm.isResetPasswordAllowed());
+        if (rep.isEditUsernameAllowed() != null) Assert.assertEquals(rep.isEditUsernameAllowed(), storedRealm.isEditUsernameAllowed());
         if (rep.getSslRequired() != null) Assert.assertEquals(rep.getSslRequired(), storedRealm.getSslRequired());
         if (rep.getAccessCodeLifespan() != null) Assert.assertEquals(rep.getAccessCodeLifespan(), storedRealm.getAccessCodeLifespan());
         if (rep.getAccessCodeLifespanUserAction() != null)
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java
index 1b02d0b..6cf6427 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/RealmTest.java
@@ -71,6 +71,7 @@ public class RealmTest extends AbstractClientTest {
         rep.setAccessCodeLifespanLogin(1234);
         rep.setRegistrationAllowed(true);
         rep.setRegistrationEmailAsUsername(true);
+        rep.setEditUsernameAllowed(true);
 
         realm.update(rep);
 
@@ -81,16 +82,19 @@ public class RealmTest extends AbstractClientTest {
         assertEquals(1234, rep.getAccessCodeLifespanLogin().intValue());
         assertEquals(Boolean.TRUE, rep.isRegistrationAllowed());
         assertEquals(Boolean.TRUE, rep.isRegistrationEmailAsUsername());
+        assertEquals(Boolean.TRUE, rep.isEditUsernameAllowed());
 
         // second change
         rep.setRegistrationAllowed(false);
         rep.setRegistrationEmailAsUsername(false);
+        rep.setEditUsernameAllowed(false);
 
         realm.update(rep);
 
         rep = realm.toRepresentation();
         assertEquals(Boolean.FALSE, rep.isRegistrationAllowed());
         assertEquals(Boolean.FALSE, rep.isRegistrationEmailAsUsername());
+        assertEquals(Boolean.FALSE, rep.isEditUsernameAllowed());
     }
 
     @Test
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java
index 5b3623d..42dd464 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java
@@ -1,13 +1,13 @@
 package org.keycloak.testsuite.admin;
 
 import org.junit.Assert;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.keycloak.admin.client.resource.IdentityProviderResource;
 import org.keycloak.admin.client.resource.UserResource;
 import org.keycloak.representations.idm.ErrorRepresentation;
 import org.keycloak.representations.idm.FederatedIdentityRepresentation;
 import org.keycloak.representations.idm.IdentityProviderRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
 
 import javax.ws.rs.ClientErrorException;
@@ -410,4 +410,78 @@ public class UserTest extends AbstractClientTest {
             Assert.assertEquals("invalidClientId not enabled", error.getErrorMessage());
         }
     }
+
+    @Test
+    public void updateUserWithNewUsername() {
+        switchEditUsernameAllowedOn();
+        String id = createUser();
+
+        UserResource user = realm.users().get(id);
+        UserRepresentation userRep = user.toRepresentation();
+        userRep.setUsername("user11");
+        user.update(userRep);
+
+        userRep = realm.users().get(id).toRepresentation();
+        assertEquals("user11", userRep.getUsername());
+    }
+
+    @Test
+    public void updateUserWithNewUsernameNotPossible() {
+        String id = createUser();
+
+        UserResource user = realm.users().get(id);
+        UserRepresentation userRep = user.toRepresentation();
+        userRep.setUsername("user11");
+        user.update(userRep);
+
+        userRep = realm.users().get(id).toRepresentation();
+        assertEquals("user1", userRep.getUsername());
+    }
+
+    @Test
+    public void updateUserWithNewUsernameAccessingViaOldUsername() {
+        switchEditUsernameAllowedOn();
+        createUser();
+
+        try {
+            UserResource user = realm.users().get("user1");
+            UserRepresentation userRep = user.toRepresentation();
+            userRep.setUsername("user1");
+            user.update(userRep);
+
+            realm.users().get("user11").toRepresentation();
+            fail("Expected failure");
+        } catch (ClientErrorException e) {
+            assertEquals(404, e.getResponse().getStatus());
+        }
+    }
+
+    @Test
+    public void updateUserWithExistingUsername() {
+        switchEditUsernameAllowedOn();
+        createUser();
+
+        UserRepresentation userRep = new UserRepresentation();
+        userRep.setUsername("user2");
+        Response response = realm.users().create(userRep);
+        String createdId = ApiUtil.getCreatedId(response);
+        response.close();
+
+        try {
+            UserResource user = realm.users().get(createdId);
+            userRep = user.toRepresentation();
+            userRep.setUsername("user1");
+            user.update(userRep);
+            fail("Expected failure");
+        } catch (ClientErrorException e) {
+            assertEquals(409, e.getResponse().getStatus());
+        }
+    }
+
+    private void switchEditUsernameAllowedOn() {
+        RealmRepresentation rep = realm.toRepresentation();
+        rep.setEditUsernameAllowed(true);
+        realm.update(rep);
+    }
+
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ModelTest.java
index 9a4fb5f..7637f1d 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ModelTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ModelTest.java
@@ -21,6 +21,7 @@ public class ModelTest extends AbstractModelTest {
         realm.setRegistrationAllowed(true);
         realm.setRegistrationEmailAsUsername(true);
         realm.setResetPasswordAllowed(true);
+        realm.setEditUsernameAllowed(true);
         realm.setSslRequired(SslRequired.EXTERNAL);
         realm.setVerifyEmail(true);
         realm.setAccessTokenLifespan(1000);
@@ -55,6 +56,7 @@ public class ModelTest extends AbstractModelTest {
         Assert.assertEquals(expected.isRegistrationAllowed(), actual.isRegistrationAllowed());
         Assert.assertEquals(expected.isRegistrationEmailAsUsername(), actual.isRegistrationEmailAsUsername());
         Assert.assertEquals(expected.isResetPasswordAllowed(), actual.isResetPasswordAllowed());
+        Assert.assertEquals(expected.isEditUsernameAllowed(), actual.isEditUsernameAllowed());
         Assert.assertEquals(expected.getSslRequired(), actual.getSslRequired());
         Assert.assertEquals(expected.isVerifyEmail(), actual.isVerifyEmail());
         Assert.assertEquals(expected.getAccessTokenLifespan(), actual.getAccessTokenLifespan());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java
index 1aa7b21..18bc795 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountUpdateProfilePage.java
@@ -35,6 +35,9 @@ public class AccountUpdateProfilePage extends AbstractAccountPage {
 
     public static String PATH = RealmsResource.accountUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT)).build("test").toString();
 
+    @FindBy(id = "username")
+    private WebElement usernameInput;
+
     @FindBy(id = "firstName")
     private WebElement firstNameInput;
 
@@ -74,11 +77,28 @@ public class AccountUpdateProfilePage extends AbstractAccountPage {
         submitButton.click();
     }
 
+    public void updateProfile(String username, String firstName, String lastName, String email) {
+        usernameInput.clear();
+        usernameInput.sendKeys(username);
+        firstNameInput.clear();
+        firstNameInput.sendKeys(firstName);
+        lastNameInput.clear();
+        lastNameInput.sendKeys(lastName);
+        emailInput.clear();
+        emailInput.sendKeys(email);
+
+        submitButton.click();
+    }
+
     public void clickCancel() {
         cancelButton.click();
     }
 
 
+    public String getUsername() {
+        return usernameInput.getAttribute("value");
+    }
+
     public String getFirstName() {
         return firstNameInput.getAttribute("value");
     }