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