Enable aliases for SSH commands
Site administrators can alias SSH commands from a plugin into the
gerrit namespace using the ssh-alias section in gerrit.config:
[ssh-alias]
ls = gerrit ls-projects
replicate = replication start
The aliases are configured statically at server startup, but are
resolved dynamically at invocation time to the currently loaded
version of the plugin. If the plugin is not loaded, or does not
define the command, "not found" is returned to the user.
Change-Id: Ie98edbef6c8f2fcb3960e7f06329f5dee670ac1e
This commit is contained in:
@@ -1920,6 +1920,17 @@ If true the deprecated `/query` URL is available to return JSON
|
||||
and text results for changes. If false, the URL is disabled and
|
||||
returns 404 to clients. Default is true, enabling `/query`.
|
||||
|
||||
[[ssh-alias]] Section ssh-alias
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Variables in section ssh-alias permit the site administrator to alias
|
||||
another command from Gerrit or a plugin into the `gerrit` command
|
||||
namespace. To alias `replication start` to `gerrit replicate`:
|
||||
|
||||
----
|
||||
[ssh-alias]
|
||||
replicate = replication start
|
||||
----
|
||||
|
||||
[[sshd]] Section sshd
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -245,7 +245,7 @@ public class Daemon extends SiteProgram {
|
||||
private Injector createSshInjector() {
|
||||
final List<Module> modules = new ArrayList<Module>();
|
||||
if (sshd) {
|
||||
modules.add(new SshModule());
|
||||
modules.add(sysInjector.getInstance(SshModule.class));
|
||||
if (slave) {
|
||||
modules.add(new SlaveCommandModule());
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright (C) 2012 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.sshd;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.util.concurrent.Atomics;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gerrit.server.account.CapabilityControl;
|
||||
import com.google.inject.Provider;
|
||||
|
||||
import org.apache.sshd.server.Command;
|
||||
import org.apache.sshd.server.Environment;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/** Command that executes some other command. */
|
||||
public class AliasCommand extends BaseCommand {
|
||||
private final DispatchCommandProvider root;
|
||||
private final Provider<CurrentUser> currentUser;
|
||||
private final CommandName command;
|
||||
private final AtomicReference<Command> atomicCmd;
|
||||
|
||||
AliasCommand(@CommandName(Commands.ROOT) DispatchCommandProvider root,
|
||||
Provider<CurrentUser> currentUser, CommandName command) {
|
||||
this.root = root;
|
||||
this.currentUser = currentUser;
|
||||
this.command = command;
|
||||
this.atomicCmd = Atomics.newReference();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Environment env) throws IOException {
|
||||
try {
|
||||
begin(env);
|
||||
} catch (UnloggedFailure e) {
|
||||
String msg = e.getMessage();
|
||||
if (!msg.endsWith("\n")) {
|
||||
msg += "\n";
|
||||
}
|
||||
err.write(msg.getBytes(ENC));
|
||||
err.flush();
|
||||
onExit(e.exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
private void begin(Environment env) throws UnloggedFailure, IOException {
|
||||
Map<String, Provider<Command>> map = root.getMap();
|
||||
for (String name : chain(command)) {
|
||||
Provider<? extends Command> p = map.get(name);
|
||||
if (p == null) {
|
||||
throw new UnloggedFailure(1, getName() + ": not found");
|
||||
}
|
||||
|
||||
Command cmd = p.get();
|
||||
if (!(cmd instanceof DispatchCommand)) {
|
||||
throw new UnloggedFailure(1, getName() + ": not found");
|
||||
}
|
||||
map = ((DispatchCommand) cmd).getMap();
|
||||
}
|
||||
|
||||
Provider<? extends Command> p = map.get(command.value());
|
||||
if (p == null) {
|
||||
throw new UnloggedFailure(1, getName() + ": not found");
|
||||
}
|
||||
|
||||
Command cmd = p.get();
|
||||
checkRequiresCapability(cmd);
|
||||
if (cmd instanceof BaseCommand) {
|
||||
BaseCommand bc = (BaseCommand)cmd;
|
||||
bc.setName(getName());
|
||||
bc.setArguments(getArguments());
|
||||
}
|
||||
provideStateTo(cmd);
|
||||
atomicCmd.set(cmd);
|
||||
cmd.start(env);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
Command cmd = atomicCmd.getAndSet(null);
|
||||
if (cmd != null) {
|
||||
cmd.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkRequiresCapability(Command cmd) throws UnloggedFailure {
|
||||
RequiresCapability rc = cmd.getClass().getAnnotation(RequiresCapability.class);
|
||||
if (rc != null) {
|
||||
CurrentUser user = currentUser.get();
|
||||
CapabilityControl ctl = user.getCapabilities();
|
||||
if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
|
||||
String msg = String.format(
|
||||
"fatal: %s does not have \"%s\" capability.",
|
||||
user.getUserName(), rc.value());
|
||||
throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static LinkedList<String> chain(CommandName command) {
|
||||
LinkedList<String> chain = Lists.newLinkedList();
|
||||
while (command != null) {
|
||||
chain.addFirst(command.value());
|
||||
command = Commands.parentOf(command);
|
||||
}
|
||||
chain.removeLast();
|
||||
return chain;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (C) 2012 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.sshd;
|
||||
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
|
||||
import org.apache.sshd.server.Command;
|
||||
|
||||
/** Resolves an alias to another command. */
|
||||
public class AliasCommandProvider implements Provider<Command> {
|
||||
private final CommandName command;
|
||||
|
||||
@Inject
|
||||
@CommandName(Commands.ROOT)
|
||||
private DispatchCommandProvider root;
|
||||
|
||||
@Inject
|
||||
private Provider<CurrentUser> currentUser;
|
||||
|
||||
public AliasCommandProvider(CommandName command) {
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Command get() {
|
||||
return new AliasCommand(root, currentUser, command);
|
||||
}
|
||||
}
|
||||
@@ -117,10 +117,18 @@ public abstract class BaseCommand implements Command {
|
||||
this.exit = callback;
|
||||
}
|
||||
|
||||
String getName() {
|
||||
return commandName;
|
||||
}
|
||||
|
||||
void setName(final String prefix) {
|
||||
this.commandName = prefix;
|
||||
}
|
||||
|
||||
String[] getArguments() {
|
||||
return argv;
|
||||
}
|
||||
|
||||
public void setArguments(final String[] argv) {
|
||||
this.argv = argv;
|
||||
}
|
||||
@@ -280,7 +288,9 @@ public abstract class BaseCommand implements Command {
|
||||
*/
|
||||
protected void onExit(final int rc) {
|
||||
exit.onExit(rc);
|
||||
cleanup.run();
|
||||
if (cleanup != null) {
|
||||
cleanup.run();
|
||||
}
|
||||
}
|
||||
|
||||
/** Wrap the supplied output stream in a UTF-8 encoded PrintWriter. */
|
||||
|
||||
@@ -97,6 +97,13 @@ public class Commands {
|
||||
return false;
|
||||
}
|
||||
|
||||
static CommandName parentOf(CommandName name) {
|
||||
if (name instanceof NestedCommandNameImpl) {
|
||||
return ((NestedCommandNameImpl) name).parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final class NestedCommandNameImpl implements CommandName {
|
||||
private final CommandName parent;
|
||||
private final String name;
|
||||
|
||||
@@ -38,11 +38,10 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
*/
|
||||
final class DispatchCommand extends BaseCommand {
|
||||
interface Factory {
|
||||
DispatchCommand create(String prefix, Map<String, Provider<Command>> map);
|
||||
DispatchCommand create(Map<String, Provider<Command>> map);
|
||||
}
|
||||
|
||||
private final Provider<CurrentUser> currentUser;
|
||||
private final String prefix;
|
||||
private final Map<String, Provider<Command>> commands;
|
||||
private final AtomicReference<Command> atomicCmd;
|
||||
|
||||
@@ -53,14 +52,17 @@ final class DispatchCommand extends BaseCommand {
|
||||
private List<String> args = new ArrayList<String>();
|
||||
|
||||
@Inject
|
||||
DispatchCommand(final Provider<CurrentUser> cu, @Assisted final String pfx,
|
||||
DispatchCommand(final Provider<CurrentUser> cu,
|
||||
@Assisted final Map<String, Provider<Command>> all) {
|
||||
currentUser = cu;
|
||||
prefix = pfx;
|
||||
commands = all;
|
||||
atomicCmd = Atomics.newReference();
|
||||
}
|
||||
|
||||
Map<String, Provider<Command>> getMap() {
|
||||
return commands;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(final Environment env) throws IOException {
|
||||
try {
|
||||
@@ -69,7 +71,7 @@ final class DispatchCommand extends BaseCommand {
|
||||
final Provider<Command> p = commands.get(commandName);
|
||||
if (p == null) {
|
||||
String msg =
|
||||
(prefix.isEmpty() ? "Gerrit Code Review" : prefix) + ": "
|
||||
(getName().isEmpty() ? "Gerrit Code Review" : getName()) + ": "
|
||||
+ commandName + ": not found";
|
||||
throw new UnloggedFailure(1, msg);
|
||||
}
|
||||
@@ -78,10 +80,10 @@ final class DispatchCommand extends BaseCommand {
|
||||
checkRequiresCapability(cmd);
|
||||
if (cmd instanceof BaseCommand) {
|
||||
final BaseCommand bc = (BaseCommand) cmd;
|
||||
if (prefix.isEmpty())
|
||||
if (getName().isEmpty())
|
||||
bc.setName(commandName);
|
||||
else
|
||||
bc.setName(prefix + " " + commandName);
|
||||
bc.setName(getName() + " " + commandName);
|
||||
bc.setArguments(args.toArray(new String[args.size()]));
|
||||
|
||||
} else if (!args.isEmpty()) {
|
||||
@@ -129,9 +131,9 @@ final class DispatchCommand extends BaseCommand {
|
||||
protected String usage() {
|
||||
final StringBuilder usage = new StringBuilder();
|
||||
usage.append("Available commands");
|
||||
if (!prefix.isEmpty()) {
|
||||
if (!getName().isEmpty()) {
|
||||
usage.append(" of ");
|
||||
usage.append(prefix);
|
||||
usage.append(getName());
|
||||
}
|
||||
usage.append(" are:\n");
|
||||
usage.append("\n");
|
||||
@@ -143,8 +145,8 @@ final class DispatchCommand extends BaseCommand {
|
||||
usage.append("\n");
|
||||
|
||||
usage.append("See '");
|
||||
if (prefix.indexOf(' ') < 0) {
|
||||
usage.append(prefix);
|
||||
if (getName().indexOf(' ') < 0) {
|
||||
usage.append(getName());
|
||||
usage.append(' ');
|
||||
}
|
||||
usage.append("COMMAND --help' for more information.\n");
|
||||
|
||||
@@ -38,24 +38,16 @@ public class DispatchCommandProvider implements Provider<DispatchCommand> {
|
||||
@Inject
|
||||
private DispatchCommand.Factory factory;
|
||||
|
||||
private final String dispatcherName;
|
||||
private final CommandName parent;
|
||||
|
||||
private volatile ConcurrentMap<String, Provider<Command>> map;
|
||||
|
||||
public DispatchCommandProvider(final CommandName cn) {
|
||||
this(Commands.nameOf(cn), cn);
|
||||
}
|
||||
|
||||
public DispatchCommandProvider(final String dispatcherName,
|
||||
final CommandName cn) {
|
||||
this.dispatcherName = dispatcherName;
|
||||
this.parent = cn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DispatchCommand get() {
|
||||
return factory.create(dispatcherName, getMap());
|
||||
return factory.create(getMap());
|
||||
}
|
||||
|
||||
public RegistrationHandle register(final CommandName name,
|
||||
@@ -84,7 +76,7 @@ public class DispatchCommandProvider implements Provider<DispatchCommand> {
|
||||
};
|
||||
}
|
||||
|
||||
private ConcurrentMap<String, Provider<Command>> getMap() {
|
||||
ConcurrentMap<String, Provider<Command>> getMap() {
|
||||
if (map == null) {
|
||||
synchronized (this) {
|
||||
if (map == null) {
|
||||
|
||||
@@ -16,6 +16,7 @@ package com.google.gerrit.sshd;
|
||||
|
||||
import static com.google.inject.Scopes.SINGLETON;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.gerrit.lifecycle.LifecycleModule;
|
||||
import com.google.gerrit.reviewdb.client.Account;
|
||||
import com.google.gerrit.reviewdb.client.AccountGroup;
|
||||
@@ -29,6 +30,7 @@ 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;
|
||||
import com.google.gerrit.server.git.QueueProvider;
|
||||
import com.google.gerrit.server.git.WorkQueue;
|
||||
import com.google.gerrit.server.plugins.ModuleGenerator;
|
||||
@@ -49,19 +51,32 @@ import com.google.gerrit.sshd.commands.DefaultCommandModule;
|
||||
import com.google.gerrit.sshd.commands.QueryShell;
|
||||
import com.google.gerrit.util.cli.CmdLineParser;
|
||||
import com.google.gerrit.util.cli.OptionHandlerUtil;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.internal.UniqueAnnotations;
|
||||
import com.google.inject.servlet.RequestScoped;
|
||||
|
||||
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 org.eclipse.jgit.lib.ObjectId;
|
||||
import org.kohsuke.args4j.spi.OptionHandler;
|
||||
|
||||
import java.net.SocketAddress;
|
||||
import java.util.Map;
|
||||
|
||||
/** Configures standard dependencies for {@link SshDaemon}. */
|
||||
public class SshModule extends FactoryModule {
|
||||
private final Map<String, String> aliases;
|
||||
|
||||
@Inject
|
||||
SshModule(@GerritServerConfig Config cfg) {
|
||||
aliases = Maps.newHashMap();
|
||||
for (String name : cfg.getNames("ssh-alias")) {
|
||||
aliases.put(name, cfg.getString("ssh-alias", null, name));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bindScope(RequestScoped.class, SshScope.REQUEST);
|
||||
@@ -69,6 +84,7 @@ public class SshModule extends FactoryModule {
|
||||
|
||||
configureRequestScope();
|
||||
configureCmdLineParser();
|
||||
configureAliases();
|
||||
|
||||
install(SshKeyCacheImpl.module());
|
||||
bind(SshLog.class);
|
||||
@@ -78,7 +94,7 @@ public class SshModule extends FactoryModule {
|
||||
factory(PeerDaemonUser.Factory.class);
|
||||
|
||||
bind(DispatchCommandProvider.class).annotatedWith(Commands.CMD_ROOT)
|
||||
.toInstance(new DispatchCommandProvider("", Commands.CMD_ROOT));
|
||||
.toInstance(new DispatchCommandProvider(Commands.CMD_ROOT));
|
||||
bind(CommandFactoryProvider.class);
|
||||
bind(CommandFactory.class).toProvider(CommandFactoryProvider.class);
|
||||
bind(WorkQueue.Executor.class).annotatedWith(StreamCommandExecutor.class)
|
||||
@@ -111,6 +127,20 @@ public class SshModule extends FactoryModule {
|
||||
});
|
||||
}
|
||||
|
||||
private void configureAliases() {
|
||||
CommandName gerrit = Commands.named("gerrit");
|
||||
for (Map.Entry<String, String> e : aliases.entrySet()) {
|
||||
String name = e.getKey();
|
||||
String[] dest = e.getValue().split("[ \\t]+");
|
||||
CommandName cmd = Commands.named(dest[0]);
|
||||
for (int i = 1; i < dest.length; i++) {
|
||||
cmd = Commands.named(cmd, dest[i]);
|
||||
}
|
||||
bind(Commands.key(gerrit, name))
|
||||
.toProvider(new AliasCommandProvider(cmd));
|
||||
}
|
||||
}
|
||||
|
||||
private void configureRequestScope() {
|
||||
bind(SshScope.Context.class).toProvider(SshScope.ContextProvider.class);
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ public class WebAppInitializer extends GuiceServletContextListener {
|
||||
|
||||
private Injector createSshInjector() {
|
||||
final List<Module> modules = new ArrayList<Module>();
|
||||
modules.add(new SshModule());
|
||||
modules.add(sysInjector.getInstance(SshModule.class));
|
||||
modules.add(new MasterCommandModule());
|
||||
return sysInjector.createChildInjector(modules);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user