diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt index 3a095777e0..096cf04a2a 100644 --- a/Documentation/rest-api-accounts.txt +++ b/Documentation/rest-api-accounts.txt @@ -253,6 +253,30 @@ Retrieves the username of an account. If the account does not have a username the response is "`404 Not Found`". +[[set-username]] +=== Set Username +-- +'PUT /accounts/link:#account-id[\{account-id\}]/username' +-- + +The new username must be provided in the request body inside +a link:#username-input[UsernameInput] entity. + +Once set, the username cannot be changed or deleted. If attempted this +fails with "`405 Method Not Allowed`". + +.Request +---- + PUT /accounts/self/name HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "username": "jdoe" + } +---- + +As response the new username is returned. + [[get-active]] === Get Active -- @@ -1690,6 +1714,17 @@ user. |`valid` ||Whether the SSH key is valid. |============================= +[[username-input]] +=== UsernameInput +The `UsernameInput` entity contains information for setting the +username for an account. + +[options="header",cols="1,6"] +|======================= +|Field Name |Description +|`username` |The new username of the account. +|======================= + GERRIT ------ diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java new file mode 100644 index 0000000000..2543095944 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java @@ -0,0 +1,82 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.rest.account; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.RestResponse; +import com.google.gerrit.common.TimeUtil; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.account.PutUsername; +import com.google.gwtorm.server.OrmException; +import com.google.gwtorm.server.SchemaFactory; +import com.google.inject.Inject; + +import org.apache.http.HttpStatus; +import org.junit.Test; + +import java.util.Collections; + +public class PutUsernameIT extends AbstractDaemonTest { + @Inject + private SchemaFactory reviewDbProvider; + + @Test + public void set() throws Exception { + PutUsername.Input in = new PutUsername.Input(); + in.username = "myUsername"; + RestResponse r = + adminSession.put("/accounts/" + createUser().get() + "/username", in); + assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK); + assertThat(newGson().fromJson(r.getReader(), String.class)).isEqualTo( + in.username); + } + + @Test + public void setExisting_Conflict() throws Exception { + PutUsername.Input in = new PutUsername.Input(); + in.username = admin.username; + RestResponse r = + adminSession.put("/accounts/" + createUser().get() + "/username", in); + assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_CONFLICT); + } + + @Test + public void setNew_MethodNotAllowed() throws Exception { + PutUsername.Input in = new PutUsername.Input(); + in.username = "newUsername"; + RestResponse r = + adminSession.put("/accounts/" + admin.username + "/username", in); + assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_METHOD_NOT_ALLOWED); + } + + @Test + public void delete_MethodNotAllowed() throws Exception { + RestResponse r = + adminSession.put("/accounts/" + admin.username + "/username"); + assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_METHOD_NOT_ALLOWED); + } + + private Account.Id createUser() throws OrmException { + try (ReviewDb db = reviewDbProvider.open()) { + Account.Id id = new Account.Id(db.nextAccountId()); + Account a = new Account(id, TimeUtil.nowTs()); + db.accounts().insert(Collections.singleton(a)); + return id; + } + } +} diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java index f382d491b2..0a1f454887 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java @@ -30,10 +30,6 @@ import java.util.Set; @RpcImpl(version = Version.V2_0) public interface AccountSecurity extends RemoteJsonService { - @Audit - @SignInRequired - void changeUserName(String newName, AsyncCallback callback); - @SignInRequired void myExternalIds(AsyncCallback> callback); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java index 02c22e128b..a796f945bd 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java @@ -53,6 +53,14 @@ public class AccountApi { new RestApi("/accounts/").id(account).view("username").get(cb); } + /** Set the username */ + public static void setUsername(String account, String username, + AsyncCallback cb) { + UsernameInput input = UsernameInput.create(); + input.username(username); + new RestApi("/accounts/").id(account).view("username").put(input, cb); + } + /** Retrieve email addresses */ public static void getEmails(String account, AsyncCallback> cb) { @@ -128,4 +136,15 @@ public class AccountApi { protected HttpPasswordInput() { } } + + private static class UsernameInput extends JavaScriptObject { + final native void username(String u) /*-{ if(u)this.username=u; }-*/; + + static UsernameInput create() { + return createObject().cast(); + } + + protected UsernameInput() { + } + } } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java index fc823b7fd5..f3884364e2 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java @@ -19,8 +19,9 @@ import com.google.gerrit.client.ConfirmationDialog; import com.google.gerrit.client.ErrorDialog; import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.rpc.GerritCallback; +import com.google.gerrit.client.rpc.NativeString; +import com.google.gerrit.client.rpc.RestApi; import com.google.gerrit.client.ui.OnEditEnabler; -import com.google.gerrit.common.errors.InvalidUserNameException; import com.google.gerrit.reviewdb.client.Account; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; @@ -34,7 +35,6 @@ import com.google.gwt.user.client.ui.TextBox; import com.google.gwtexpui.clippy.client.CopyableLabel; import com.google.gwtexpui.globalkey.client.NpTextBox; import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder; -import com.google.gwtjsonrpc.common.VoidResult; class UsernameField extends Composite { private CopyableLabel userNameLbl; @@ -114,27 +114,27 @@ class UsernameField extends Composite { } final String newUserName = newName; - Util.ACCOUNT_SEC.changeUserName(newUserName, - new GerritCallback() { - @Override - public void onSuccess(VoidResult result) { - Gerrit.getUserAccount().username(newUserName); - userNameLbl.setText(newUserName); - userNameLbl.setVisible(true); - userNameTxt.setVisible(false); - setUserName.setVisible(false); - } + AccountApi.setUsername("self", newUserName, + new GerritCallback() { + @Override + public void onSuccess(NativeString result) { + Gerrit.getUserAccount().username(newUserName); + userNameLbl.setText(newUserName); + userNameLbl.setVisible(true); + userNameTxt.setVisible(false); + setUserName.setVisible(false); + } - @Override - public void onFailure(final Throwable caught) { - enableUI(true); - if (caught instanceof InvalidUserNameException) { - new ErrorDialog(Util.C.invalidUserName()).center(); - } else { - super.onFailure(caught); - } - } - }); + @Override + public void onFailure(Throwable caught) { + enableUI(true); + if (RestApi.isExpected(422 /* Unprocessable Entity */)) { + new ErrorDialog(Util.C.invalidUserName()).center(); + } else { + super.onFailure(caught); + } + } + }); } private void enableUI(final boolean on) { diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java index 58f5dcc4af..ce235c40b4 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java @@ -21,11 +21,9 @@ import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.data.AccountSecurity; import com.google.gerrit.common.data.ContributorAgreement; import com.google.gerrit.common.errors.ContactInformationStoreException; -import com.google.gerrit.common.errors.InvalidUserNameException; import com.google.gerrit.common.errors.NoSuchEntityException; import com.google.gerrit.common.errors.PermissionDeniedException; import com.google.gerrit.httpd.rpc.BaseServiceImplementation; -import com.google.gerrit.httpd.rpc.Handler; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountExternalId; import com.google.gerrit.reviewdb.client.AccountGroup; @@ -38,7 +36,6 @@ import com.google.gerrit.server.account.AccountByEmailCache; import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountException; import com.google.gerrit.server.account.AccountManager; -import com.google.gerrit.server.account.ChangeUserName; import com.google.gerrit.server.account.GroupCache; import com.google.gerrit.server.account.Realm; import com.google.gerrit.server.contact.ContactStore; @@ -66,7 +63,6 @@ class AccountSecurityImpl extends BaseServiceImplementation implements private final AccountManager accountManager; private final boolean useContactInfo; - private final ChangeUserName.CurrentUser changeUserNameFactory; private final DeleteExternalIds.Factory deleteExternalIdsFactory; private final ExternalIdDetailFactory.Factory externalIdDetailFactory; @@ -81,7 +77,6 @@ class AccountSecurityImpl extends BaseServiceImplementation implements final EmailTokenVerifier etv, final ProjectCache pc, final AccountByEmailCache abec, final AccountCache uac, final AccountManager am, - final ChangeUserName.CurrentUser changeUserNameFactory, final DeleteExternalIds.Factory deleteExternalIdsFactory, final ExternalIdDetailFactory.Factory externalIdDetailFactory, final ChangeHooks hooks, final GroupCache groupCache, @@ -99,27 +94,12 @@ class AccountSecurityImpl extends BaseServiceImplementation implements useContactInfo = contactStore != null && contactStore.isEnabled(); - this.changeUserNameFactory = changeUserNameFactory; this.deleteExternalIdsFactory = deleteExternalIdsFactory; this.externalIdDetailFactory = externalIdDetailFactory; this.hooks = hooks; this.groupCache = groupCache; } - @Override - public void changeUserName(final String newName, - final AsyncCallback callback) { - if (realm.allowsEdit(Account.FieldName.USER_NAME)) { - if (newName == null || !newName.matches(Account.USER_NAME_PATTERN)) { - callback.onFailure(new InvalidUserNameException()); - } - Handler.wrap(changeUserNameFactory.create(newName)).to(callback); - } else { - callback.onFailure( - new PermissionDeniedException("Not allowed to change username")); - } - } - @Override public void myExternalIds(AsyncCallback> callback) { externalIdDetailFactory.create().to(callback); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java index 1005569eb1..b413e815e9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java @@ -28,7 +28,6 @@ import com.google.gwtjsonrpc.common.VoidResult; import com.google.gwtorm.server.OrmDuplicateKeyException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; -import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; import java.util.ArrayList; @@ -39,28 +38,12 @@ import java.util.regex.Pattern; /** Operation to change the username of an account. */ public class ChangeUserName implements Callable { + public static final String USERNAME_CANNOT_BE_CHANGED = + "Username cannot be changed."; + private static final Pattern USER_NAME_PATTERN = Pattern.compile(Account.USER_NAME_PATTERN); - /** Factory to change the username for the current user. */ - public static class CurrentUser { - private final Factory factory; - private final Provider db; - private final Provider user; - - @Inject - CurrentUser(Factory factory, Provider db, - Provider user) { - this.factory = factory; - this.db = db; - this.user = user; - } - - public ChangeUserName create(String newUsername) { - return factory.create(db.get(), user.get(), newUsername); - } - } - /** Generic factory to change any user's username. */ public interface Factory { ChangeUserName create(ReviewDb db, IdentifiedUser user, String newUsername); @@ -92,7 +75,7 @@ public class ChangeUserName implements Callable { InvalidUserNameException { final Collection old = old(); if (!old.isEmpty()) { - throw new IllegalStateException("Username cannot be changed."); + throw new IllegalStateException(USERNAME_CANNOT_BE_CHANGED); } if (newUsername != null && !newUsername.isEmpty()) { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java index c9a3714b6d..553392d05d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java @@ -43,6 +43,7 @@ public class Module extends RestApiModule { put(ACCOUNT_KIND, "name").to(PutName.class); delete(ACCOUNT_KIND, "name").to(PutName.class); get(ACCOUNT_KIND, "username").to(GetUsername.class); + put(ACCOUNT_KIND, "username").to(PutUsername.class); get(ACCOUNT_KIND, "active").to(GetActive.class); put(ACCOUNT_KIND, "active").to(PutActive.class); delete(ACCOUNT_KIND, "active").to(DeleteActive.class); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java new file mode 100644 index 0000000000..9506b01193 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java @@ -0,0 +1,90 @@ +// Copyright (C) 2015 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.account; + +import com.google.gerrit.common.errors.InvalidUserNameException; +import com.google.gerrit.common.errors.NameAlreadyUsedException; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.DefaultInput; +import com.google.gerrit.extensions.restapi.MethodNotAllowedException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.extensions.restapi.UnprocessableEntityException; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.account.PutUsername.Input; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +@Singleton +public class PutUsername implements RestModifyView { + public static class Input { + @DefaultInput + public String username; + } + + private final Provider self; + private final ChangeUserName.Factory changeUserNameFactory; + private final Realm realm; + private final Provider db; + + @Inject + PutUsername(Provider self, + ChangeUserName.Factory changeUserNameFactory, + Realm realm, + Provider db) { + this.self = self; + this.changeUserNameFactory = changeUserNameFactory; + this.realm = realm; + this.db = db; + } + + @Override + public String apply(AccountResource rsrc, Input input) throws AuthException, + MethodNotAllowedException, UnprocessableEntityException, + ResourceConflictException, OrmException { + if (self.get() != rsrc.getUser() + && !self.get().getCapabilities().canAdministrateServer()) { + throw new AuthException("not allowed to set username"); + } + + if (!realm.allowsEdit(Account.FieldName.USER_NAME)) { + throw new MethodNotAllowedException("realm does not allow editing username"); + } + + if (input == null) { + input = new Input(); + } + + try { + changeUserNameFactory.create(db.get(), rsrc.getUser(), input.username).call(); + } catch (IllegalStateException e) { + if (ChangeUserName.USERNAME_CANNOT_BE_CHANGED.equals(e.getMessage())) { + throw new MethodNotAllowedException(e.getMessage()); + } else { + throw e; + } + } catch (InvalidUserNameException e) { + throw new UnprocessableEntityException("invalid username"); + } catch (NameAlreadyUsedException e) { + throw new ResourceConflictException("username already used"); + } + + return input.username; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java index e1b0db035d..478febe8a0 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java @@ -302,7 +302,6 @@ public class GerritGlobalModule extends FactoryModule { factory(SubmoduleSectionParser.Factory.class); bind(AccountManager.class); - bind(ChangeUserName.CurrentUser.class); factory(ChangeUserName.Factory.class); bind(new TypeLiteral>() {})