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

@@ -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;
}
}
}
}