Allow suexec to run any command as any user

The suexec command can only be run by a peer daemon, and permits
the daemon to execute another command as a specific user identity.

This is the foundation of allowing writes to be proxied from a
slave server into the master, the slave just needs to SSH into the
master and use suexec in front of the user supplied command line
to perform an action on their behalf on the master.

Unfortunately this means we have to trust the slave process, as it
can become anyone, including an administrator.  A better approach
would be to use agent authentication and authenticate back through
the slave to the user's agent process, but not every user connection
may be using an agent.  In particular batch jobs might be using an
unencrypted key and no agent to authenticate.

Change-Id: Icb8ddb16959f01189a6c0bdfc8fec45cdd99659b
Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Shawn O. Pearce
2010-01-16 14:27:28 -08:00
parent 1fc80a6eba
commit 2f8b9bc3b7
15 changed files with 411 additions and 148 deletions

View File

@@ -15,15 +15,18 @@
package com.google.gerrit.sshd;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.sshd.args4j.SubcommandHandler;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Argument;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
@@ -39,6 +42,12 @@ final class DispatchCommand extends BaseCommand {
private final Map<String, Provider<Command>> commands;
private Command cmd;
@Argument(index = 0, required = true, metaVar = "COMMAND", handler = SubcommandHandler.class)
private String commandName;
@Argument(index = 1, multiValued = true, metaVar = "ARG")
private List<String> args = new ArrayList<String>();
@Inject
DispatchCommand(final Provider<CurrentUser> cu, @Assisted final String pfx,
@Assisted final Map<String, Provider<Command>> all) {
@@ -49,65 +58,58 @@ final class DispatchCommand extends BaseCommand {
@Override
public void start(final Environment env) throws IOException {
if (commandLine.isEmpty()) {
usage();
return;
}
try {
parseCommandLine();
final String name, args;
int sp = commandLine.indexOf(' ');
if (0 < sp) {
name = commandLine.substring(0, sp);
while (Character.isWhitespace(commandLine.charAt(sp))) {
sp++;
final Provider<Command> p = commands.get(commandName);
if (p == null) {
String msg =
(prefix.isEmpty() ? "Gerrit Code Review" : prefix) + ": "
+ commandName + ": not found";
throw new UnloggedFailure(1, msg);
}
args = commandLine.substring(sp);
} else {
name = commandLine;
args = "";
}
if (name.equals("help") || name.equals("--help") || name.equals("-h")) {
usage();
return;
}
final Provider<Command> p = commands.get(name);
if (p != null) {
final Command cmd = p.get();
if (isAdminCommand(cmd) && !currentUser.get().isAdministrator()) {
final String msg = "fatal: Not a Gerrit administrator";
throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
}
if (cmd instanceof BaseCommand) {
final BaseCommand bc = (BaseCommand) cmd;
if (prefix.isEmpty())
bc.setName(commandName);
else
bc.setName(prefix + " " + commandName);
bc.setArguments(args.toArray(new String[args.size()]));
} else if (!args.isEmpty()) {
throw new UnloggedFailure(1, commandName + " does not take arguments");
}
provideStateTo(cmd);
synchronized (this) {
this.cmd = cmd;
}
if (cmd.getClass().getAnnotation(AdminCommand.class) != null) {
final CurrentUser u = currentUser.get();
if (!u.isAdministrator()) {
err.write("fatal: Not a Gerrit administrator\n".getBytes(ENC));
err.flush();
onExit(BaseCommand.STATUS_NOT_ADMIN);
return;
}
}
provideStateTo(cmd);
if (cmd instanceof BaseCommand) {
final BaseCommand bc = (BaseCommand) cmd;
if (commandPrefix.isEmpty())
bc.setCommandPrefix(name);
else
bc.setCommandPrefix(commandPrefix + " " + name);
bc.setCommandLine(args);
}
cmd.start(env);
} else {
final String msg = prefix + ": " + name + ": not found\n";
} catch (UnloggedFailure e) {
String msg = e.getMessage();
if (!msg.endsWith("\n")) {
msg += "\n";
}
err.write(msg.getBytes(ENC));
err.flush();
onExit(BaseCommand.STATUS_NOT_FOUND);
onExit(e.exitCode);
}
}
private boolean isAdminCommand(final Command cmd) {
return cmd.getClass().getAnnotation(AdminCommand.class) != null;
}
@Override
public void destroy() {
synchronized (this) {
@@ -118,13 +120,15 @@ final class DispatchCommand extends BaseCommand {
}
}
private void usage() throws IOException, UnsupportedEncodingException {
@Override
protected String usage() {
final StringBuilder usage = new StringBuilder();
if (prefix.indexOf(' ') < 0) {
usage.append("usage: " + prefix + " COMMAND [ARGS]\n");
usage.append("Available commands");
if (!prefix.isEmpty()) {
usage.append(" of ");
usage.append(prefix);
}
usage.append("\n");
usage.append("Available commands of " + prefix + " are:\n");
usage.append(" are:\n");
usage.append("\n");
for (Map.Entry<String, Provider<Command>> e : commands.entrySet()) {
usage.append(" ");
@@ -140,8 +144,6 @@ final class DispatchCommand extends BaseCommand {
}
usage.append("COMMAND --help' for more information.\n");
usage.append("\n");
err.write(usage.toString().getBytes("UTF-8"));
err.flush();
onExit(1);
return usage.toString();
}
}