summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/sshd/SuExec.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/sshd/SuExec.java')
-rw-r--r--java/com/google/gerrit/sshd/SuExec.java172
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);
+ }
+ }
+ }
+}