Configure allowed SSH commands

Certain SSH commands can still be allowed because
are very useful and do not impact the status of the Gerrit
reviews, configuration or repository.

Allow to configure SSH commands to be permitted even
if the entire Gerrit instance is in read-only mode.

Change-Id: Iaec1403302ad2df67551f51f010192ec266f876d
diff --git a/src/main/java/com/googlesource/gerrit/plugins/readonly/DisableCommandInterceptor.java b/src/main/java/com/googlesource/gerrit/plugins/readonly/DisableCommandInterceptor.java
index 7fec1aa..7bab398 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/readonly/DisableCommandInterceptor.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/readonly/DisableCommandInterceptor.java
@@ -18,6 +18,8 @@
 import com.google.gerrit.sshd.SshCreateCommandInterceptor;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.regex.Pattern;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -28,17 +30,26 @@
   private static final String PATTERN = "^gerrit plugin (\\brm\\b|\\bremove\\b) %s$";
 
   private final String disableCommand;
-  private final Pattern pattern;
+  private final List<Pattern> allowPatterns = new ArrayList<>();
+  private final List<String> allowPrefixes = new ArrayList<>();
 
   @Inject
-  DisableCommandInterceptor(@PluginName String pluginName) {
+  DisableCommandInterceptor(@PluginName String pluginName, ReadOnlyConfig config) {
     this.disableCommand = pluginName + " disable";
-    this.pattern = Pattern.compile(String.format(PATTERN, pluginName));
+    allowPatterns.add(Pattern.compile(String.format(PATTERN, pluginName)));
+    for (String allow : config.allowSshCommands()) {
+      if (allow.startsWith("^")) {
+        allowPatterns.add(Pattern.compile(allow));
+      } else {
+        allowPrefixes.add(allow);
+      }
+    }
   }
 
   @Override
   public String intercept(String in) {
-    if (pattern.matcher(in).matches()) {
+    if (allowPrefixes.stream().anyMatch(p -> in.startsWith(p))
+        || allowPatterns.stream().anyMatch(p -> p.matcher(in).matches())) {
       return in;
     }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyConfig.java b/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyConfig.java
index 718a894..ba87301 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/readonly/ReadOnlyConfig.java
@@ -16,10 +16,12 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.util.List;
 import org.eclipse.jgit.lib.Config;
 
 @Singleton
@@ -27,16 +29,23 @@
   private static final String MESSAGE_KEY = "message";
   private static final String DEFAULT_MESSAGE =
       "Gerrit is under maintenance - all data is READ ONLY";
+  private static final String SSH_ALLOW = "allowSshCommand";
 
   private final String message;
+  private final List<String> allowSshCommands;
 
   @Inject
   ReadOnlyConfig(PluginConfigFactory pluginConfigFactory, @PluginName String pluginName) {
     Config cfg = pluginConfigFactory.getGlobalPluginConfig(pluginName);
     this.message = firstNonNull(cfg.getString(pluginName, null, MESSAGE_KEY), DEFAULT_MESSAGE);
+    allowSshCommands = ImmutableList.copyOf(cfg.getStringList(pluginName, null, SSH_ALLOW));
   }
 
   String message() {
     return message;
   }
+
+  List<String> allowSshCommands() {
+    return allowSshCommands;
+  }
 }
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 883a5c5..2c3cb50 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -19,3 +19,10 @@
 :   Message to be shown to clients when attempting to perform an opeation that
     is blocked due to the server being in read-only mode. When not specified,
     the default is "Gerrit is under maintenance - all data is READ ONLY".
+
+```readonly.allowSshCommand```
+:   Allow one or more SSH commands to be executed. When the allow value starts
+    with a caret '^' then it is interpreted as regex, otherwise as a prefix.
+    Repeat with multiple values to allow more than one command or pattern
+    of commands.
+    The command 'gerrit plugin rm' or 'gerrit plugin remove' is always allowed.