Merge pull request #1291 from AOEpeople/KEYCLOAK-1305

KEYCLOAK-1305 Add possibility to change username
This commit is contained in:
Stian Thorgersen 2015-06-03 10:51:34 +01:00
commit 3d3871b0e6
29 changed files with 302 additions and 22 deletions

View file

@ -94,6 +94,9 @@
<column name="ADMIN_EVENTS_DETAILS_ENABLED" type="BOOLEAN" defaultValueBoolean="false"> <column name="ADMIN_EVENTS_DETAILS_ENABLED" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/> <constraints nullable="false"/>
</column> </column>
<column name="EDIT_USERNAME_ALLOWED" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn> </addColumn>
<createTable tableName="CLIENT_SESSION_AUTH_STATUS"> <createTable tableName="CLIENT_SESSION_AUTH_STATUS">
<column name="AUTHENTICATOR" type="VARCHAR(36)"> <column name="AUTHENTICATOR" type="VARCHAR(36)">

View file

@ -24,6 +24,7 @@ public class RealmRepresentation {
protected Boolean rememberMe; protected Boolean rememberMe;
protected Boolean verifyEmail; protected Boolean verifyEmail;
protected Boolean resetPasswordAllowed; protected Boolean resetPasswordAllowed;
protected Boolean editUsernameAllowed;
protected Boolean userCacheEnabled; protected Boolean userCacheEnabled;
protected Boolean realmCacheEnabled; protected Boolean realmCacheEnabled;
@ -328,6 +329,14 @@ public class RealmRepresentation {
this.resetPasswordAllowed = resetPassword; this.resetPasswordAllowed = resetPassword;
} }
public Boolean isEditUsernameAllowed() {
return editUsernameAllowed;
}
public void setEditUsernameAllowed(Boolean editUsernameAllowed) {
this.editUsernameAllowed = editUsernameAllowed;
}
@Deprecated @Deprecated
public Boolean isSocial() { public Boolean isSocial() {
return social; return social;

View file

@ -38,7 +38,7 @@ public class AccountBean {
} }
public String getUsername() { public String getUsername() {
return user.getUsername(); return profileFormData != null ? profileFormData.getFirst("username") : user.getUsername();
} }
public String getEmail() { public String getEmail() {

View file

@ -46,4 +46,8 @@ public class RealmBean {
return realm.getSupportedLocales(); return realm.getSupportedLocales();
} }
public boolean isEditUsernameAllowed() {
return realm.isEditUsernameAllowed();
}
} }

View file

@ -16,11 +16,11 @@
<div class="form-group ${messagesPerField.printIfExists('username','has-error')}"> <div class="form-group ${messagesPerField.printIfExists('username','has-error')}">
<div class="col-sm-2 col-md-2"> <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>
<div class="col-sm-10 col-md-10"> <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>
</div> </div>

View file

@ -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. 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. 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. missingFirstNameMessage=Bitte geben Sie einen Vornamen ein.
missingEmailMessage=Bitte geben Sie eine E-Mail Adresse ein. missingEmailMessage=Bitte geben Sie eine E-Mail Adresse ein.
missingLastNameMessage=Bitte geben Sie einen Nachnamen ein. missingLastNameMessage=Bitte geben Sie einen Nachnamen ein.

View file

@ -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. 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. 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. missingFirstNameMessage=Please specify first name.
invalidEmailMessage=Invalid email address. invalidEmailMessage=Invalid email address.
missingLastNameMessage=Please specify last name. missingLastNameMessage=Please specify last name.

View file

@ -198,6 +198,7 @@ module.controller('UserListCtrl', function($scope, realm, User) {
module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFederationInstances, $location, Dialog, Notifications) { module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFederationInstances, $location, Dialog, Notifications) {
$scope.realm = realm; $scope.realm = realm;
$scope.create = !user.id; $scope.create = !user.id;
$scope.editUsername = $scope.create || $scope.realm.editUsernameAllowed;
if ($scope.create) { if ($scope.create) {
$scope.user = { enabled: true, attributes: {} } $scope.user = { enabled: true, attributes: {} }

View file

@ -19,6 +19,13 @@
</div> </div>
<kc-tooltip>If enabled then username field is hidden from registration form and email is used as username for new user.</kc-tooltip> <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>
<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"> <div class="form-group">
<label for="resetPasswordAllowed" class="col-md-2 control-label">Forget password</label> <label for="resetPasswordAllowed" class="col-md-2 control-label">Forget password</label>
<div class="col-md-6"> <div class="col-md-6">

View file

@ -25,10 +25,11 @@
<div class="col-md-6"> <div class="col-md-6">
<!-- Characters >,<,/,\ are forbidden in username --> <!-- Characters >,<,/,\ are forbidden in username -->
<input class="form-control" type="text" id="username" name="username" data-ng-model="user.username" autofocus <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> </div>
<div class="form-group"> <div class="form-group">
<label class="col-md-2 control-label" for="email">Email</label> <label class="col-md-2 control-label" for="email">Email</label>

View file

@ -59,6 +59,10 @@ public interface RealmModel extends RoleContainerModel {
void setRememberMe(boolean rememberMe); void setRememberMe(boolean rememberMe);
boolean isEditUsernameAllowed();
void setEditUsernameAllowed(boolean editUsernameAllowed);
//--- brute force settings //--- brute force settings
boolean isBruteForceProtected(); boolean isBruteForceProtected();
void setBruteForceProtected(boolean value); void setBruteForceProtected(boolean value);

View file

@ -20,6 +20,7 @@ public class RealmEntity extends AbstractIdentifiableEntity {
private boolean passwordCredentialGrantAllowed; private boolean passwordCredentialGrantAllowed;
private boolean resetPasswordAllowed; private boolean resetPasswordAllowed;
private String passwordPolicy; private String passwordPolicy;
private boolean editUsernameAllowed;
//--- brute force settings //--- brute force settings
private boolean bruteForceProtected; private boolean bruteForceProtected;
private int maxFailureWaitSeconds; private int maxFailureWaitSeconds;
@ -150,6 +151,14 @@ public class RealmEntity extends AbstractIdentifiableEntity {
this.resetPasswordAllowed = resetPasswordAllowed; this.resetPasswordAllowed = resetPasswordAllowed;
} }
public boolean isEditUsernameAllowed() {
return editUsernameAllowed;
}
public void setEditUsernameAllowed(boolean editUsernameAllowed) {
this.editUsernameAllowed = editUsernameAllowed;
}
public String getPasswordPolicy() { public String getPasswordPolicy() {
return passwordPolicy; return passwordPolicy;
} }

View file

@ -124,6 +124,7 @@ public class ModelToRepresentation {
rep.setVerifyEmail(realm.isVerifyEmail()); rep.setVerifyEmail(realm.isVerifyEmail());
rep.setResetPasswordAllowed(realm.isResetPasswordAllowed()); rep.setResetPasswordAllowed(realm.isResetPasswordAllowed());
rep.setEditUsernameAllowed(realm.isEditUsernameAllowed());
rep.setAccessTokenLifespan(realm.getAccessTokenLifespan()); rep.setAccessTokenLifespan(realm.getAccessTokenLifespan());
rep.setSsoSessionIdleTimeout(realm.getSsoSessionIdleTimeout()); rep.setSsoSessionIdleTimeout(realm.getSsoSessionIdleTimeout());
rep.setSsoSessionMaxLifespan(realm.getSsoSessionMaxLifespan()); rep.setSsoSessionMaxLifespan(realm.getSsoSessionMaxLifespan());

View file

@ -103,6 +103,7 @@ public class RepresentationToModel {
if (rep.isRememberMe() != null) newRealm.setRememberMe(rep.isRememberMe()); if (rep.isRememberMe() != null) newRealm.setRememberMe(rep.isRememberMe());
if (rep.isVerifyEmail() != null) newRealm.setVerifyEmail(rep.isVerifyEmail()); if (rep.isVerifyEmail() != null) newRealm.setVerifyEmail(rep.isVerifyEmail());
if (rep.isResetPasswordAllowed() != null) newRealm.setResetPasswordAllowed(rep.isResetPasswordAllowed()); if (rep.isResetPasswordAllowed() != null) newRealm.setResetPasswordAllowed(rep.isResetPasswordAllowed());
if (rep.isEditUsernameAllowed() != null) newRealm.setEditUsernameAllowed(rep.isEditUsernameAllowed());
if (rep.getPrivateKey() == null || rep.getPublicKey() == null) { if (rep.getPrivateKey() == null || rep.getPublicKey() == null) {
KeycloakModelUtils.generateRealmKeys(newRealm); KeycloakModelUtils.generateRealmKeys(newRealm);
} else { } else {
@ -426,6 +427,7 @@ public class RepresentationToModel {
if (rep.isRememberMe() != null) realm.setRememberMe(rep.isRememberMe()); if (rep.isRememberMe() != null) realm.setRememberMe(rep.isRememberMe());
if (rep.isVerifyEmail() != null) realm.setVerifyEmail(rep.isVerifyEmail()); if (rep.isVerifyEmail() != null) realm.setVerifyEmail(rep.isVerifyEmail());
if (rep.isResetPasswordAllowed() != null) realm.setResetPasswordAllowed(rep.isResetPasswordAllowed()); 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.getSslRequired() != null) realm.setSslRequired(SslRequired.valueOf(rep.getSslRequired().toUpperCase()));
if (rep.getAccessCodeLifespan() != null) realm.setAccessCodeLifespan(rep.getAccessCodeLifespan()); if (rep.getAccessCodeLifespan() != null) realm.setAccessCodeLifespan(rep.getAccessCodeLifespan());
if (rep.getAccessCodeLifespanUserAction() != null) realm.setAccessCodeLifespanUserAction(rep.getAccessCodeLifespanUserAction()); if (rep.getAccessCodeLifespanUserAction() != null) realm.setAccessCodeLifespanUserAction(rep.getAccessCodeLifespanUserAction());

View file

@ -270,6 +270,16 @@ public class RealmAdapter implements RealmModel {
realm.setResetPasswordAllowed(resetPassword); realm.setResetPasswordAllowed(resetPassword);
} }
@Override
public boolean isEditUsernameAllowed() {
return realm.isEditUsernameAllowed();
}
@Override
public void setEditUsernameAllowed(boolean editUsernameAllowed) {
realm.setEditUsernameAllowed(editUsernameAllowed);
}
@Override @Override
public PasswordPolicy getPasswordPolicy() { public PasswordPolicy getPasswordPolicy() {
if (passwordPolicy == null) { if (passwordPolicy == null) {

View file

@ -8,14 +8,12 @@ import org.keycloak.models.AuthenticatorModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.PasswordPolicy; import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredCredentialModel; import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationMapperModel;
import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.cache.entities.CachedRealm; import org.keycloak.models.cache.entities.CachedRealm;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
@ -256,6 +254,18 @@ public class RealmAdapter implements RealmModel {
updated.setResetPasswordAllowed(resetPasswordAllowed); 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 @Override
public int getSsoSessionIdleTimeout() { public int getSsoSessionIdleTimeout() {
if (updated != null) return updated.getSsoSessionIdleTimeout(); if (updated != null) return updated.getSsoSessionIdleTimeout();

View file

@ -42,6 +42,7 @@ public class CachedRealm {
private boolean passwordCredentialGrantAllowed; private boolean passwordCredentialGrantAllowed;
private boolean resetPasswordAllowed; private boolean resetPasswordAllowed;
private boolean identityFederationEnabled; private boolean identityFederationEnabled;
private boolean editUsernameAllowed;
//--- brute force settings //--- brute force settings
private boolean bruteForceProtected; private boolean bruteForceProtected;
private int maxFailureWaitSeconds; private int maxFailureWaitSeconds;
@ -114,6 +115,7 @@ public class CachedRealm {
passwordCredentialGrantAllowed = model.isPasswordCredentialGrantAllowed(); passwordCredentialGrantAllowed = model.isPasswordCredentialGrantAllowed();
resetPasswordAllowed = model.isResetPasswordAllowed(); resetPasswordAllowed = model.isResetPasswordAllowed();
identityFederationEnabled = model.isIdentityFederationEnabled(); identityFederationEnabled = model.isIdentityFederationEnabled();
editUsernameAllowed = model.isEditUsernameAllowed();
//--- brute force settings //--- brute force settings
bruteForceProtected = model.isBruteForceProtected(); bruteForceProtected = model.isBruteForceProtected();
maxFailureWaitSeconds = model.getMaxFailureWaitSeconds(); maxFailureWaitSeconds = model.getMaxFailureWaitSeconds();
@ -288,6 +290,10 @@ public class CachedRealm {
return resetPasswordAllowed; return resetPasswordAllowed;
} }
public boolean isEditUsernameAllowed() {
return editUsernameAllowed;
}
public int getSsoSessionIdleTimeout() { public int getSsoSessionIdleTimeout() {
return ssoSessionIdleTimeout; return ssoSessionIdleTimeout;
} }

View file

@ -320,6 +320,17 @@ public class RealmAdapter implements RealmModel {
em.flush(); em.flush();
} }
@Override
public boolean isEditUsernameAllowed() {
return realm.isEditUsernameAllowed();
}
@Override
public void setEditUsernameAllowed(boolean editUsernameAllowed) {
realm.setEditUsernameAllowed(editUsernameAllowed);
em.flush();
}
@Override @Override
public int getNotBefore() { public int getNotBefore() {
return realm.getNotBefore(); return realm.getNotBefore();

View file

@ -59,6 +59,8 @@ public class RealmEntity {
protected boolean rememberMe; protected boolean rememberMe;
@Column(name="PASSWORD_POLICY") @Column(name="PASSWORD_POLICY")
protected String passwordPolicy; protected String passwordPolicy;
@Column(name="EDIT_USERNAME_ALLOWED")
protected boolean editUsernameAllowed;
@Column(name="SSO_IDLE_TIMEOUT") @Column(name="SSO_IDLE_TIMEOUT")
private int ssoSessionIdleTimeout; private int ssoSessionIdleTimeout;
@ -254,6 +256,14 @@ public class RealmEntity {
this.resetPasswordAllowed = resetPasswordAllowed; this.resetPasswordAllowed = resetPasswordAllowed;
} }
public boolean isEditUsernameAllowed() {
return editUsernameAllowed;
}
public void setEditUsernameAllowed(boolean editUsernameAllowed) {
this.editUsernameAllowed = editUsernameAllowed;
}
public int getSsoSessionIdleTimeout() { public int getSsoSessionIdleTimeout() {
return ssoSessionIdleTimeout; return ssoSessionIdleTimeout;
} }

View file

@ -254,6 +254,17 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
updateRealm(); updateRealm();
} }
@Override
public boolean isEditUsernameAllowed() {
return realm.isEditUsernameAllowed();
}
@Override
public void setEditUsernameAllowed(boolean editUsernameAllowed) {
realm.setEditUsernameAllowed(editUsernameAllowed);
updateRealm();
}
@Override @Override
public PasswordPolicy getPasswordPolicy() { public PasswordPolicy getPasswordPolicy() {
if (passwordPolicy == null) { if (passwordPolicy == null) {

View file

@ -35,23 +35,35 @@ import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventStoreProvider; import org.keycloak.events.EventStoreProvider;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.login.LoginFormsProvider; 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.FormMessage;
import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.utils.RedirectUtils; import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.ForbiddenException; import org.keycloak.services.ForbiddenException;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.Urls;
import org.keycloak.services.util.CookieHelper; import org.keycloak.services.util.CookieHelper;
import org.keycloak.services.util.ResolveRelative; import org.keycloak.services.util.ResolveRelative;
import org.keycloak.services.validation.Validation; 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.UriBuilder;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import javax.ws.rs.core.Variant; import javax.ws.rs.core.Variant;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.net.URI; import java.net.URI;
import java.util.HashSet; import java.util.HashSet;
@ -414,13 +425,16 @@ public class AccountService {
UserModel user = auth.getUser(); UserModel user = auth.getUser();
List<FormMessage> errors = Validation.validateUpdateProfileForm(formData); List<FormMessage> errors = Validation.validateUpdateProfileForm(realm, formData);
if (errors != null && !errors.isEmpty()) { if (errors != null && !errors.isEmpty()) {
setReferrerOnPage(); setReferrerOnPage();
return account.setErrors(errors).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT); return account.setErrors(errors).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT);
} }
try { try {
if (realm.isEditUsernameAllowed()) {
user.setUsername(formData.getFirst("username"));
}
user.setFirstName(formData.getFirst("firstName")); user.setFirstName(formData.getFirst("firstName"));
user.setLastName(formData.getFirst("lastName")); user.setLastName(formData.getFirst("lastName"));

View file

@ -186,6 +186,9 @@ public class UsersResource {
} }
private void updateUserFromRep(UserModel user, UserRepresentation rep, Set<String> attrsToRemove) { private void updateUserFromRep(UserModel user, UserRepresentation rep, Set<String> attrsToRemove) {
if (realm.isEditUsernameAllowed()) {
user.setUsername(rep.getUsername());
}
user.setEmail(rep.getEmail()); user.setEmail(rep.getEmail());
user.setFirstName(rep.getFirstName()); user.setFirstName(rep.getFirstName());
user.setLastName(rep.getLastName()); user.setLastName(rep.getLastName());

View file

@ -1,17 +1,16 @@
package org.keycloak.services.validation; 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.PasswordPolicy;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.messages.Messages; 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 class Validation {
public static final String FIELD_PASSWORD_CONFIRM = "password-confirm"; public static final String FIELD_PASSWORD_CONFIRM = "password-confirm";
@ -66,10 +65,17 @@ public class Validation {
errors.add(new FormMessage(field, message)); errors.add(new FormMessage(field, message));
} }
public static List<FormMessage> validateUpdateProfileForm(MultivaluedMap<String, String> formData) { 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<>(); 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))) { if (isEmpty(formData.getFirst(FIELD_FIRST_NAME))) {
addError(errors, FIELD_FIRST_NAME, Messages.MISSING_FIRST_NAME); addError(errors, FIELD_FIRST_NAME, Messages.MISSING_FIRST_NAME);
} }

View file

@ -24,11 +24,9 @@ package org.keycloak.testsuite.account;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule; import org.junit.ClassRule;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.account.freemarker.model.ApplicationsBean;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Event; import org.keycloak.events.Event;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
@ -155,6 +153,9 @@ public class AccountTest {
@Override @Override
public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) { public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) {
UserModel user = manager.getSession().users().getUserByUsername("test-user@localhost", 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(); UserCredentialModel cred = new UserCredentialModel();
cred.setType(CredentialRepresentation.PASSWORD); 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(); 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 @Test
public void setupTotp() { public void setupTotp() {
totpPage.open(); totpPage.open();

View file

@ -242,6 +242,7 @@ public class AdminAPITest {
if (rep.isRememberMe() != null) Assert.assertEquals(rep.isRememberMe(), storedRealm.isRememberMe()); if (rep.isRememberMe() != null) Assert.assertEquals(rep.isRememberMe(), storedRealm.isRememberMe());
if (rep.isVerifyEmail() != null) Assert.assertEquals(rep.isVerifyEmail(), storedRealm.isVerifyEmail()); if (rep.isVerifyEmail() != null) Assert.assertEquals(rep.isVerifyEmail(), storedRealm.isVerifyEmail());
if (rep.isResetPasswordAllowed() != null) Assert.assertEquals(rep.isResetPasswordAllowed(), storedRealm.isResetPasswordAllowed()); 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.getSslRequired() != null) Assert.assertEquals(rep.getSslRequired(), storedRealm.getSslRequired());
if (rep.getAccessCodeLifespan() != null) Assert.assertEquals(rep.getAccessCodeLifespan(), storedRealm.getAccessCodeLifespan()); if (rep.getAccessCodeLifespan() != null) Assert.assertEquals(rep.getAccessCodeLifespan(), storedRealm.getAccessCodeLifespan());
if (rep.getAccessCodeLifespanUserAction() != null) if (rep.getAccessCodeLifespanUserAction() != null)

View file

@ -71,6 +71,7 @@ public class RealmTest extends AbstractClientTest {
rep.setAccessCodeLifespanLogin(1234); rep.setAccessCodeLifespanLogin(1234);
rep.setRegistrationAllowed(true); rep.setRegistrationAllowed(true);
rep.setRegistrationEmailAsUsername(true); rep.setRegistrationEmailAsUsername(true);
rep.setEditUsernameAllowed(true);
realm.update(rep); realm.update(rep);
@ -81,16 +82,19 @@ public class RealmTest extends AbstractClientTest {
assertEquals(1234, rep.getAccessCodeLifespanLogin().intValue()); assertEquals(1234, rep.getAccessCodeLifespanLogin().intValue());
assertEquals(Boolean.TRUE, rep.isRegistrationAllowed()); assertEquals(Boolean.TRUE, rep.isRegistrationAllowed());
assertEquals(Boolean.TRUE, rep.isRegistrationEmailAsUsername()); assertEquals(Boolean.TRUE, rep.isRegistrationEmailAsUsername());
assertEquals(Boolean.TRUE, rep.isEditUsernameAllowed());
// second change // second change
rep.setRegistrationAllowed(false); rep.setRegistrationAllowed(false);
rep.setRegistrationEmailAsUsername(false); rep.setRegistrationEmailAsUsername(false);
rep.setEditUsernameAllowed(false);
realm.update(rep); realm.update(rep);
rep = realm.toRepresentation(); rep = realm.toRepresentation();
assertEquals(Boolean.FALSE, rep.isRegistrationAllowed()); assertEquals(Boolean.FALSE, rep.isRegistrationAllowed());
assertEquals(Boolean.FALSE, rep.isRegistrationEmailAsUsername()); assertEquals(Boolean.FALSE, rep.isRegistrationEmailAsUsername());
assertEquals(Boolean.FALSE, rep.isEditUsernameAllowed());
} }
@Test @Test

View file

@ -1,13 +1,13 @@
package org.keycloak.testsuite.admin; package org.keycloak.testsuite.admin;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.keycloak.admin.client.resource.IdentityProviderResource; import org.keycloak.admin.client.resource.IdentityProviderResource;
import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.representations.idm.ErrorRepresentation; import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation; import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import javax.ws.rs.ClientErrorException; import javax.ws.rs.ClientErrorException;
@ -410,4 +410,78 @@ public class UserTest extends AbstractClientTest {
Assert.assertEquals("invalidClientId not enabled", error.getErrorMessage()); 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);
}
} }

View file

@ -21,6 +21,7 @@ public class ModelTest extends AbstractModelTest {
realm.setRegistrationAllowed(true); realm.setRegistrationAllowed(true);
realm.setRegistrationEmailAsUsername(true); realm.setRegistrationEmailAsUsername(true);
realm.setResetPasswordAllowed(true); realm.setResetPasswordAllowed(true);
realm.setEditUsernameAllowed(true);
realm.setSslRequired(SslRequired.EXTERNAL); realm.setSslRequired(SslRequired.EXTERNAL);
realm.setVerifyEmail(true); realm.setVerifyEmail(true);
realm.setAccessTokenLifespan(1000); realm.setAccessTokenLifespan(1000);
@ -55,6 +56,7 @@ public class ModelTest extends AbstractModelTest {
Assert.assertEquals(expected.isRegistrationAllowed(), actual.isRegistrationAllowed()); Assert.assertEquals(expected.isRegistrationAllowed(), actual.isRegistrationAllowed());
Assert.assertEquals(expected.isRegistrationEmailAsUsername(), actual.isRegistrationEmailAsUsername()); Assert.assertEquals(expected.isRegistrationEmailAsUsername(), actual.isRegistrationEmailAsUsername());
Assert.assertEquals(expected.isResetPasswordAllowed(), actual.isResetPasswordAllowed()); Assert.assertEquals(expected.isResetPasswordAllowed(), actual.isResetPasswordAllowed());
Assert.assertEquals(expected.isEditUsernameAllowed(), actual.isEditUsernameAllowed());
Assert.assertEquals(expected.getSslRequired(), actual.getSslRequired()); Assert.assertEquals(expected.getSslRequired(), actual.getSslRequired());
Assert.assertEquals(expected.isVerifyEmail(), actual.isVerifyEmail()); Assert.assertEquals(expected.isVerifyEmail(), actual.isVerifyEmail());
Assert.assertEquals(expected.getAccessTokenLifespan(), actual.getAccessTokenLifespan()); Assert.assertEquals(expected.getAccessTokenLifespan(), actual.getAccessTokenLifespan());

View file

@ -35,6 +35,9 @@ public class AccountUpdateProfilePage extends AbstractAccountPage {
public static String PATH = RealmsResource.accountUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT)).build("test").toString(); public static String PATH = RealmsResource.accountUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT)).build("test").toString();
@FindBy(id = "username")
private WebElement usernameInput;
@FindBy(id = "firstName") @FindBy(id = "firstName")
private WebElement firstNameInput; private WebElement firstNameInput;
@ -74,11 +77,28 @@ public class AccountUpdateProfilePage extends AbstractAccountPage {
submitButton.click(); 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() { public void clickCancel() {
cancelButton.click(); cancelButton.click();
} }
public String getUsername() {
return usernameInput.getAttribute("value");
}
public String getFirstName() { public String getFirstName() {
return firstNameInput.getAttribute("value"); return firstNameInput.getAttribute("value");
} }