Support creation of accounts via REST
PUT on '/accounts/<username>' creates a new account.
The code from the 'create-account' SSH command was moved to the REST
layer.
Change-Id: Ic8fd9daf9fb64f7918b7f7eb461f8e8e957099a9
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index f17a741..5addd16 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -35,6 +35,50 @@
}
----
+[[create-account]]
+Create Account
+~~~~~~~~~~~~~~
+[verse]
+'PUT /accounts/link:#username[\{username\}]'
+
+Creates a new account.
+
+In the request body additional data for the account can be provided as
+link:#account-input[AccountInput].
+
+.Request
+----
+ PUT /accounts/john HTTP/1.0
+ Content-Type: application/json;charset=UTF-8
+
+ {
+ "name": "John Doe",
+ "email": "john.doe@example.com",
+ "ssh_key": "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw==",
+ "http_password": "19D9aIn7zePb",
+ "groups": [
+ "MyProject-Owners"
+ ]
+ }
+----
+
+As response a detailed link:#account-info[AccountInfo] entity is
+returned that describes the created account.
+
+.Response
+----
+ HTTP/1.1 201 Created
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "_account_id": 1000195,
+ "name": "John Doe",
+ "email": "john.doe@example.com"
+ }
+----
+
[[list-account-capabilities]]
List Account Capabilities
~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -391,6 +435,11 @@
Identifier of a global capability. Valid values are all field names of
the link:#capability-info[CapabilityInfo] entity.
+[[username]]
+\{username\}
+~~~~~~~~~~~~
+The user name.
+
[[json-entities]]
JSON Entities
@@ -412,6 +461,26 @@
Only set if detailed account information is requested.
|===========================
+[[account-input]]
+AccountInput
+~~~~~~~~~~~~
+The `AccountInput` entity contains information for the creation of
+a new account.
+
+[options="header",width="50%",cols="1,^2,4"]
+|============================
+|Field Name ||Description
+|`username` |optional|
+The user name. If provided, must match the user name from the URL.
+|`name` |optional|The full name of the user.
+|`email` |optional|The email address of the user.
+|`ssh_key` |optional|The public SSH key of the user.
+|`http_password`|optional|The HTTP password of the user.
+|`groups` |optional|
+A list of link:rest-api-groups.html#group-id[group IDs] that identify
+the groups to which the user should be added.
+|============================
+
[[capability-info]]
CapabilityInfo
~~~~~~~~~~~~~~
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
index 674046c..cff3a35 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
@@ -16,6 +16,7 @@
import com.google.common.collect.Iterables;
import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsCreate;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -34,24 +35,28 @@
import java.util.Set;
public class AccountsCollection implements
- RestCollection<TopLevelResource, AccountResource> {
+ RestCollection<TopLevelResource, AccountResource>,
+ AcceptsCreate<TopLevelResource>{
private final Provider<CurrentUser> self;
private final AccountResolver resolver;
private final AccountControl.Factory accountControlFactory;
private final IdentifiedUser.GenericFactory userFactory;
private final DynamicMap<RestView<AccountResource>> views;
+ private final CreateAccount.Factory createAccountFactory;
@Inject
AccountsCollection(Provider<CurrentUser> self,
AccountResolver resolver,
AccountControl.Factory accountControlFactory,
IdentifiedUser.GenericFactory userFactory,
- DynamicMap<RestView<AccountResource>> views) {
+ DynamicMap<RestView<AccountResource>> views,
+ CreateAccount.Factory createAccountFactory) {
this.self = self;
this.resolver = resolver;
this.accountControlFactory = accountControlFactory;
this.userFactory = userFactory;
this.views = views;
+ this.createAccountFactory = createAccountFactory;
}
@Override
@@ -116,4 +121,10 @@
public DynamicMap<RestView<AccountResource>> views() {
return views;
}
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public CreateAccount create(TopLevelResource parent, IdString username) {
+ return createAccountFactory.create(username.get());
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
new file mode 100644
index 0000000..4a1f4db
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
@@ -0,0 +1,200 @@
+// Copyright (C) 2013 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.common.collect.Sets;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupDescriptions;
+import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.CreateAccount.Input;
+import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+@RequiresCapability(GlobalCapability.CREATE_ACCOUNT)
+public class CreateAccount implements RestModifyView<TopLevelResource, Input> {
+ public static class Input {
+ @DefaultInput
+ public String username;
+ public String name;
+ public String email;
+ public String sshKey;
+ public String httpPassword;
+ public List<String> groups;
+ }
+
+ public static interface Factory {
+ CreateAccount create(String username);
+ }
+
+ private final ReviewDb db;
+ private final IdentifiedUser currentUser;
+ private final GroupsCollection groupsCollection;
+ private final SshKeyCache sshKeyCache;
+ private final AccountCache accountCache;
+ private final AccountByEmailCache byEmailCache;
+ private final String username;
+
+ @Inject
+ CreateAccount(ReviewDb db, IdentifiedUser currentUser,
+ GroupsCollection groupsCollection, SshKeyCache sshKeyCache,
+ AccountCache accountCache, AccountByEmailCache byEmailCache,
+ @Assisted String username) {
+ this.db = db;
+ this.currentUser = currentUser;
+ this.groupsCollection = groupsCollection;
+ this.sshKeyCache = sshKeyCache;
+ this.accountCache = accountCache;
+ this.byEmailCache = byEmailCache;
+ this.username = username;
+ }
+
+ @Override
+ public Object apply(TopLevelResource rsrc, Input input)
+ throws BadRequestException, ResourceConflictException,
+ UnprocessableEntityException, OrmException {
+ if (input == null) {
+ input = new Input();
+ }
+ if (input.username != null && !username.equals(input.username)) {
+ throw new BadRequestException("username must match URL");
+ }
+
+ if (!username.matches(Account.USER_NAME_PATTERN)) {
+ throw new BadRequestException("Username '" + username + "'"
+ + " must contain only letters, numbers, _, - or .");
+ }
+
+ Set<AccountGroup.Id> groups = parseGroups(input.groups);
+
+ Account.Id id = new Account.Id(db.nextAccountId());
+ AccountSshKey key = createSshKey(id, input.sshKey);
+
+ AccountExternalId extUser =
+ new AccountExternalId(id, new AccountExternalId.Key(
+ AccountExternalId.SCHEME_USERNAME, username));
+
+ if (input.httpPassword != null) {
+ extUser.setPassword(input.httpPassword);
+ }
+
+ if (db.accountExternalIds().get(extUser.getKey()) != null) {
+ throw new ResourceConflictException(
+ "username '" + username + "' already exists");
+ }
+ if (input.email != null
+ && db.accountExternalIds().get(getEmailKey(input.email)) != null) {
+ throw new UnprocessableEntityException(
+ "email '" + input.email + "' already exists");
+ }
+
+ try {
+ db.accountExternalIds().insert(Collections.singleton(extUser));
+ } catch (OrmDuplicateKeyException duplicateKey) {
+ throw new ResourceConflictException(
+ "username '" + username + "' already exists");
+ }
+
+ if (input.email != null) {
+ AccountExternalId extMailto =
+ new AccountExternalId(id, getEmailKey(input.email));
+ extMailto.setEmailAddress(input.email);
+ try {
+ db.accountExternalIds().insert(Collections.singleton(extMailto));
+ } catch (OrmDuplicateKeyException duplicateKey) {
+ try {
+ db.accountExternalIds().delete(Collections.singleton(extUser));
+ } catch (OrmException cleanupError) {
+ }
+ throw new UnprocessableEntityException(
+ "email '" + input.email + "' already exists");
+ }
+ }
+
+ Account a = new Account(id);
+ a.setFullName(input.name);
+ a.setPreferredEmail(input.email);
+ db.accounts().insert(Collections.singleton(a));
+
+ if (key != null) {
+ db.accountSshKeys().insert(Collections.singleton(key));
+ }
+
+ for (AccountGroup.Id groupId : groups) {
+ AccountGroupMember m =
+ new AccountGroupMember(new AccountGroupMember.Key(id, groupId));
+ db.accountGroupMembersAudit().insert(Collections.singleton(
+ new AccountGroupMemberAudit(m, currentUser.getAccountId())));
+ db.accountGroupMembers().insert(Collections.singleton(m));
+ }
+
+ sshKeyCache.evict(username);
+ accountCache.evictByUsername(username);
+ byEmailCache.evict(input.email);
+
+ return Response.created(AccountInfo.parse(a, true));
+ }
+
+ private Set<AccountGroup.Id> parseGroups(List<String> groups)
+ throws UnprocessableEntityException {
+ Set<AccountGroup.Id> groupIds = Sets.newHashSet();
+ if (groups != null) {
+ for (String g : groups) {
+ groupIds.add(GroupDescriptions.toAccountGroup(
+ groupsCollection.parseInternal(g)).getId());
+ }
+ }
+ return groupIds;
+ }
+
+ private AccountSshKey createSshKey(Account.Id id, String sshKey)
+ throws BadRequestException {
+ if (sshKey == null) {
+ return null;
+ }
+ try {
+ return sshKeyCache.create(new AccountSshKey.Id(id, 1), sshKey.trim());
+ } catch (InvalidSshKeyException e) {
+ throw new BadRequestException(e.getMessage());
+ }
+ }
+
+ private AccountExternalId.Key getEmailKey(String email) {
+ return new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email);
+ }
+}
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 57a4a22..dedcc03 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
@@ -19,6 +19,7 @@
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
public class Module extends RestApiModule {
@Override
@@ -29,6 +30,7 @@
DynamicMap.mapOf(binder(), ACCOUNT_KIND);
DynamicMap.mapOf(binder(), CAPABILITY_KIND);
+ put(ACCOUNT_KIND).to(PutAccount.class);
get(ACCOUNT_KIND).to(GetAccount.class);
get(ACCOUNT_KIND, "avatar").to(GetAvatar.class);
get(ACCOUNT_KIND, "avatar.change.url").to(GetAvatarChangeUrl.class);
@@ -37,5 +39,7 @@
get(ACCOUNT_KIND, "preferences.diff").to(GetDiffPreferences.class);
put(ACCOUNT_KIND, "preferences.diff").to(SetDiffPreferences.class);
get(CAPABILITY_KIND).to(GetCapabilities.CheckOne.class);
+
+ install(new FactoryModuleBuilder().build(CreateAccount.Factory.class));
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java
new file mode 100644
index 0000000..34e9143
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2013 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.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.account.CreateAccount.Input;
+
+public class PutAccount implements RestModifyView<AccountResource, Input> {
+ @Override
+ public Object apply(AccountResource resource, Input input)
+ throws ResourceConflictException {
+ throw new ResourceConflictException("Account \"" + resource.getUser().getNameEmail()
+ + "\" already exists");
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index c209249..6c4b41c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -14,23 +14,16 @@
package com.google.gerrit.sshd.commands;
+import com.google.common.base.Function;
+import com.google.common.collect.Lists;
import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.errors.InvalidSshKeyException;
import com.google.gerrit.extensions.annotations.RequiresCapability;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountByEmailCache;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gerrit.server.account.CreateAccount;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
-import com.google.gwtorm.server.OrmDuplicateKeyException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -42,8 +35,6 @@
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
import java.util.List;
/** Create a new user account. **/
@@ -69,94 +60,29 @@
private String username;
@Inject
- private IdentifiedUser currentUser;
-
- @Inject
- private ReviewDb db;
-
- @Inject
- private SshKeyCache sshKeyCache;
-
- @Inject
- private AccountCache accountCache;
-
- @Inject
- private AccountByEmailCache byEmailCache;
+ private CreateAccount.Factory createAccountFactory;
@Override
- protected void run() throws OrmException, IOException,
- InvalidSshKeyException, UnloggedFailure {
- if (!username.matches(Account.USER_NAME_PATTERN)) {
- throw die("Username '" + username + "'"
- + " must contain only letters, numbers, _, - or .");
- }
-
- final Account.Id id = new Account.Id(db.nextAccountId());
- final AccountSshKey key = readSshKey(id);
-
- AccountExternalId extUser =
- new AccountExternalId(id, new AccountExternalId.Key(
- AccountExternalId.SCHEME_USERNAME, username));
-
- if (httpPassword != null) {
- extUser.setPassword(httpPassword);
- }
-
- if (db.accountExternalIds().get(extUser.getKey()) != null) {
- throw die("username '" + username + "' already exists");
- }
- if (email != null && db.accountExternalIds().get(getEmailKey()) != null) {
- throw die("email '" + email + "' already exists");
- }
-
+ protected void run() throws OrmException, IOException, UnloggedFailure {
+ CreateAccount.Input input = new CreateAccount.Input();
+ input.username = username;
+ input.email = email;
+ input.name = fullName;
+ input.sshKey = readSshKey();
+ input.httpPassword = httpPassword;
+ input.groups = Lists.transform(groups, new Function<AccountGroup.Id, String>() {
+ @Override
+ public String apply(AccountGroup.Id id) {
+ return id.toString();
+ }});
try {
- db.accountExternalIds().insert(Collections.singleton(extUser));
- } catch (OrmDuplicateKeyException duplicateKey) {
- throw die("username '" + username + "' already exists");
+ createAccountFactory.create(username).apply(TopLevelResource.INSTANCE, input);
+ } catch (RestApiException e) {
+ throw die(e.getMessage());
}
-
- if (email != null) {
- AccountExternalId extMailto = new AccountExternalId(id, getEmailKey());
- extMailto.setEmailAddress(email);
- try {
- db.accountExternalIds().insert(Collections.singleton(extMailto));
- } catch (OrmDuplicateKeyException duplicateKey) {
- try {
- db.accountExternalIds().delete(Collections.singleton(extUser));
- } catch (OrmException cleanupError) {
- }
- throw die("email '" + email + "' already exists");
- }
- }
-
- Account a = new Account(id);
- a.setFullName(fullName);
- a.setPreferredEmail(email);
- db.accounts().insert(Collections.singleton(a));
-
- if (key != null) {
- db.accountSshKeys().insert(Collections.singleton(key));
- }
-
- for (AccountGroup.Id groupId : new HashSet<AccountGroup.Id>(groups)) {
- AccountGroupMember m =
- new AccountGroupMember(new AccountGroupMember.Key(id, groupId));
- db.accountGroupMembersAudit().insert(Collections.singleton( //
- new AccountGroupMemberAudit(m, currentUser.getAccountId())));
- db.accountGroupMembers().insert(Collections.singleton(m));
- }
-
- sshKeyCache.evict(username);
- accountCache.evictByUsername(username);
- byEmailCache.evict(email);
}
- private AccountExternalId.Key getEmailKey() {
- return new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email);
- }
-
- private AccountSshKey readSshKey(final Account.Id id)
- throws UnsupportedEncodingException, IOException, InvalidSshKeyException {
+ private String readSshKey() throws UnsupportedEncodingException, IOException {
if (sshKey == null) {
return null;
}
@@ -169,6 +95,6 @@
sshKey += line + "\n";
}
}
- return sshKeyCache.create(new AccountSshKey.Id(id, 1), sshKey.trim());
+ return sshKey;
}
}