diff options
Diffstat (limited to 'java/com/google/gerrit/sshd/SuExec.java')
-rw-r--r-- | java/com/google/gerrit/sshd/SuExec.java | 172 |
1 files changed, 172 insertions, 0 deletions
diff --git a/java/com/google/gerrit/sshd/SuExec.java b/java/com/google/gerrit/sshd/SuExec.java new file mode 100644 index 0000000000..7053a0d5f4 --- /dev/null +++ b/java/com/google/gerrit/sshd/SuExec.java @@ -0,0 +1,172 @@ +// 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 static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.base.Throwables; +import com.google.common.util.concurrent.Atomics; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.PeerDaemonUser; +import com.google.gerrit.server.config.AuthConfig; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.sshd.SshScope.Context; +import com.google.inject.Inject; +import java.io.IOException; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.sshd.server.Environment; +import org.apache.sshd.server.command.Command; +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.Option; + +/** + * 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 SshScope sshScope; + private final DispatchCommandProvider dispatcher; + private final PermissionBackend permissionBackend; + + private boolean enableRunAs; + private CurrentUser caller; + private SshSession session; + private IdentifiedUser.GenericFactory userFactory; + private SshScope.Context callingContext; + + @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<>(); + + private final AtomicReference<Command> atomicCmd; + + @Inject + SuExec( + final SshScope sshScope, + @CommandName(Commands.ROOT) final DispatchCommandProvider dispatcher, + PermissionBackend permissionBackend, + final CurrentUser caller, + final SshSession session, + final IdentifiedUser.GenericFactory userFactory, + final SshScope.Context callingContext, + AuthConfig config) { + this.sshScope = sshScope; + this.dispatcher = dispatcher; + this.permissionBackend = permissionBackend; + this.caller = caller; + this.session = session; + this.userFactory = userFactory; + this.callingContext = callingContext; + this.enableRunAs = config.isRunAsEnabled(); + atomicCmd = Atomics.newReference(); + } + + @Override + public void start(Environment env) throws IOException { + try { + checkCanRunAs(); + parseCommandLine(); + + final Context ctx = callingContext.subContext(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); + atomicCmd.set(cmd); + cmd.start(env); + } finally { + sshScope.set(old); + } + } catch (UnloggedFailure e) { + String msg = e.getMessage(); + if (!msg.endsWith("\n")) { + msg += "\n"; + } + err.write(msg.getBytes(UTF_8)); + err.flush(); + onExit(1); + } + } + + private void checkCanRunAs() throws UnloggedFailure { + if (caller instanceof PeerDaemonUser) { + // OK. + } else if (!enableRunAs) { + throw die("suexec disabled by auth.enableRunAs = false"); + } else { + try { + permissionBackend.user(caller).check(GlobalPermission.RUN_AS); + } catch (AuthException e) { + throw die("suexec not permitted"); + } catch (PermissionBackendException e) { + throw die("suexec not available: " + e); + } + } + } + + private SshSession newSession() { + final SocketAddress peer; + if (peerAddress == null) { + peer = session.getRemoteAddress(); + } else { + peer = peerAddress; + } + if (caller instanceof PeerDaemonUser) { + caller = null; + } + return new SshSession(session, peer, userFactory.runAs(peer, accountId, caller)); + } + + 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() { + Command cmd = atomicCmd.getAndSet(null); + if (cmd != null) { + try { + cmd.destroy(); + } catch (Exception e) { + Throwables.throwIfUnchecked(e); + throw new RuntimeException(e); + } + } + } +} |