Merge "Support creation of accounts via REST"

This commit is contained in:
David Pursehouse 2013-05-07 13:31:07 +00:00 committed by Gerrit Code Review
commit df5679f0a3
6 changed files with 337 additions and 99 deletions

View File

@ -35,6 +35,50 @@ Returns an account as an link:#account-info[AccountInfo] entity.
}
----
[[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 @@ This can be:
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 @@ The email address the user prefers to be contacted through. +
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
~~~~~~~~~~~~~~

View File

@ -16,6 +16,7 @@ package com.google.gerrit.server.account;
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 com.google.inject.Provider;
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 class AccountsCollection implements
public DynamicMap<RestView<AccountResource>> views() {
return views;
}
@SuppressWarnings("unchecked")
@Override
public CreateAccount create(TopLevelResource parent, IdString username) {
return createAccountFactory.create(username.get());
}
}

View File

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

View File

@ -19,6 +19,7 @@ import static com.google.gerrit.server.account.AccountResource.CAPABILITY_KIND;
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 @@ public class Module extends RestApiModule {
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 @@ public class Module extends RestApiModule {
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));
}
}

View File

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

View File

@ -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.IOException;
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 @@ final class CreateAccountCommand extends SshCommand {
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 @@ final class CreateAccountCommand extends SshCommand {
sshKey += line + "\n";
}
}
return sshKeyCache.create(new AccountSshKey.Id(id, 1), sshKey.trim());
return sshKey;
}
}