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
This commit is contained in:
parent
05bd48c6dd
commit
55a0c3f2cc
@ -2713,8 +2713,8 @@ public class MyPlugin implements MailFilter {
|
|||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
[[ssh-command-interception]]
|
[[ssh-command-creation-interception]]
|
||||||
== SSH Command Interception
|
== SSH Command Creation Interception
|
||||||
|
|
||||||
Gerrit provides an extension point that allows a plugin to intercept
|
Gerrit provides an extension point that allows a plugin to intercept
|
||||||
creation of SSH commands and override the functionality with its own
|
creation of SSH commands and override the functionality with its own
|
||||||
@ -2732,6 +2732,40 @@ class MyCommandInterceptor implements SshCreateCommandInterceptor {
|
|||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
|
[[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
|
== SEE ALSO
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import com.google.common.base.Strings;
|
|||||||
import com.google.common.base.Throwables;
|
import com.google.common.base.Throwables;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
import com.google.common.util.concurrent.Atomics;
|
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.extensions.restapi.AuthException;
|
||||||
import com.google.gerrit.server.CurrentUser;
|
import com.google.gerrit.server.CurrentUser;
|
||||||
import com.google.gerrit.server.args4j.SubcommandHandler;
|
import com.google.gerrit.server.args4j.SubcommandHandler;
|
||||||
@ -46,6 +47,7 @@ final class DispatchCommand extends BaseCommand {
|
|||||||
private final PermissionBackend permissionBackend;
|
private final PermissionBackend permissionBackend;
|
||||||
private final Map<String, CommandProvider> commands;
|
private final Map<String, CommandProvider> commands;
|
||||||
private final AtomicReference<Command> atomicCmd;
|
private final AtomicReference<Command> atomicCmd;
|
||||||
|
private final DynamicSet<SshExecuteCommandInterceptor> commandInterceptors;
|
||||||
|
|
||||||
@Argument(index = 0, required = false, metaVar = "COMMAND", handler = SubcommandHandler.class)
|
@Argument(index = 0, required = false, metaVar = "COMMAND", handler = SubcommandHandler.class)
|
||||||
private String commandName;
|
private String commandName;
|
||||||
@ -57,11 +59,13 @@ final class DispatchCommand extends BaseCommand {
|
|||||||
DispatchCommand(
|
DispatchCommand(
|
||||||
CurrentUser user,
|
CurrentUser user,
|
||||||
PermissionBackend permissionBackend,
|
PermissionBackend permissionBackend,
|
||||||
|
DynamicSet<SshExecuteCommandInterceptor> commandInterceptors,
|
||||||
@Assisted Map<String, CommandProvider> all) {
|
@Assisted Map<String, CommandProvider> all) {
|
||||||
this.currentUser = user;
|
this.currentUser = user;
|
||||||
this.permissionBackend = permissionBackend;
|
this.permissionBackend = permissionBackend;
|
||||||
commands = all;
|
commands = all;
|
||||||
atomicCmd = Atomics.newReference();
|
atomicCmd = Atomics.newReference();
|
||||||
|
this.commandInterceptors = commandInterceptors;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, CommandProvider> getMap() {
|
Map<String, CommandProvider> getMap() {
|
||||||
@ -90,19 +94,29 @@ final class DispatchCommand extends BaseCommand {
|
|||||||
|
|
||||||
final Command cmd = p.getProvider().get();
|
final Command cmd = p.getProvider().get();
|
||||||
checkRequiresCapability(cmd);
|
checkRequiresCapability(cmd);
|
||||||
|
String actualCommandName = commandName;
|
||||||
if (cmd instanceof BaseCommand) {
|
if (cmd instanceof BaseCommand) {
|
||||||
final BaseCommand bc = (BaseCommand) cmd;
|
final BaseCommand bc = (BaseCommand) cmd;
|
||||||
if (getName().isEmpty()) {
|
if (!getName().isEmpty()) {
|
||||||
bc.setName(commandName);
|
actualCommandName = getName() + " " + commandName;
|
||||||
} else {
|
|
||||||
bc.setName(getName() + " " + commandName);
|
|
||||||
}
|
}
|
||||||
|
bc.setName(actualCommandName);
|
||||||
bc.setArguments(args.toArray(new String[args.size()]));
|
bc.setArguments(args.toArray(new String[args.size()]));
|
||||||
|
|
||||||
} else if (!args.isEmpty()) {
|
} else if (!args.isEmpty()) {
|
||||||
throw die(commandName + " does not take arguments");
|
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);
|
provideStateTo(cmd);
|
||||||
atomicCmd.set(cmd);
|
atomicCmd.set(cmd);
|
||||||
cmd.start(env);
|
cmd.start(env);
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@ import static com.google.inject.Scopes.SINGLETON;
|
|||||||
|
|
||||||
import com.google.gerrit.extensions.registration.DynamicItem;
|
import com.google.gerrit.extensions.registration.DynamicItem;
|
||||||
import com.google.gerrit.extensions.registration.DynamicMap;
|
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||||
|
import com.google.gerrit.extensions.registration.DynamicSet;
|
||||||
import com.google.gerrit.lifecycle.LifecycleModule;
|
import com.google.gerrit.lifecycle.LifecycleModule;
|
||||||
import com.google.gerrit.server.DynamicOptions;
|
import com.google.gerrit.server.DynamicOptions;
|
||||||
import com.google.gerrit.server.PeerDaemonUser;
|
import com.google.gerrit.server.PeerDaemonUser;
|
||||||
@ -99,6 +100,7 @@ public class SshModule extends LifecycleModule {
|
|||||||
|
|
||||||
DynamicMap.mapOf(binder(), DynamicOptions.DynamicBean.class);
|
DynamicMap.mapOf(binder(), DynamicOptions.DynamicBean.class);
|
||||||
DynamicItem.itemOf(binder(), SshCreateCommandInterceptor.class);
|
DynamicItem.itemOf(binder(), SshCreateCommandInterceptor.class);
|
||||||
|
DynamicSet.setOf(binder(), SshExecuteCommandInterceptor.class);
|
||||||
|
|
||||||
listener().toInstance(registerInParentInjectors());
|
listener().toInstance(registerInParentInjectors());
|
||||||
listener().to(SshLog.class);
|
listener().to(SshLog.class);
|
||||||
|
Loading…
Reference in New Issue
Block a user