Close open SSH connections upon account deactivation

Deactivating an account prohibits logging in on new SSH connections.
But it did not close already open connections (which may be kept open
long into the future as for example stream-events) and did not
prohibit opening new sessions on existing authenticated connections.

By closing all open sessions for a user upon deactivation, we force the
user through authentication again. Thereby, we lock out inactive users,
as this authentication will fail as the account is now inactive.

Change-Id: Icbe6f04a96297ee7d7e3748562a3f95206fb9ed2
diff --git a/java/com/google/gerrit/sshd/InactiveAccountDisconnector.java b/java/com/google/gerrit/sshd/InactiveAccountDisconnector.java
new file mode 100644
index 0000000..1086626
--- /dev/null
+++ b/java/com/google/gerrit/sshd/InactiveAccountDisconnector.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2020 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.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.events.AccountActivationListener;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.sshd.BaseCommand.Failure;
+import com.google.inject.Inject;
+import java.io.IOException;
+
+/** Closes open SSH connections upon account deactivation. */
+public class InactiveAccountDisconnector implements AccountActivationListener {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final SshDaemon sshDaemon;
+
+  @Inject
+  InactiveAccountDisconnector(SshDaemon sshDaemon) {
+    this.sshDaemon = sshDaemon;
+  }
+
+  @Override
+  public void onAccountDeactivated(int id) {
+    try {
+      SshUtil.forEachSshSession(
+          sshDaemon,
+          (sshId, sshSession, abstractSession, ioSession) -> {
+            CurrentUser sessionUser = sshSession.getUser();
+            if (sessionUser.isIdentifiedUser() && sessionUser.getAccountId().get() == id) {
+              logger.atInfo().log(
+                  "Disconnecting SSH session %s because user %s(%d) got deactivated",
+                  abstractSession, sessionUser.getLoggableName(), id);
+              try {
+                abstractSession.disconnect(-1, "user deactivated");
+              } catch (IOException e) {
+                logger.atWarning().withCause(e).log(
+                    "Failure while deactivating session %s", abstractSession);
+              }
+            }
+          });
+    } catch (Failure e) {
+      // Ssh Daemon no longer running. Since we're only disconnecting connections anyways, this is
+      // most likely ok, so we log only at info level.
+      logger.atInfo().withCause(e).log("Failure while disconnecting deactivated account %d", id);
+    }
+  }
+}
diff --git a/java/com/google/gerrit/sshd/SshModule.java b/java/com/google/gerrit/sshd/SshModule.java
index e4aa14c..9301f8a 100644
--- a/java/com/google/gerrit/sshd/SshModule.java
+++ b/java/com/google/gerrit/sshd/SshModule.java
@@ -19,6 +19,7 @@
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Splitter;
+import com.google.gerrit.extensions.events.AccountActivationListener;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.lifecycle.LifecycleModule;
@@ -103,6 +104,9 @@
     DynamicItem.itemOf(binder(), SshCreateCommandInterceptor.class);
     DynamicSet.setOf(binder(), SshExecuteCommandInterceptor.class);
 
+    DynamicSet.bind(binder(), AccountActivationListener.class)
+        .to(InactiveAccountDisconnector.class);
+
     listener().toInstance(registerInParentInjectors());
     listener().to(SshLog.class);
     listener().to(SshDaemon.class);