| // 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.entities.Account; | |
| import com.google.gerrit.extensions.restapi.AuthException; | |
| import com.google.gerrit.server.CurrentUser; | |
| import com.google.gerrit.server.DynamicOptions; | |
| 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.channel.ChannelSession; | |
| 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(ChannelSession channel, Environment env) throws IOException { | |
| try (DynamicOptions pluginOptions = new DynamicOptions(injector, dynamicBeans)) { | |
| checkCanRunAs(); | |
| parseCommandLine(pluginOptions); | |
| 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(channel, 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", e); | |
| } 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(ChannelSession channel) { | |
| Command cmd = atomicCmd.getAndSet(null); | |
| if (cmd != null) { | |
| try { | |
| cmd.destroy(channel); | |
| } catch (Exception e) { | |
| Throwables.throwIfUnchecked(e); | |
| throw new RuntimeException(e); | |
| } | |
| } | |
| } | |
| } |