diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt index c65acb7b1f..9a6350318f 100644 --- a/Documentation/dev-plugins.txt +++ b/Documentation/dev-plugins.txt @@ -2786,8 +2786,8 @@ public class MyPlugin implements MailFilter { } ---- -[[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 @@ -2910,6 +2910,40 @@ public class MyPluginModule extends AbstractModule { } ---- +[[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 sessionProvider; + + @Inject + SshExecuteCommandInterceptorImpl(Provider sessionProvider) { + this.sessionProvider = sessionProvider; + } + + @Override + public boolean accept(String command, List 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); +---- + Plugin authors should also consider binding their SubmitRule using a `Gerrit-BatchModule`. See link:dev-plugins.html[Batch runtime] for more informations. diff --git a/java/com/google/gerrit/sshd/DispatchCommand.java b/java/com/google/gerrit/sshd/DispatchCommand.java index 4c9ca91471..68962db18b 100644 --- a/java/com/google/gerrit/sshd/DispatchCommand.java +++ b/java/com/google/gerrit/sshd/DispatchCommand.java @@ -18,6 +18,7 @@ import com.google.common.base.Strings; 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.args4j.SubcommandHandler; import com.google.gerrit.server.permissions.GlobalPermission; @@ -44,6 +45,7 @@ final class DispatchCommand extends BaseCommand { private final PermissionBackend permissionBackend; private final Map commands; private final AtomicReference atomicCmd; + private final DynamicSet commandInterceptors; @Argument(index = 0, required = false, metaVar = "COMMAND", handler = SubcommandHandler.class) private String commandName; @@ -52,10 +54,14 @@ final class DispatchCommand extends BaseCommand { private List args = new ArrayList<>(); @Inject - DispatchCommand(PermissionBackend permissionBackend, @Assisted Map all) { + DispatchCommand( + PermissionBackend permissionBackend, + DynamicSet commandInterceptors, + @Assisted Map all) { this.permissionBackend = permissionBackend; commands = all; atomicCmd = Atomics.newReference(); + this.commandInterceptors = commandInterceptors; } Map getMap() { @@ -84,19 +90,29 @@ final class DispatchCommand extends BaseCommand { 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/java/com/google/gerrit/sshd/SshExecuteCommandInterceptor.java b/java/com/google/gerrit/sshd/SshExecuteCommandInterceptor.java new file mode 100644 index 0000000000..ee606706f9 --- /dev/null +++ b/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 arguments); + + default String name() { + return this.getClass().getSimpleName(); + } +} diff --git a/java/com/google/gerrit/sshd/SshModule.java b/java/com/google/gerrit/sshd/SshModule.java index acdc9582ed..df1f633f94 100644 --- a/java/com/google/gerrit/sshd/SshModule.java +++ b/java/com/google/gerrit/sshd/SshModule.java @@ -106,6 +106,7 @@ public class SshModule extends LifecycleModule { DynamicMap.mapOf(binder(), DynamicOptions.DynamicBean.class); DynamicItem.itemOf(binder(), SshCreateCommandInterceptor.class); + DynamicSet.setOf(binder(), SshExecuteCommandInterceptor.class); listener().toInstance(registerInParentInjectors()); listener().to(SshLog.class);