Line data Source code
1 : // Copyright (C) 2010 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.sshd; 16 : 17 : import static java.nio.charset.StandardCharsets.UTF_8; 18 : 19 : import com.google.common.base.Throwables; 20 : import com.google.common.util.concurrent.Atomics; 21 : import com.google.gerrit.entities.Account; 22 : import com.google.gerrit.extensions.restapi.AuthException; 23 : import com.google.gerrit.server.CurrentUser; 24 : import com.google.gerrit.server.DynamicOptions; 25 : import com.google.gerrit.server.IdentifiedUser; 26 : import com.google.gerrit.server.PeerDaemonUser; 27 : import com.google.gerrit.server.config.AuthConfig; 28 : import com.google.gerrit.server.permissions.GlobalPermission; 29 : import com.google.gerrit.server.permissions.PermissionBackend; 30 : import com.google.gerrit.server.permissions.PermissionBackendException; 31 : import com.google.gerrit.sshd.SshScope.Context; 32 : import com.google.inject.Inject; 33 : import java.io.IOException; 34 : import java.net.SocketAddress; 35 : import java.util.ArrayList; 36 : import java.util.List; 37 : import java.util.concurrent.atomic.AtomicReference; 38 : import org.apache.sshd.server.Environment; 39 : import org.apache.sshd.server.channel.ChannelSession; 40 : import org.apache.sshd.server.command.Command; 41 : import org.kohsuke.args4j.Argument; 42 : import org.kohsuke.args4j.Option; 43 : 44 : /** 45 : * Executes any other command as a different user identity. 46 : * 47 : * <p>The calling user must be authenticated as a {@link PeerDaemonUser}, which usually requires 48 : * public key authentication using this daemon's private host key, or a key on this daemon's peer 49 : * host key ring. 50 : */ 51 : public final class SuExec extends BaseCommand { 52 : private final SshScope sshScope; 53 : private final DispatchCommandProvider dispatcher; 54 : private final PermissionBackend permissionBackend; 55 : 56 : private boolean enableRunAs; 57 : private CurrentUser caller; 58 : private SshSession session; 59 : private IdentifiedUser.GenericFactory userFactory; 60 : private SshScope.Context callingContext; 61 : 62 : @Option(name = "--as", required = true) 63 : private Account.Id accountId; 64 : 65 : @Option(name = "--from") 66 : private SocketAddress peerAddress; 67 : 68 1 : @Argument(index = 0, multiValued = true, metaVar = "COMMAND") 69 : private List<String> args = new ArrayList<>(); 70 : 71 : private final AtomicReference<Command> atomicCmd; 72 : 73 : @Inject 74 : SuExec( 75 : final SshScope sshScope, 76 : @CommandName(Commands.ROOT) final DispatchCommandProvider dispatcher, 77 : PermissionBackend permissionBackend, 78 : final CurrentUser caller, 79 : final SshSession session, 80 : final IdentifiedUser.GenericFactory userFactory, 81 : final SshScope.Context callingContext, 82 1 : AuthConfig config) { 83 1 : this.sshScope = sshScope; 84 1 : this.dispatcher = dispatcher; 85 1 : this.permissionBackend = permissionBackend; 86 1 : this.caller = caller; 87 1 : this.session = session; 88 1 : this.userFactory = userFactory; 89 1 : this.callingContext = callingContext; 90 1 : this.enableRunAs = config.isRunAsEnabled(); 91 1 : atomicCmd = Atomics.newReference(); 92 1 : } 93 : 94 : @Override 95 : public void start(ChannelSession channel, Environment env) throws IOException { 96 1 : try (DynamicOptions pluginOptions = new DynamicOptions(injector, dynamicBeans)) { 97 1 : checkCanRunAs(); 98 1 : parseCommandLine(pluginOptions); 99 : 100 1 : final Context ctx = callingContext.subContext(newSession(), join(args)); 101 1 : final Context old = sshScope.set(ctx); 102 : try { 103 1 : final BaseCommand cmd = dispatcher.get(); 104 1 : cmd.setArguments(args.toArray(new String[args.size()])); 105 1 : provideStateTo(cmd); 106 1 : atomicCmd.set(cmd); 107 1 : cmd.start(channel, env); 108 : } finally { 109 1 : sshScope.set(old); 110 : } 111 0 : } catch (UnloggedFailure e) { 112 0 : String msg = e.getMessage(); 113 0 : if (!msg.endsWith("\n")) { 114 0 : msg += "\n"; 115 : } 116 0 : err.write(msg.getBytes(UTF_8)); 117 0 : err.flush(); 118 0 : onExit(1); 119 1 : } 120 1 : } 121 : 122 : private void checkCanRunAs() throws UnloggedFailure { 123 1 : if (caller instanceof PeerDaemonUser) { 124 : // OK. 125 0 : } else if (!enableRunAs) { 126 0 : throw die("suexec disabled by auth.enableRunAs = false"); 127 : } else { 128 : try { 129 0 : permissionBackend.user(caller).check(GlobalPermission.RUN_AS); 130 0 : } catch (AuthException e) { 131 0 : throw die("suexec not permitted", e); 132 0 : } catch (PermissionBackendException e) { 133 0 : throw die("suexec not available", e); 134 0 : } 135 : } 136 1 : } 137 : 138 : private SshSession newSession() { 139 : final SocketAddress peer; 140 1 : if (peerAddress == null) { 141 1 : peer = session.getRemoteAddress(); 142 : } else { 143 0 : peer = peerAddress; 144 : } 145 1 : if (caller instanceof PeerDaemonUser) { 146 1 : caller = null; 147 : } 148 1 : return new SshSession(session, peer, userFactory.runAs(peer, accountId, caller)); 149 : } 150 : 151 : private static String join(List<String> args) { 152 1 : StringBuilder r = new StringBuilder(); 153 1 : for (String a : args) { 154 1 : if (r.length() > 0) { 155 1 : r.append(" "); 156 : } 157 1 : r.append(a); 158 1 : } 159 1 : return r.toString(); 160 : } 161 : 162 : @Override 163 : public void destroy(ChannelSession channel) { 164 1 : Command cmd = atomicCmd.getAndSet(null); 165 1 : if (cmd != null) { 166 : try { 167 1 : cmd.destroy(channel); 168 0 : } catch (Exception e) { 169 0 : Throwables.throwIfUnchecked(e); 170 0 : throw new RuntimeException(e); 171 1 : } 172 : } 173 1 : } 174 : }