Add REST endpoint to set username

This REST endpoint only allows to set the initial username. Once set
the username cannot be changed or deleted.

Use the new REST endpoint from the UI instead of the old
AccountSecurity.changeUserName(...) RPC.

The AccountSecurity.changeUserName(...) RPC is removed since it is no
longer used.

Change-Id: I48f4d7642b551e17ceef7772563a229aa83fc1ad
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
This commit is contained in:
Edwin Kempin
2015-07-24 11:47:57 +02:00
parent 7c87d0f929
commit f07e98ba5c
10 changed files with 253 additions and 68 deletions

View File

@@ -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
------

View File

@@ -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<ReviewDb> 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;
}
}
}

View File

@@ -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<VoidResult> callback);
@SignInRequired
void myExternalIds(AsyncCallback<List<AccountExternalId>> callback);

View File

@@ -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<NativeString> 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<JsArray<EmailInfo>> 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() {
}
}
}

View File

@@ -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,10 +114,10 @@ class UsernameField extends Composite {
}
final String newUserName = newName;
Util.ACCOUNT_SEC.changeUserName(newUserName,
new GerritCallback<VoidResult>() {
AccountApi.setUsername("self", newUserName,
new GerritCallback<NativeString>() {
@Override
public void onSuccess(VoidResult result) {
public void onSuccess(NativeString result) {
Gerrit.getUserAccount().username(newUserName);
userNameLbl.setText(newUserName);
userNameLbl.setVisible(true);
@@ -126,9 +126,9 @@ class UsernameField extends Composite {
}
@Override
public void onFailure(final Throwable caught) {
public void onFailure(Throwable caught) {
enableUI(true);
if (caught instanceof InvalidUserNameException) {
if (RestApi.isExpected(422 /* Unprocessable Entity */)) {
new ErrorDialog(Util.C.invalidUserName()).center();
} else {
super.onFailure(caught);

View File

@@ -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<VoidResult> 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<List<AccountExternalId>> callback) {
externalIdDetailFactory.create().to(callback);

View File

@@ -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<VoidResult> {
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<ReviewDb> db;
private final Provider<IdentifiedUser> user;
@Inject
CurrentUser(Factory factory, Provider<ReviewDb> db,
Provider<IdentifiedUser> 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<VoidResult> {
InvalidUserNameException {
final Collection<AccountExternalId> old = old();
if (!old.isEmpty()) {
throw new IllegalStateException("Username cannot be changed.");
throw new IllegalStateException(USERNAME_CANNOT_BE_CHANGED);
}
if (newUsername != null && !newUsername.isEmpty()) {

View File

@@ -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);

View File

@@ -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<AccountResource, Input> {
public static class Input {
@DefaultInput
public String username;
}
private final Provider<CurrentUser> self;
private final ChangeUserName.Factory changeUserNameFactory;
private final Realm realm;
private final Provider<ReviewDb> db;
@Inject
PutUsername(Provider<CurrentUser> self,
ChangeUserName.Factory changeUserNameFactory,
Realm realm,
Provider<ReviewDb> 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;
}
}

View File

@@ -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<List<CommentLinkInfo>>() {})