Merge "Add REST endpoint to add members to a group"

This commit is contained in:
Shawn Pearce
2013-01-17 19:05:10 +00:00
committed by Gerrit Code Review
14 changed files with 424 additions and 34 deletions

View File

@@ -16,6 +16,8 @@ package com.google.gerrit.client.admin;
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.groups.GroupApi;
import com.google.gerrit.client.groups.MemberInfo;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
import com.google.gerrit.client.ui.AccountLink;
@@ -175,15 +177,10 @@ public class AccountGroupMembersScreen extends AccountGroupScreen {
}
addMemberBox.setEnabled(false);
Util.GROUP_SVC.addGroupMember(getGroupId(), nameEmail,
new GerritCallback<GroupDetail>() {
public void onSuccess(final GroupDetail result) {
addMemberBox.setEnabled(true);
addMemberBox.setText("");
if (result.accounts != null && result.members != null) {
accounts.merge(result.accounts);
members.display(result.members);
}
GroupApi.addMember(getGroupUUID(), nameEmail,
new GerritCallback<MemberInfo>() {
public void onSuccess(final MemberInfo memberInfo) {
Gerrit.display(Dispatcher.toGroup(getGroupUUID(), AccountGroupScreen.MEMBERS));
}
@Override

View File

@@ -60,6 +60,10 @@ public abstract class AccountGroupScreen extends MenuScreen {
return groupDetail.group.getId();
}
protected AccountGroup.UUID getGroupUUID() {
return groupDetail.group.getGroupUUID();
}
protected void setMembersTabVisible(final boolean visible) {
setLinkVisible(membersTabToken, visible);
}

View File

@@ -0,0 +1,72 @@
// 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.client.groups;
import com.google.gerrit.client.rpc.NativeList;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.http.client.URL;
import com.google.gwt.user.client.rpc.AsyncCallback;
import java.util.Set;
/**
* A collection of static methods which work on the Gerrit REST API for specific
* groups.
*/
public class GroupApi {
/** Add member to a group. */
public static void addMember(AccountGroup.UUID groupUUID,
String member, AsyncCallback<MemberInfo> cb) {
new RestApi(membersBase(groupUUID) + "/" + member).put(cb);
}
/** Add members to a group. */
public static void addMembers(AccountGroup.UUID groupUUID,
Set<String> members, AsyncCallback<NativeList<MemberInfo>> cb) {
RestApi call = new RestApi(membersBase(groupUUID));
MemberInput input = MemberInput.create();
for (String member : members) {
input.add_member(member);
}
call.data(input).put(cb);
}
private static String membersBase(AccountGroup.UUID groupUUID) {
return base(groupUUID) + "members";
}
private static String base(AccountGroup.UUID groupUUID) {
String id = URL.encodePathSegment(groupUUID.get());
return "/groups/" + id + "/";
}
private static class MemberInput extends JavaScriptObject {
final native void init() /*-{ this.members = []; }-*/;
final native void add_member(String n) /*-{ this.members.push(n); }-*/;
static MemberInput create() {
MemberInput m = (MemberInput) createObject();
m.init();
return m;
}
protected MemberInput() {
}
}
}

View File

@@ -0,0 +1,32 @@
// 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.client.groups;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gwt.core.client.JavaScriptObject;
public class MemberInfo extends JavaScriptObject {
public final Account.Id getAccountId() {
return new Account.Id(account_id());
}
private final native int account_id() /*-{ return this.account_id; }-*/;
public final native String fullName() /*-{ return this.full_name; }-*/;
public final native String preferredEmail() /*-{ return this.preferred_email; }-*/;
protected MemberInfo() {
}
}

View File

@@ -29,8 +29,6 @@ import com.google.gerrit.httpd.rpc.UiRpcModule;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.CmdLineParserModule;
import com.google.gerrit.server.RemotePeer;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.ChangeUserName;
import com.google.gerrit.server.account.ClearPassword;
import com.google.gerrit.server.account.GeneratePassword;
import com.google.gerrit.server.config.AuthConfig;
@@ -134,9 +132,6 @@ public class WebModule extends FactoryModule {
bind(GerritConfig.class).toProvider(GerritConfigProvider.class);
DynamicSet.setOf(binder(), WebUiPlugin.class);
bind(AccountManager.class);
bind(ChangeUserName.CurrentUser.class);
factory(ChangeUserName.Factory.class);
factory(ClearPassword.Factory.class);
install(new CmdLineParserModule());
factory(GeneratePassword.Factory.class);

View File

@@ -15,7 +15,6 @@
package com.google.gerrit.httpd;
import com.google.gerrit.server.ssh.SshInfo;
import com.google.gerrit.server.ssh.SshKeyCache;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -29,18 +28,14 @@ import com.google.inject.Provider;
*/
public class WebSshGlueModule extends AbstractModule {
private final Provider<SshInfo> sshInfoProvider;
private final Provider<SshKeyCache> sshKeyCacheProvider;
@Inject
WebSshGlueModule(Provider<SshInfo> sshInfoProvider,
Provider<SshKeyCache> sshKeyCacheProvider) {
WebSshGlueModule(Provider<SshInfo> sshInfoProvider) {
this.sshInfoProvider = sshInfoProvider;
this.sshKeyCacheProvider = sshKeyCacheProvider;
}
@Override
protected void configure() {
bind(SshInfo.class).toProvider(sshInfoProvider);
bind(SshKeyCache.class).toProvider(sshKeyCacheProvider);
}
}

View File

@@ -56,6 +56,7 @@ import com.google.gerrit.server.schema.SchemaUpdater;
import com.google.gerrit.server.schema.SchemaVersionCheck;
import com.google.gerrit.server.schema.UpdateUI;
import com.google.gerrit.server.ssh.NoSshModule;
import com.google.gerrit.sshd.SshKeyCacheImpl;
import com.google.gerrit.sshd.SshModule;
import com.google.gerrit.sshd.commands.MasterCommandModule;
import com.google.gerrit.sshd.commands.SlaveCommandModule;
@@ -310,6 +311,7 @@ public class Daemon extends SiteProgram {
}
});
}
modules.add(SshKeyCacheImpl.module());
if (!slave) {
modules.add(new MasterNodeStartup());
}

View File

@@ -0,0 +1,57 @@
// 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;
import com.google.common.collect.Lists;
import com.google.gerrit.extensions.restapi.BadRequestException;
import java.util.List;
public class BadRequestHandler {
private final List<String> errors = Lists.newLinkedList();
private String action;
public BadRequestHandler(final String action) {
this.action = action;
}
public void addError(final String message) {
errors.add(message);
}
public void addError(final Throwable t) {
errors.add(t.getMessage());
}
public void failOnError()
throws BadRequestException {
if (errors.isEmpty()) {
return;
}
if (errors.size() == 1) {
throw new BadRequestException(action + " failed: " + errors.get(0));
}
final StringBuilder b = new StringBuilder();
b.append("Multiple errors on " + action + ":");
for (final String error : errors) {
b.append("\n");
b.append(error);
}
throw new BadRequestException(b.toString());
}
}

View File

@@ -36,10 +36,12 @@ import com.google.gerrit.server.account.AccountByEmailCacheImpl;
import com.google.gerrit.server.account.AccountCacheImpl;
import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountInfoCacheFactory;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.AccountVisibility;
import com.google.gerrit.server.account.AccountVisibilityProvider;
import com.google.gerrit.server.account.CapabilityControl;
import com.google.gerrit.server.account.ChangeUserName;
import com.google.gerrit.server.account.DefaultRealm;
import com.google.gerrit.server.account.EmailExpander;
import com.google.gerrit.server.account.GroupBackend;
@@ -219,5 +221,9 @@ public class GerritGlobalModule extends FactoryModule {
factory(CommitValidators.Factory.class);
factory(NotesBranchUtil.Factory.class);
bind(AccountManager.class);
bind(ChangeUserName.CurrentUser.class);
factory(ChangeUserName.Factory.class);
}
}

View File

@@ -16,6 +16,7 @@ package com.google.gerrit.server.group;
import com.google.gerrit.common.data.GroupDetail;
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.ChildCollection;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -24,32 +25,41 @@ import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.GroupDetailFactory;
import com.google.gerrit.server.group.PutMembers.PutMember;
import com.google.gerrit.server.util.Url;
import com.google.inject.Inject;
import com.google.inject.Provider;
public class MembersCollection implements
ChildCollection<GroupResource, MemberResource> {
ChildCollection<GroupResource, MemberResource>,
AcceptsCreate<GroupResource>{
private final DynamicMap<RestView<MemberResource>> views;
private final Provider<ListMembers> list;
private final IdentifiedUser.GenericFactory userGenericFactory;
private final GroupCache groupCache;
private final GroupDetailFactory.Factory groupDetailFactory;
private final AccountResolver accountResolver;
private final Provider<PutMembers> put;
@Inject
MembersCollection(final DynamicMap<RestView<MemberResource>> views,
final Provider<ListMembers> list,
final IdentifiedUser.GenericFactory userGenericFactory,
final GroupCache groupCache,
final GroupDetailFactory.Factory groupDetailFactory) {
final GroupDetailFactory.Factory groupDetailFactory,
final AccountResolver accountResolver,
final Provider<PutMembers> put) {
this.views = views;
this.list = list;
this.userGenericFactory = userGenericFactory;
this.groupCache = groupCache;
this.groupDetailFactory = groupDetailFactory;
this.accountResolver = accountResolver;
this.put = put;
}
@Override
@@ -61,10 +71,8 @@ public class MembersCollection implements
@Override
public MemberResource parse(final GroupResource parent, final String id)
throws ResourceNotFoundException, Exception {
final Account.Id accountId;
try {
accountId = new Account.Id(Integer.parseInt(Url.decode(id)));
} catch (NumberFormatException e) {
final Account a = accountResolver.find(Url.decode(id));
if (a == null) {
throw new ResourceNotFoundException(id);
}
@@ -74,15 +82,20 @@ public class MembersCollection implements
groupDetailFactory.create(group.getId()).call();
if (groupDetail.members != null) {
for (final AccountGroupMember member : groupDetail.members) {
if (member.getAccountId().equals(accountId)) {
return new MemberResource(
userGenericFactory.create(accountId));
if (member.getAccountId().equals(a.getId())) {
return new MemberResource(userGenericFactory.create(a.getId()));
}
}
}
throw new ResourceNotFoundException(id);
}
@SuppressWarnings("unchecked")
@Override
public PutMember create(final GroupResource group, final String id) {
return new PutMember(put, Url.decode(id));
}
@Override
public DynamicMap<RestView<MemberResource>> views() {
return views;

View File

@@ -34,6 +34,7 @@ public class Module extends RestApiModule {
child(GROUP_KIND, "members").to(MembersCollection.class);
get(MEMBER_KIND).to(GetMember.class);
put(GROUP_KIND, "members").to(PutMembers.class);
child(GROUP_KIND, "groups").to(IncludedGroupsCollection.class);
get(INCLUDED_GROUP_KIND).to(GetIncludedGroup.class);

View File

@@ -0,0 +1,218 @@
// 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.group;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.errors.InactiveAccountException;
import com.google.gerrit.common.errors.NoSuchAccountException;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.DefaultInput;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.reviewdb.client.Account;
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.AuthType;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.BadRequestHandler;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
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.AccountResolver;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.group.MembersCollection.MemberInfo;
import com.google.gerrit.server.group.PutMembers.Input;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.List;
import java.util.Map;
class PutMembers implements RestModifyView<GroupResource, Input> {
static class Input {
@DefaultInput
String _oneMember;
List<String> members;
static Input init(Input in) {
if (in == null) {
in = new Input();
}
if (in.members == null) {
in.members = Lists.newArrayListWithCapacity(1);
}
if (!Strings.isNullOrEmpty(in._oneMember)) {
in.members.add(in._oneMember);
}
return in;
}
}
private final GroupControl.Factory groupControlFactory;
private final AccountManager accountManager;
private final AuthType authType;
private final AccountResolver accountResolver;
private final AccountCache accountCache;
private final ReviewDb db;
private final Provider<CurrentUser> self;
@Inject
PutMembers(final GroupControl.Factory groupControlFactory,
final AccountManager accountManager,
final AuthConfig authConfig,
final AccountResolver accountResolver,
final AccountCache accountCache, final ReviewDb db,
final Provider<CurrentUser> self) {
this.groupControlFactory = groupControlFactory;
this.accountManager = accountManager;
this.authType = authConfig.getAuthType();
this.accountResolver = accountResolver;
this.accountCache = accountCache;
this.db = db;
this.self = self;
}
@Override
public Class<Input> inputType() {
return Input.class;
}
@Override
public List<MemberInfo> apply(GroupResource resource, Input input)
throws AuthException, MethodNotAllowedException, BadRequestException,
OrmException {
final GroupDescription.Basic group = resource.getGroup();
if (!(group instanceof GroupDescription.Internal)) {
throw new MethodNotAllowedException();
}
input = Input.init(input);
final AccountGroup internalGroup = ((GroupDescription.Internal) group).getAccountGroup();
final GroupControl control = groupControlFactory.controlFor(internalGroup);
final Map<Account.Id, AccountGroupMember> newAccountGroupMembers = Maps.newHashMap();
final List<AccountGroupMemberAudit> newAccountGroupMemberAudits = Lists.newLinkedList();
final BadRequestHandler badRequest = new BadRequestHandler("adding new group members");
final List<MemberInfo> newMembers = Lists.newLinkedList();
final Account.Id me = ((IdentifiedUser) self.get()).getAccountId();
for (final String nameOrEmail : input.members) {
final Account a = findAccount(nameOrEmail);
if (a == null) {
badRequest.addError(new NoSuchAccountException(nameOrEmail));
}
if (!a.isActive()) {
badRequest.addError(new InactiveAccountException(a.getFullName()));
continue;
}
if (!control.canAddMember(a.getId())) {
throw new AuthException("Cannot add member: " + a.getFullName());
}
if (!newAccountGroupMembers.containsKey(a.getId())) {
final AccountGroupMember.Key key =
new AccountGroupMember.Key(a.getId(), internalGroup.getId());
AccountGroupMember m = db.accountGroupMembers().get(key);
if (m == null) {
m = new AccountGroupMember(key);
newAccountGroupMembers.put(m.getAccountId(), m);
newAccountGroupMemberAudits.add(new AccountGroupMemberAudit(m, me));
}
newMembers.add(MembersCollection.parse(a));
}
}
badRequest.failOnError();
db.accountGroupMembersAudit().insert(newAccountGroupMemberAudits);
db.accountGroupMembers().insert(newAccountGroupMembers.values());
for (final AccountGroupMember m : newAccountGroupMembers.values()) {
accountCache.evict(m.getAccountId());
}
return newMembers;
}
private Account findAccount(final String nameOrEmail) throws OrmException {
Account r = accountResolver.find(nameOrEmail);
if (r == null) {
switch (authType) {
case HTTP_LDAP:
case CLIENT_SSL_CERT_LDAP:
case LDAP:
r = createAccountByLdap(nameOrEmail);
break;
default:
}
}
return r;
}
private Account createAccountByLdap(String user) {
if (!user.matches(Account.USER_NAME_PATTERN)) {
return null;
}
try {
final AuthRequest req = AuthRequest.forUser(user);
req.setSkipAuthentication(true);
return accountCache.get(accountManager.authenticate(req).getAccountId())
.getAccount();
} catch (AccountException e) {
return null;
}
}
static class PutMember implements RestModifyView<GroupResource, PutMember.Input> {
static class Input {
}
private final Provider<PutMembers> put;
private final String id;
PutMember(final Provider<PutMembers> put, String id) {
this.put = put;
this.id = id;
}
@Override
public Class<PutMember.Input> inputType() {
return PutMember.Input.class;
}
@Override
public Object apply(GroupResource resource, PutMember.Input input)
throws AuthException, MethodNotAllowedException, BadRequestException,
OrmException {
PutMembers.Input in = new PutMembers.Input();
in._oneMember = id;
return put.get().apply(resource, in);
}
}
}

View File

@@ -22,8 +22,6 @@ import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.CmdLineParserModule;
import com.google.gerrit.server.PeerDaemonUser;
import com.google.gerrit.server.RemotePeer;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.ChangeUserName;
import com.google.gerrit.server.config.FactoryModule;
import com.google.gerrit.server.config.GerritRequestModule;
import com.google.gerrit.server.config.GerritServerConfig;
@@ -44,6 +42,7 @@ import org.apache.sshd.common.KeyPairProvider;
import org.apache.sshd.server.CommandFactory;
import org.apache.sshd.server.PublickeyAuthenticator;
import org.eclipse.jgit.lib.Config;
import java.net.SocketAddress;
import java.util.Map;
@@ -69,7 +68,6 @@ public class SshModule extends FactoryModule {
install(new CmdLineParserModule());
configureAliases();
install(SshKeyCacheImpl.module());
bind(SshLog.class);
bind(SshInfo.class).to(SshDaemon.class).in(SINGLETON);
factory(DispatchCommand.Factory.class);
@@ -83,8 +81,6 @@ public class SshModule extends FactoryModule {
bind(WorkQueue.Executor.class).annotatedWith(StreamCommandExecutor.class)
.toProvider(StreamCommandExecutorProvider.class).in(SINGLETON);
bind(QueueProvider.class).to(CommandExecutorQueueProvider.class).in(SINGLETON);
bind(AccountManager.class);
factory(ChangeUserName.Factory.class);
bind(PublickeyAuthenticator.class).to(DatabasePubKeyAuth.class);
bind(KeyPairProvider.class).toProvider(HostKeyProvider.class).in(SINGLETON);

View File

@@ -46,6 +46,7 @@ import com.google.gerrit.server.schema.DataSourceType;
import com.google.gerrit.server.schema.DatabaseModule;
import com.google.gerrit.server.schema.SchemaModule;
import com.google.gerrit.server.schema.SchemaVersionCheck;
import com.google.gerrit.sshd.SshKeyCacheImpl;
import com.google.gerrit.sshd.SshModule;
import com.google.gerrit.sshd.commands.MasterCommandModule;
import com.google.inject.AbstractModule;
@@ -238,6 +239,7 @@ public class WebAppInitializer extends GuiceServletContextListener {
return HttpCanonicalWebUrlProvider.class;
}
});
modules.add(SshKeyCacheImpl.module());
modules.add(new MasterNodeStartup());
return cfgInjector.createChildInjector(modules);
}