suexec: Honor Run As capability like HTTP REST API

This makes the behavior of the two APIs more consistent.  suexec is
still allowed for the magical "Gerrit Code Review" PeerDaemonUser, but
is now also permitted if the Run As capability has been granted.

Change-Id: I9acd6467d9e02084519b30e0c4a3d08065e74484
This commit is contained in:
Shawn Pearce 2013-06-11 13:47:21 -07:00
parent a931fe1bad
commit 08ae5775ea
5 changed files with 50 additions and 27 deletions

View File

@ -1270,9 +1270,10 @@ command, but also to the web UI results pagination size.
Run As Run As
~~~~~~ ~~~~~~
Allow users to impersonate any other user with the X-Gerrit-RunAs Allow users to impersonate any other user with the X-Gerrit-RunAs HTTP
HTTP header on REST API calls. Site administrators do not inherit header on REST API calls or the link:cmd-suexec.html[suexec] SSH
this capability; it must be granted explicitly. command. Site administrators do not inherit this capability; it must
be granted explicitly.
[[capability_runGC]] [[capability_runGC]]

View File

@ -19,10 +19,14 @@ SYNOPSIS
DESCRIPTION DESCRIPTION
----------- -----------
The suexec command can only be invoked by the magic user `Gerrit The suexec command permits executing any other command as any other
Code Review` and permits executing any other command as any other
registered user account. registered user account.
suexec can only be invoked by the magic user `Gerrit Code Review`,
or any user granted granted the link:access-control.html#capability_runAs[Run As]
capability. The run as capability is permitted to be used only if
link:config-gerrit.html[auth.enableRunAs] is true.
OPTIONS OPTIONS
------- -------
@ -39,7 +43,8 @@ COMMAND::
ACCESS ACCESS
------ ------
Caller must be the magic user Gerrit Code Review using the SSH Caller must be the magic user Gerrit Code Review using the SSH
daemon's host key or a key on this daemon's peer host key ring. daemon's host key, or a key on this daemon's peer host key ring,
or a user granted the Run As capability.
SCRIPTING SCRIPTING
--------- ---------

View File

@ -21,14 +21,13 @@ import com.google.gerrit.httpd.restapi.RestApiServlet;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountResolver; import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.AuthConfig;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import com.google.inject.servlet.ServletModule; import com.google.inject.servlet.ServletModule;
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -61,10 +60,10 @@ class RunAsFilter implements Filter {
private final AccountResolver accountResolver; private final AccountResolver accountResolver;
@Inject @Inject
RunAsFilter(@GerritServerConfig Config config, RunAsFilter(AuthConfig config,
Provider<WebSession> session, Provider<WebSession> session,
AccountResolver accountResolver) { AccountResolver accountResolver) {
this.enabled = config.getBoolean("auth", null, "enableRunAs", true); this.enabled = config.isRunAsEnabled();
this.session = session; this.session = session;
this.accountResolver = accountResolver; this.accountResolver = accountResolver;
} }

View File

@ -37,6 +37,7 @@ public class AuthConfig {
private final AuthType authType; private final AuthType authType;
private final String httpHeader; private final String httpHeader;
private final boolean trustContainerAuth; private final boolean trustContainerAuth;
private final boolean enableRunAs;
private final boolean userNameToLowerCase; private final boolean userNameToLowerCase;
private final boolean gitBasicAuth; private final boolean gitBasicAuth;
private final String logoutUrl; private final String logoutUrl;
@ -64,6 +65,7 @@ public class AuthConfig {
cookiePath = cfg.getString("auth", null, "cookiepath"); cookiePath = cfg.getString("auth", null, "cookiepath");
cookieSecure = cfg.getBoolean("auth", "cookiesecure", false); cookieSecure = cfg.getBoolean("auth", "cookiesecure", false);
trustContainerAuth = cfg.getBoolean("auth", "trustContainerAuth", false); trustContainerAuth = cfg.getBoolean("auth", "trustContainerAuth", false);
enableRunAs = cfg.getBoolean("auth", null, "enableRunAs", true);
gitBasicAuth = cfg.getBoolean("auth", "gitBasicAuth", false); gitBasicAuth = cfg.getBoolean("auth", "gitBasicAuth", false);
userNameToLowerCase = cfg.getBoolean("auth", "userNameToLowerCase", false); userNameToLowerCase = cfg.getBoolean("auth", "userNameToLowerCase", false);
@ -164,6 +166,11 @@ public class AuthConfig {
return trustContainerAuth; return trustContainerAuth;
} }
/** @return true if users with Run As capability can impersonate others. */
public boolean isRunAsEnabled() {
return enableRunAs;
}
/** Whether user name should be converted to lower-case before validation */ /** Whether user name should be converted to lower-case before validation */
public boolean isUserNameToLowerCase() { public boolean isUserNameToLowerCase() {
return userNameToLowerCase; return userNameToLowerCase;

View File

@ -19,6 +19,7 @@ import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PeerDaemonUser; import com.google.gerrit.server.PeerDaemonUser;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.sshd.SshScope.Context; import com.google.gerrit.sshd.SshScope.Context;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
@ -45,6 +46,7 @@ public final class SuExec extends BaseCommand {
private final SshScope sshScope; private final SshScope sshScope;
private final DispatchCommandProvider dispatcher; private final DispatchCommandProvider dispatcher;
private boolean enableRunAs;
private Provider<CurrentUser> caller; private Provider<CurrentUser> caller;
private Provider<SshSession> session; private Provider<SshSession> session;
private IdentifiedUser.GenericFactory userFactory; private IdentifiedUser.GenericFactory userFactory;
@ -66,36 +68,34 @@ public final class SuExec extends BaseCommand {
@CommandName(Commands.ROOT) final DispatchCommandProvider dispatcher, @CommandName(Commands.ROOT) final DispatchCommandProvider dispatcher,
final Provider<CurrentUser> caller, final Provider<SshSession> session, final Provider<CurrentUser> caller, final Provider<SshSession> session,
final IdentifiedUser.GenericFactory userFactory, final IdentifiedUser.GenericFactory userFactory,
final SshScope.Context callingContext) { final SshScope.Context callingContext,
AuthConfig config) {
this.sshScope = sshScope; this.sshScope = sshScope;
this.dispatcher = dispatcher; this.dispatcher = dispatcher;
this.caller = caller; this.caller = caller;
this.session = session; this.session = session;
this.userFactory = userFactory; this.userFactory = userFactory;
this.callingContext = callingContext; this.callingContext = callingContext;
this.enableRunAs = config.isRunAsEnabled();
atomicCmd = Atomics.newReference(); atomicCmd = Atomics.newReference();
} }
@Override @Override
public void start(Environment env) throws IOException { public void start(Environment env) throws IOException {
try { try {
if (caller.get() instanceof PeerDaemonUser) { checkCanRunAs();
parseCommandLine(); parseCommandLine();
final Context ctx = callingContext.subContext(newSession(), join(args)); final Context ctx = callingContext.subContext(newSession(), join(args));
final Context old = sshScope.set(ctx); final Context old = sshScope.set(ctx);
try { try {
final BaseCommand cmd = dispatcher.get(); final BaseCommand cmd = dispatcher.get();
cmd.setArguments(args.toArray(new String[args.size()])); cmd.setArguments(args.toArray(new String[args.size()]));
provideStateTo(cmd); provideStateTo(cmd);
atomicCmd.set(cmd); atomicCmd.set(cmd);
cmd.start(env); cmd.start(env);
} finally { } finally {
sshScope.set(old); sshScope.set(old);
}
} else {
throw new UnloggedFailure(1, "fatal: Not a peer daemon");
} }
} catch (UnloggedFailure e) { } catch (UnloggedFailure e) {
String msg = e.getMessage(); String msg = e.getMessage();
@ -108,6 +108,17 @@ public final class SuExec extends BaseCommand {
} }
} }
private void checkCanRunAs() throws UnloggedFailure {
if (caller.get() instanceof PeerDaemonUser) {
// OK.
} else if (!enableRunAs) {
throw new UnloggedFailure(1,
"fatal: suexec disabled by auth.enableRunAs = false");
} else if (!caller.get().getCapabilities().canRunAs()) {
throw new UnloggedFailure(1, "fatal: suexec not permitted");
}
}
private SshSession newSession() { private SshSession newSession() {
final SocketAddress peer; final SocketAddress peer;
if (peerAddress == null) { if (peerAddress == null) {