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 b4332a57cc..9f880e5087 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 @@ + + + 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 6ff027ca7d..6864854ed4 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 242225b1b4..20323705fe 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 b0a5eb4959..05a84c9618 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 922d9c522d..d2a6af16e0 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 @@
- + <#if realm.editUsernameAllowed>*
- + disabled="disabled" value="${(account.username!'')?html}"/>
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 add9daa906..ad3f45963b 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 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 256169288a..92e5db2c38 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 1a1b90d9ba..bdc36103b2 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 @@ -19,6 +19,13 @@ If enabled then username field is hidden from registration form and email is used as username for new user. +
+ +
+ +
+ If enabled, the username field is editable, readonly otherwise. +
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 35b5645119..1dcadd4f1b 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 @@
+ required ng-pattern="/^[^\<\>\\\/]*$/" data-ng-readonly="!editUsername">
+
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 43eaa30eb9..b04d387a3a 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/entities/RealmEntity.java b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java index 7c393bbb33..46e9fa34f9 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/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index e9e4b3650b..38b125df3d 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 e2a0573fd4..bfbfda5f5c 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 26e09bb53e..184b965e88 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 @@ -270,6 +270,16 @@ public class RealmAdapter implements RealmModel { realm.setResetPasswordAllowed(resetPassword); } + @Override + public boolean isEditUsernameAllowed() { + return realm.isEditUsernameAllowed(); + } + + @Override + public void setEditUsernameAllowed(boolean editUsernameAllowed) { + realm.setEditUsernameAllowed(editUsernameAllowed); + } + @Override public PasswordPolicy getPasswordPolicy() { if (passwordPolicy == null) { 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 afb1c2c574..dc5334f3fc 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; @@ -256,6 +254,18 @@ public class RealmAdapter implements RealmModel { updated.setResetPasswordAllowed(resetPasswordAllowed); } + @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(); 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 d93acf6948..115fe177e7 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/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 2350a7a617..3f2a00d492 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 @@ -320,6 +320,17 @@ public class RealmAdapter implements RealmModel { em.flush(); } + @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/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 089cd67039..8f1958b304 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/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 e6674772f6..673477d337 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 @@ -254,6 +254,17 @@ public class RealmAdapter extends AbstractMongoAdapter impleme updateRealm(); } + @Override + public boolean isEditUsernameAllowed() { + return realm.isEditUsernameAllowed(); + } + + @Override + public void setEditUsernameAllowed(boolean editUsernameAllowed) { + realm.setEditUsernameAllowed(editUsernameAllowed); + updateRealm(); + } + @Override public PasswordPolicy getPasswordPolicy() { if (passwordPolicy == null) { 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 026c877637..39f62991d5 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 errors = Validation.validateUpdateProfileForm(formData); + List 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 d6dc0d36b8..55d97b0418 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 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 1a4392b2a0..fefe74ef8d 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 validateUpdateProfileForm(MultivaluedMap formData) { + return validateUpdateProfileForm(null, formData); + } + + public static List validateUpdateProfileForm(RealmModel realm, MultivaluedMap formData) { List 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 b8cf2a8dba..0f1eba124e 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); @@ -393,6 +394,61 @@ public class AccountTest { events.expectAccount(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent(); } + @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(); 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 afee212b59..df62362337 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 1b02d0b122..6cf642728a 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 5b3623df33..42dd464011 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 9a4fb5f113..7637f1d00f 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 1aa7b219c3..18bc795701 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"); }