keycloak-uncached
Changes
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountBean.java 2(+1 -1)
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/RealmBean.java 4(+4 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-login-settings.html 7(+7 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html 3(+2 -1)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java 6(+6 -0)
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");
}