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:
parent
1fc80a6eba
commit
2f8b9bc3b7
@ -45,8 +45,6 @@ import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public abstract class BaseCommand implements Command {
|
||||
@ -61,6 +59,7 @@ public abstract class BaseCommand implements Command {
|
||||
@Option(name = "--help", usage = "display this help text", aliases = {"-h"})
|
||||
private boolean help;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Option(name = "--", usage = "end of options", handler = EndOfOptionsHandler.class)
|
||||
private boolean endOfOptions;
|
||||
|
||||
@ -90,10 +89,10 @@ public abstract class BaseCommand implements Command {
|
||||
private Future<?> task;
|
||||
|
||||
/** Text of the command line which lead up to invoking this instance. */
|
||||
protected String commandPrefix = "";
|
||||
private String commandName = "";
|
||||
|
||||
/** Unparsed rest of the command line. */
|
||||
protected String commandLine = "";
|
||||
/** Unparsed command line options. */
|
||||
private String[] argv;
|
||||
|
||||
public void setInputStream(final InputStream in) {
|
||||
this.in = in;
|
||||
@ -111,21 +110,12 @@ public abstract class BaseCommand implements Command {
|
||||
this.exit = callback;
|
||||
}
|
||||
|
||||
public void setCommandPrefix(final String prefix) {
|
||||
this.commandPrefix = prefix;
|
||||
void setName(final String prefix) {
|
||||
this.commandName = prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the command line to be evaluated by this command.
|
||||
* <p>
|
||||
* If this command is being invoked from a higher level
|
||||
* {@link DispatchCommand} then only the portion after the command name (that
|
||||
* is, the arguments) is supplied.
|
||||
*
|
||||
* @param line the command line received from the client.
|
||||
*/
|
||||
public void setCommandLine(final String line) {
|
||||
this.commandLine = line;
|
||||
public void setArguments(final String[] argv) {
|
||||
this.argv = argv;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -154,50 +144,16 @@ public abstract class BaseCommand implements Command {
|
||||
/**
|
||||
* Parses the command line argument, injecting parsed values into fields.
|
||||
* <p>
|
||||
* This method must be explicitly invoked to cause a parse. When parsing,
|
||||
* arguments are split out of and read from the {@link #commandLine} field.
|
||||
* This method must be explicitly invoked to cause a parse.
|
||||
*
|
||||
* @throws Failure if the command line arguments were invalid.
|
||||
* @throws UnloggedFailure if the command line arguments were invalid.
|
||||
* @see Option
|
||||
* @see Argument
|
||||
*/
|
||||
protected void parseCommandLine() throws Failure {
|
||||
final List<String> list = new ArrayList<String>();
|
||||
boolean inquote = false;
|
||||
StringBuilder r = new StringBuilder();
|
||||
for (int ip = 0; ip < commandLine.length();) {
|
||||
final char b = commandLine.charAt(ip++);
|
||||
switch (b) {
|
||||
case '\t':
|
||||
case ' ':
|
||||
if (inquote)
|
||||
r.append(b);
|
||||
else if (r.length() > 0) {
|
||||
list.add(r.toString());
|
||||
r = new StringBuilder();
|
||||
}
|
||||
continue;
|
||||
case '\'':
|
||||
inquote = !inquote;
|
||||
continue;
|
||||
case '\\':
|
||||
if (inquote || ip == commandLine.length())
|
||||
r.append(b); // literal within a quote
|
||||
else
|
||||
r.append(commandLine.charAt(ip++));
|
||||
continue;
|
||||
default:
|
||||
r.append(b);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (r.length() > 0) {
|
||||
list.add(r.toString());
|
||||
}
|
||||
|
||||
protected void parseCommandLine() throws UnloggedFailure {
|
||||
final CmdLineParser clp = newCmdLineParser();
|
||||
try {
|
||||
clp.parseArgument(list.toArray(new String[list.size()]));
|
||||
clp.parseArgument(argv);
|
||||
} catch (IllegalArgumentException err) {
|
||||
if (!help) {
|
||||
throw new UnloggedFailure(1, "fatal: " + err.getMessage());
|
||||
@ -210,17 +166,22 @@ public abstract class BaseCommand implements Command {
|
||||
|
||||
if (help) {
|
||||
final StringWriter msg = new StringWriter();
|
||||
msg.write(commandPrefix);
|
||||
msg.write(commandName);
|
||||
clp.printSingleLineUsage(msg, null);
|
||||
msg.write('\n');
|
||||
|
||||
msg.write('\n');
|
||||
clp.printUsage(msg, null);
|
||||
msg.write('\n');
|
||||
msg.write(usage());
|
||||
throw new UnloggedFailure(1, msg.toString());
|
||||
}
|
||||
}
|
||||
|
||||
protected String usage() {
|
||||
return "";
|
||||
}
|
||||
|
||||
/** Construct a new parser for this command's received command line. */
|
||||
protected CmdLineParser newCmdLineParser() {
|
||||
return cmdLineParserFactory.create(this);
|
||||
@ -347,7 +308,7 @@ public abstract class BaseCommand implements Command {
|
||||
m.append(")");
|
||||
}
|
||||
m.append(" during ");
|
||||
m.append(getFullCommandLine());
|
||||
m.append(contextProvider.get().getCommandLine());
|
||||
log.error(m.toString(), e);
|
||||
}
|
||||
|
||||
@ -374,20 +335,6 @@ public abstract class BaseCommand implements Command {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getFullCommandLine();
|
||||
}
|
||||
|
||||
private String getFullCommandLine() {
|
||||
if (commandPrefix.isEmpty())
|
||||
return commandLine;
|
||||
else if (commandLine.isEmpty())
|
||||
return commandPrefix;
|
||||
else
|
||||
return commandPrefix + " " + commandLine;
|
||||
}
|
||||
|
||||
private final class TaskThunk implements CancelableRunnable {
|
||||
private final CommandRunnable thunk;
|
||||
private final Context context;
|
||||
@ -398,7 +345,7 @@ public abstract class BaseCommand implements Command {
|
||||
this.context = contextProvider.get();
|
||||
|
||||
StringBuilder m = new StringBuilder();
|
||||
m.append(getFullCommandLine());
|
||||
m.append(context.getCommandLine());
|
||||
if (userProvider.get() instanceof IdentifiedUser) {
|
||||
IdentifiedUser u = (IdentifiedUser) userProvider.get();
|
||||
m.append(" (" + u.getAccount().getUserName() + ")");
|
||||
|
@ -28,6 +28,8 @@ import org.apache.sshd.server.session.ServerSession;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Creates a CommandFactory using commands registered by {@link CommandModule}.
|
||||
@ -55,6 +57,7 @@ class CommandFactoryProvider implements Provider<CommandFactory> {
|
||||
|
||||
private class Trampoline implements Command, SessionAware {
|
||||
private final String commandLine;
|
||||
private final String[] argv;
|
||||
private InputStream in;
|
||||
private OutputStream out;
|
||||
private OutputStream err;
|
||||
@ -65,6 +68,7 @@ class CommandFactoryProvider implements Provider<CommandFactory> {
|
||||
|
||||
Trampoline(final String cmdLine) {
|
||||
commandLine = cmdLine;
|
||||
argv = split(cmdLine);
|
||||
}
|
||||
|
||||
public void setInputStream(final InputStream in) {
|
||||
@ -84,7 +88,8 @@ class CommandFactoryProvider implements Provider<CommandFactory> {
|
||||
}
|
||||
|
||||
public void setSession(final ServerSession session) {
|
||||
this.ctx = new Context(session.getAttribute(SshSession.KEY));
|
||||
final SshSession s = session.getAttribute(SshSession.KEY);
|
||||
this.ctx = new Context(s, commandLine);
|
||||
}
|
||||
|
||||
public void start(final Environment env) throws IOException {
|
||||
@ -92,7 +97,7 @@ class CommandFactoryProvider implements Provider<CommandFactory> {
|
||||
final Context old = SshScope.set(ctx);
|
||||
try {
|
||||
cmd = dispatcher.get();
|
||||
cmd.setCommandLine(commandLine);
|
||||
cmd.setArguments(argv);
|
||||
cmd.setInputStream(in);
|
||||
cmd.setOutputStream(out);
|
||||
cmd.setErrorStream(err);
|
||||
@ -135,8 +140,7 @@ class CommandFactoryProvider implements Provider<CommandFactory> {
|
||||
private void log(final int rc) {
|
||||
synchronized (this) {
|
||||
if (!logged) {
|
||||
ctx.finished = System.currentTimeMillis();
|
||||
log.onExecute(ctx, commandLine, rc);
|
||||
log.onExecute(rc);
|
||||
logged = true;
|
||||
}
|
||||
}
|
||||
@ -159,4 +163,41 @@ class CommandFactoryProvider implements Provider<CommandFactory> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Split a command line into a string array. */
|
||||
static String[] split(String commandLine) {
|
||||
final List<String> list = new ArrayList<String>();
|
||||
boolean inquote = false;
|
||||
StringBuilder r = new StringBuilder();
|
||||
for (int ip = 0; ip < commandLine.length();) {
|
||||
final char b = commandLine.charAt(ip++);
|
||||
switch (b) {
|
||||
case '\t':
|
||||
case ' ':
|
||||
if (inquote)
|
||||
r.append(b);
|
||||
else if (r.length() > 0) {
|
||||
list.add(r.toString());
|
||||
r = new StringBuilder();
|
||||
}
|
||||
continue;
|
||||
case '\'':
|
||||
inquote = !inquote;
|
||||
continue;
|
||||
case '\\':
|
||||
if (inquote || ip == commandLine.length())
|
||||
r.append(b); // literal within a quote
|
||||
else
|
||||
r.append(commandLine.charAt(ip++));
|
||||
continue;
|
||||
default:
|
||||
r.append(b);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (r.length() > 0) {
|
||||
list.add(r.toString());
|
||||
}
|
||||
return list.toArray(new String[list.size()]);
|
||||
}
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ class DatabasePasswordAuth implements PasswordAuthenticator {
|
||||
// session, record a login event in the log and add
|
||||
// a close listener to record a logout event.
|
||||
//
|
||||
Context ctx = new Context(sd);
|
||||
Context ctx = new Context(sd, null);
|
||||
Context old = SshScope.set(ctx);
|
||||
try {
|
||||
log.onLogin();
|
||||
@ -89,7 +89,7 @@ class DatabasePasswordAuth implements PasswordAuthenticator {
|
||||
new IoFutureListener<IoFuture>() {
|
||||
@Override
|
||||
public void operationComplete(IoFuture future) {
|
||||
final Context ctx = new Context(sd);
|
||||
final Context ctx = new Context(sd, null);
|
||||
final Context old = SshScope.set(ctx);
|
||||
try {
|
||||
log.onLogout();
|
||||
|
@ -156,7 +156,7 @@ class DatabasePubKeyAuth implements PublickeyAuthenticator {
|
||||
// session, record a login event in the log and add
|
||||
// a close listener to record a logout event.
|
||||
//
|
||||
Context ctx = new Context(sd);
|
||||
Context ctx = new Context(sd, null);
|
||||
Context old = SshScope.set(ctx);
|
||||
try {
|
||||
sshLog.onLogin();
|
||||
@ -168,7 +168,7 @@ class DatabasePubKeyAuth implements PublickeyAuthenticator {
|
||||
new IoFutureListener<IoFuture>() {
|
||||
@Override
|
||||
public void operationComplete(IoFuture future) {
|
||||
final Context ctx = new Context(sd);
|
||||
final Context ctx = new Context(sd, null);
|
||||
final Context old = SshScope.set(ctx);
|
||||
try {
|
||||
sshLog.onLogout();
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -108,7 +108,7 @@ import java.util.List;
|
||||
@Singleton
|
||||
public class SshDaemon extends SshServer implements SshInfo, LifecycleListener {
|
||||
private static final int IANA_SSH_PORT = 22;
|
||||
private static final int DEFAULT_PORT = 29418;
|
||||
public static final int DEFAULT_PORT = 29418;
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SshDaemon.class);
|
||||
|
||||
|
@ -54,11 +54,14 @@ class SshLog implements LifecycleListener {
|
||||
private static final String P_STATUS = "status";
|
||||
|
||||
private final Provider<SshSession> session;
|
||||
private final Provider<Context> context;
|
||||
private final AsyncAppender async;
|
||||
|
||||
@Inject
|
||||
SshLog(final Provider<SshSession> session, final SitePaths site) {
|
||||
SshLog(final Provider<SshSession> session, final Provider<Context> context,
|
||||
final SitePaths site) {
|
||||
this.session = session;
|
||||
this.context = context;
|
||||
|
||||
final DailyRollingFileAppender dst = new DailyRollingFileAppender();
|
||||
dst.setName(LOG_NAME);
|
||||
@ -118,7 +121,11 @@ class SshLog implements LifecycleListener {
|
||||
async.append(event);
|
||||
}
|
||||
|
||||
void onExecute(final Context ctx, final String commandLine, int exitValue) {
|
||||
void onExecute(int exitValue) {
|
||||
final Context ctx = context.get();
|
||||
ctx.finished = System.currentTimeMillis();
|
||||
|
||||
final String commandLine = ctx.getCommandLine();
|
||||
String cmd = QuotedString.BOURNE.quote(commandLine);
|
||||
if (cmd == commandLine) {
|
||||
cmd = "'" + commandLine + "'";
|
||||
|
@ -33,6 +33,7 @@ import com.google.gerrit.sshd.args4j.AccountGroupIdHandler;
|
||||
import com.google.gerrit.sshd.args4j.AccountIdHandler;
|
||||
import com.google.gerrit.sshd.args4j.PatchSetIdHandler;
|
||||
import com.google.gerrit.sshd.args4j.ProjectControlHandler;
|
||||
import com.google.gerrit.sshd.args4j.SocketAddressHandler;
|
||||
import com.google.gerrit.sshd.commands.DefaultCommandModule;
|
||||
import com.google.gerrit.sshd.commands.QueryShell;
|
||||
import com.google.gerrit.util.cli.CmdLineParser;
|
||||
@ -53,8 +54,6 @@ import java.net.SocketAddress;
|
||||
|
||||
/** Configures standard dependencies for {@link SshDaemon}. */
|
||||
public class SshModule extends FactoryModule {
|
||||
private static final String NAME = "Gerrit Code Review";
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bindScope(RequestScoped.class, SshScope.REQUEST);
|
||||
@ -70,7 +69,7 @@ public class SshModule extends FactoryModule {
|
||||
factory(PeerDaemonUser.Factory.class);
|
||||
|
||||
bind(DispatchCommandProvider.class).annotatedWith(Commands.CMD_ROOT)
|
||||
.toInstance(new DispatchCommandProvider(NAME, Commands.CMD_ROOT));
|
||||
.toInstance(new DispatchCommandProvider("", Commands.CMD_ROOT));
|
||||
bind(CommandFactoryProvider.class);
|
||||
bind(CommandFactory.class).toProvider(CommandFactoryProvider.class);
|
||||
|
||||
@ -115,6 +114,7 @@ public class SshModule extends FactoryModule {
|
||||
registerOptionHandler(AccountGroup.Id.class, AccountGroupIdHandler.class);
|
||||
registerOptionHandler(PatchSet.Id.class, PatchSetIdHandler.class);
|
||||
registerOptionHandler(ProjectControl.class, ProjectControlHandler.class);
|
||||
registerOptionHandler(SocketAddress.class, SocketAddressHandler.class);
|
||||
}
|
||||
|
||||
private <T> void registerOptionHandler(Class<T> type,
|
||||
|
@ -26,17 +26,26 @@ import java.util.Map;
|
||||
class SshScope {
|
||||
static class Context {
|
||||
private final SshSession session;
|
||||
private final String commandLine;
|
||||
private final Map<Key<?>, Object> map;
|
||||
|
||||
final long created;
|
||||
volatile long started;
|
||||
volatile long finished;
|
||||
|
||||
Context(final SshSession s) {
|
||||
Context(final SshSession s, final String c) {
|
||||
session = s;
|
||||
commandLine = c;
|
||||
map = new HashMap<Key<?>, Object>();
|
||||
created = System.currentTimeMillis();
|
||||
started = created;
|
||||
|
||||
final long now = System.currentTimeMillis();
|
||||
created = now;
|
||||
started = now;
|
||||
finished = now;
|
||||
}
|
||||
|
||||
String getCommandLine() {
|
||||
return commandLine;
|
||||
}
|
||||
|
||||
synchronized <T> T get(Key<T> key, Provider<T> creator) {
|
||||
|
@ -42,6 +42,17 @@ public class SshSession {
|
||||
this.remoteAsString = format(remoteAddress);
|
||||
}
|
||||
|
||||
SshSession(SshSession parent, SocketAddress peer, CurrentUser user) {
|
||||
this.sessionId = parent.sessionId;
|
||||
this.remoteAddress = peer;
|
||||
if (parent.remoteAddress == peer) {
|
||||
this.remoteAsString = parent.remoteAsString;
|
||||
} else {
|
||||
this.remoteAsString = format(peer) + "/" + parent.remoteAsString;
|
||||
}
|
||||
this.identity = user;
|
||||
}
|
||||
|
||||
/** Unique session number, assigned during connect. */
|
||||
public int getSessionId() {
|
||||
return sessionId;
|
||||
|
145
gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
Normal file
145
gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
Normal file
@ -0,0 +1,145 @@
|
||||
// Copyright (C) 2010 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.reviewdb.Account;
|
||||
import com.google.gerrit.server.AccessPath;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gerrit.server.IdentifiedUser;
|
||||
import com.google.gerrit.server.PeerDaemonUser;
|
||||
import com.google.gerrit.sshd.SshScope.Context;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
|
||||
import org.apache.sshd.server.Command;
|
||||
import org.apache.sshd.server.Environment;
|
||||
import org.kohsuke.args4j.Argument;
|
||||
import org.kohsuke.args4j.Option;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Executes any other command as a different user identity.
|
||||
* <p>
|
||||
* The calling user must be authenticated as a {@link PeerDaemonUser}, which
|
||||
* usually requires public key authentication using this daemon's private host
|
||||
* key, or a key on this daemon's peer host key ring.
|
||||
*/
|
||||
public final class SuExec extends BaseCommand {
|
||||
private final DispatchCommandProvider dispatcher;
|
||||
|
||||
private Provider<CurrentUser> caller;
|
||||
private Provider<SshSession> session;
|
||||
private IdentifiedUser.GenericFactory userFactory;
|
||||
|
||||
@Option(name = "--as", required = true)
|
||||
private Account.Id accountId;
|
||||
|
||||
@Option(name = "--from")
|
||||
private SocketAddress peerAddress;
|
||||
|
||||
@Argument(index = 0, multiValued = true, metaVar = "COMMAND")
|
||||
private List<String> args = new ArrayList<String>();
|
||||
|
||||
private Command cmd;
|
||||
|
||||
@Inject
|
||||
SuExec(@CommandName(Commands.ROOT) final DispatchCommandProvider dispatcher,
|
||||
final Provider<CurrentUser> caller, final Provider<SshSession> session,
|
||||
final IdentifiedUser.GenericFactory userFactory) {
|
||||
this.dispatcher = dispatcher;
|
||||
this.caller = caller;
|
||||
this.session = session;
|
||||
this.userFactory = userFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Environment env) throws IOException {
|
||||
try {
|
||||
if (caller.get() instanceof PeerDaemonUser) {
|
||||
final PeerDaemonUser peer = (PeerDaemonUser) caller.get();
|
||||
|
||||
parseCommandLine();
|
||||
|
||||
final Context ctx = new Context(newSession(), join(args));
|
||||
final Context old = SshScope.set(ctx);
|
||||
try {
|
||||
final BaseCommand cmd = dispatcher.get();
|
||||
cmd.setArguments(args.toArray(new String[args.size()]));
|
||||
provideStateTo(cmd);
|
||||
|
||||
synchronized (this) {
|
||||
this.cmd = cmd;
|
||||
}
|
||||
cmd.start(env);
|
||||
} finally {
|
||||
SshScope.set(old);
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new UnloggedFailure(1, "fatal: Not a peer daemon");
|
||||
}
|
||||
} catch (UnloggedFailure e) {
|
||||
String msg = e.getMessage();
|
||||
if (!msg.endsWith("\n")) {
|
||||
msg += "\n";
|
||||
}
|
||||
err.write(msg.getBytes("UTF-8"));
|
||||
err.flush();
|
||||
onExit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private SshSession newSession() {
|
||||
final SocketAddress peer;
|
||||
if (peerAddress == null) {
|
||||
peer = session.get().getRemoteAddress();
|
||||
} else {
|
||||
peer = peerAddress;
|
||||
}
|
||||
|
||||
return new SshSession(session.get(), peer, userFactory.create(
|
||||
AccessPath.SSH, new Provider<SocketAddress>() {
|
||||
@Override
|
||||
public SocketAddress get() {
|
||||
return peer;
|
||||
}
|
||||
}, accountId));
|
||||
}
|
||||
|
||||
private static String join(List<String> args) {
|
||||
StringBuilder r = new StringBuilder();
|
||||
for (String a : args) {
|
||||
if (r.length() > 0) {
|
||||
r.append(" ");
|
||||
}
|
||||
r.append(a);
|
||||
}
|
||||
return r.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
synchronized (this) {
|
||||
if (cmd != null) {
|
||||
cmd.destroy();
|
||||
cmd = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
// Copyright (C) 2010 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.args4j;
|
||||
|
||||
import com.google.gerrit.server.util.SocketUtil;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.assistedinject.Assisted;
|
||||
|
||||
import org.kohsuke.args4j.CmdLineException;
|
||||
import org.kohsuke.args4j.CmdLineParser;
|
||||
import org.kohsuke.args4j.OptionDef;
|
||||
import org.kohsuke.args4j.spi.OptionHandler;
|
||||
import org.kohsuke.args4j.spi.Parameters;
|
||||
import org.kohsuke.args4j.spi.Setter;
|
||||
|
||||
import java.net.SocketAddress;
|
||||
|
||||
public class SocketAddressHandler extends OptionHandler<SocketAddress> {
|
||||
@SuppressWarnings("unchecked")
|
||||
@Inject
|
||||
public SocketAddressHandler(@Assisted final CmdLineParser parser,
|
||||
@Assisted final OptionDef option, @Assisted final Setter setter) {
|
||||
super(parser, option, setter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int parseArguments(final Parameters params)
|
||||
throws CmdLineException {
|
||||
final String token = params.getParameter(0);
|
||||
try {
|
||||
setter.addValue(SocketUtil.parse(token, 0));
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new CmdLineException(owner, e.getMessage());
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getDefaultMetaVariable() {
|
||||
return "HOST:PORT";
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
// Copyright (C) 2010 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.args4j;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.assistedinject.Assisted;
|
||||
|
||||
import org.kohsuke.args4j.CmdLineException;
|
||||
import org.kohsuke.args4j.CmdLineParser;
|
||||
import org.kohsuke.args4j.OptionDef;
|
||||
import org.kohsuke.args4j.spi.OptionHandler;
|
||||
import org.kohsuke.args4j.spi.Parameters;
|
||||
import org.kohsuke.args4j.spi.Setter;
|
||||
|
||||
public class SubcommandHandler extends OptionHandler<String> {
|
||||
@SuppressWarnings("unchecked")
|
||||
@Inject
|
||||
public SubcommandHandler(@Assisted final CmdLineParser parser,
|
||||
@Assisted final OptionDef option, @Assisted final Setter setter) {
|
||||
super(parser, option, setter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int parseArguments(final Parameters params)
|
||||
throws CmdLineException {
|
||||
setter.addValue(params.getParameter(0));
|
||||
owner.stopOptionParsing();
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getDefaultMetaVariable() {
|
||||
return "COMMAND";
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ import com.google.gerrit.sshd.CommandModule;
|
||||
import com.google.gerrit.sshd.CommandName;
|
||||
import com.google.gerrit.sshd.Commands;
|
||||
import com.google.gerrit.sshd.DispatchCommandProvider;
|
||||
import com.google.gerrit.sshd.SuExec;
|
||||
|
||||
|
||||
/** Register the basic commands any Gerrit server should support. */
|
||||
@ -52,5 +53,7 @@ public class DefaultCommandModule extends CommandModule {
|
||||
command("git-upload-pack").to(Commands.key(git, "upload-pack"));
|
||||
command("git-receive-pack").to(Commands.key(git, "receive-pack"));
|
||||
command("gerrit-receive-pack").to(Commands.key(git, "receive-pack"));
|
||||
|
||||
command("suexec").to(SuExec.class);
|
||||
}
|
||||
}
|
||||
|
@ -56,10 +56,7 @@ final class ScpCommand extends BaseCommand {
|
||||
private IOException error;
|
||||
|
||||
@Override
|
||||
public void setCommandLine(final String line) {
|
||||
super.setCommandLine(line);
|
||||
|
||||
final String[] args = line.split(" ");
|
||||
public void setArguments(final String[] args) {
|
||||
root = "";
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (args[i].charAt(0) == '-') {
|
||||
|
Loading…
Reference in New Issue
Block a user