Add REST endpoint to add members to a group

With PUT on 'groups/*/members/<NEW-MEMBER>' it is now possible to add a
new member to a group.

In addition with PUT on 'groups/*/members' it is possible to add
multiple new members to a group at once.

The AccountGroupMembersScreen uses the new REST endpoint to add new
members. After a member was added the screen is reloaded. Other changes
should follow to update the displayed member list without reloading the
complete screen.

The Guice bindings needed to be modified to make AccountManager
injectable into PutMember. It was needed to bind an implementation of
SshKeyCache since AccountManager depends on ChangeUserName.Factory
which depends on SshKeyCache.

Change-Id: I8de09da94790a6ad55757a1051f9098f5e937312
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
This commit is contained in:
Edwin Kempin
2013-01-11 17:15:10 +01:00
parent b46c9fb932
commit 9a701696ae
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

@@ -34,10 +34,12 @@ import com.google.gerrit.server.MimeUtilFileTypeRegistry;
import com.google.gerrit.server.account.AccountByEmailCacheImpl;
import com.google.gerrit.server.account.AccountCacheImpl;
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;
@@ -214,5 +216,9 @@ public class GerritGlobalModule extends FactoryModule {
bind(AnonymousUser.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);
}