Add a new extension point SshExecuteCommandInterceptor

It allows plugin to intercept ssh commands within the SshScope.

It is added to address some limitations of the current
SshCreateCommandInterceptor extension point by allowing:
- to inject the SshSession within the interceptor (impossible with
  SshCreateCommandInterceptor being injected just before the
  SshContext is created)
- multiple plugins to bind it using DynamicSet bindings

Change-Id: If57cae8f82b48f6c2ae8f71fc6ff2027b51b9e98
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 4234ab2..c23443d 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2713,8 +2713,8 @@
 }
 ----
 
-[[ssh-command-interception]]
-== SSH Command Interception
+[[ssh-command-creation-interception]]
+== SSH Command Creation Interception
 
 Gerrit provides an extension point that allows a plugin to intercept
 creation of SSH commands and override the functionality with its own
@@ -2732,6 +2732,40 @@
 }
 ----
 
+[[ssh-command-execution-interception]]
+== SSH Command Execution Interception
+Gerrit provides an extension point that enables plugins to check and
+prevent an SSH command from being run.
+
+[source, java]
+----
+import com.google.gerrit.sshd.SshExecuteCommandInterceptor;
+
+@Singleton
+public class SshExecuteCommandInterceptorImpl implements SshExecuteCommandInterceptor {
+  private final Provider<SshSession> sessionProvider;
+
+  @Inject
+  SshExecuteCommandInterceptorImpl(Provider<SshSession> sessionProvider) {
+    this.sessionProvider = sessionProvider;
+  }
+
+  @Override
+  public boolean accept(String command, List<String> arguments) {
+    if (command.startsWith("gerrit") && !"10.1.2.3".equals(sessionProvider.get().getRemoteAddressAsString())) {
+      return false;
+    }
+    return true;
+  }
+}
+----
+
+And then declare it in your SSH module:
+[source, java]
+----
+  DynamicSet.bind(binder(), SshExecuteCommandInterceptor.class).to(SshExecuteCommandInterceptorImpl.class);
+----
+
 
 == SEE ALSO
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
index 3f2e258..0da3427 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -18,6 +18,7 @@
 import com.google.common.base.Throwables;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.Atomics;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.args4j.SubcommandHandler;
@@ -46,6 +47,7 @@
   private final PermissionBackend permissionBackend;
   private final Map<String, CommandProvider> commands;
   private final AtomicReference<Command> atomicCmd;
+  private final DynamicSet<SshExecuteCommandInterceptor> commandInterceptors;
 
   @Argument(index = 0, required = false, metaVar = "COMMAND", handler = SubcommandHandler.class)
   private String commandName;
@@ -57,11 +59,13 @@
   DispatchCommand(
       CurrentUser user,
       PermissionBackend permissionBackend,
+      DynamicSet<SshExecuteCommandInterceptor> commandInterceptors,
       @Assisted Map<String, CommandProvider> all) {
     this.currentUser = user;
     this.permissionBackend = permissionBackend;
     commands = all;
     atomicCmd = Atomics.newReference();
+    this.commandInterceptors = commandInterceptors;
   }
 
   Map<String, CommandProvider> getMap() {
@@ -90,19 +94,29 @@
 
       final Command cmd = p.getProvider().get();
       checkRequiresCapability(cmd);
+      String actualCommandName = commandName;
       if (cmd instanceof BaseCommand) {
         final BaseCommand bc = (BaseCommand) cmd;
-        if (getName().isEmpty()) {
-          bc.setName(commandName);
-        } else {
-          bc.setName(getName() + " " + commandName);
+        if (!getName().isEmpty()) {
+          actualCommandName = getName() + " " + commandName;
         }
+        bc.setName(actualCommandName);
         bc.setArguments(args.toArray(new String[args.size()]));
 
       } else if (!args.isEmpty()) {
         throw die(commandName + " does not take arguments");
       }
 
+      for (SshExecuteCommandInterceptor commandInterceptor : commandInterceptors) {
+        if (!commandInterceptor.accept(actualCommandName, args)) {
+          throw new UnloggedFailure(
+              126,
+              String.format(
+                  "blocked by %s, contact gerrit administrators for more details",
+                  commandInterceptor.name()));
+        }
+      }
+
       provideStateTo(cmd);
       atomicCmd.set(cmd);
       cmd.start(env);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshExecuteCommandInterceptor.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshExecuteCommandInterceptor.java
new file mode 100644
index 0000000..ee60670
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshExecuteCommandInterceptor.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2019 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 com.google.gerrit.extensions.annotations.ExtensionPoint;
+import java.util.List;
+
+@ExtensionPoint
+public interface SshExecuteCommandInterceptor {
+
+  /**
+   * Check the command and return false if this command must not be run.
+   *
+   * @param command the command
+   * @param arguments the list of arguments
+   * @return whether or not this command with these arguments can be executed
+   */
+  boolean accept(String command, List<String> arguments);
+
+  default String name() {
+    return this.getClass().getSimpleName();
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
index 4134496..dc88740 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
@@ -19,6 +19,7 @@
 
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.DynamicOptions;
 import com.google.gerrit.server.PeerDaemonUser;
@@ -99,6 +100,7 @@
 
     DynamicMap.mapOf(binder(), DynamicOptions.DynamicBean.class);
     DynamicItem.itemOf(binder(), SshCreateCommandInterceptor.class);
+    DynamicSet.setOf(binder(), SshExecuteCommandInterceptor.class);
 
     listener().toInstance(registerInParentInjectors());
     listener().to(SshLog.class);