// 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);
      }
    }
  }
}
