Trigger audit for GIT over Http commands

Before this change only git over SSH commands were audited, this
change allows auditing of git-receive-pack and git-upload-pack
commands over http.

To allow testing AuditService is now an interface so that a fake
implementation can be injected in the tests.

Bug: Issue 9982
Change-Id: Iffcd0fbd7332ef0f6b4b4a8e57bb5d571ee4cb39
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 9e45953..3d81179 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -109,6 +109,7 @@
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.testutil.ConfigSuite;
+import com.google.gerrit.testutil.FakeAuditService;
 import com.google.gerrit.testutil.FakeEmailSender;
 import com.google.gerrit.testutil.FakeEmailSender.Message;
 import com.google.gerrit.testutil.NoteDbMode;
@@ -215,6 +216,7 @@
   @Inject protected ChangeNoteUtil changeNoteUtil;
   @Inject protected ChangeResource.Factory changeResourceFactory;
   @Inject protected FakeEmailSender sender;
+  @Inject protected FakeAuditService auditService;
   @Inject protected GerritApi gApi;
   @Inject protected GitRepositoryManager repoManager;
   @Inject protected GroupCache groupCache;
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
index f724033..7e5fd5c 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gerrit.server.util.SocketUtil;
 import com.google.gerrit.server.util.SystemLog;
+import com.google.gerrit.testutil.FakeAuditService;
 import com.google.gerrit.testutil.FakeEmailSender;
 import com.google.gerrit.testutil.NoteDbChecker;
 import com.google.gerrit.testutil.NoteDbMode;
@@ -301,6 +302,7 @@
             },
             site);
     daemon.setEmailModuleForTesting(new FakeEmailSender.Module());
+    daemon.setAuditEventModuleForTesting(new FakeAuditService.Module());
     daemon.setAdditionalSysModuleForTesting(testSysModule);
     daemon.setEnableSshd(desc.useSsh());
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java
new file mode 100644
index 0000000..f6d4277
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitOverHttpServletIT.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2018 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.acceptance.git;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.audit.AuditEvent;
+import java.util.Collections;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
+import org.junit.Before;
+import org.junit.Test;
+
+public class GitOverHttpServletIT extends AbstractPushForReview {
+
+  @Before
+  public void beforeEach() throws Exception {
+    CredentialsProvider.setDefault(
+        new UsernamePasswordCredentialsProvider(admin.username, admin.httpPassword));
+    selectProtocol(AbstractPushForReview.Protocol.HTTP);
+    auditService.clearEvents();
+  }
+
+  @Test
+  public void receivePackAuditEventLog() throws Exception {
+    testRepo
+        .git()
+        .push()
+        .setRemote("origin")
+        .setRefSpecs(new RefSpec("HEAD:refs/for/master"))
+        .call();
+
+    // Git smart protocol makes two requests:
+    // https://github.com/git/git/blob/master/Documentation/technical/http-protocol.txt
+    assertThat(auditService.auditEvents.size()).isEqualTo(2);
+
+    AuditEvent e = auditService.auditEvents.get(1);
+    assertThat(e.who.getAccountId()).isEqualTo(admin.id);
+    assertThat(e.what).endsWith("/git-receive-pack");
+    assertThat(e.params).isEmpty();
+  }
+
+  @Test
+  public void uploadPackAuditEventLog() throws Exception {
+    testRepo.git().fetch().call();
+
+    assertThat(auditService.auditEvents.size()).isEqualTo(1);
+
+    AuditEvent e = auditService.auditEvents.get(0);
+    assertThat(e.who.toString()).isEqualTo("ANONYMOUS");
+    assertThat(e.params.get("service"))
+        .containsExactlyElementsIn(Collections.singletonList("git-upload-pack"));
+    assertThat(e.what).endsWith("service=git-upload-pack");
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 329beab..0da2f92 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -15,8 +15,13 @@
 package com.google.gerrit.httpd;
 
 import com.google.common.cache.Cache;
+import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
+import com.google.gerrit.audit.AuditService;
+import com.google.gerrit.audit.HttpAuditEvent;
+import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -141,6 +146,30 @@
     addReceivePackFilter(receiveFilter);
   }
 
+  private static String extractWhat(HttpServletRequest request) {
+    StringBuilder commandName = new StringBuilder(request.getRequestURL());
+    if (request.getQueryString() != null) {
+      commandName.append("?").append(request.getQueryString());
+    }
+    return commandName.toString();
+  }
+
+  private static ListMultimap<String, String> extractParameters(HttpServletRequest request) {
+
+    ListMultimap<String, String> multiMap = ArrayListMultimap.create();
+    if (request.getQueryString() != null) {
+      request
+          .getParameterMap()
+          .forEach(
+              (k, v) -> {
+                for (int i = 0; i < v.length; i++) {
+                  multiMap.put(k, v[i]);
+                }
+              });
+    }
+    return multiMap;
+  }
+
   static class Resolver implements RepositoryResolver<HttpServletRequest> {
     private final GitRepositoryManager manager;
     private final PermissionBackend permissionBackend;
@@ -243,15 +272,21 @@
     private final VisibleRefFilter.Factory refFilterFactory;
     private final UploadValidators.Factory uploadValidatorsFactory;
     private final PermissionBackend permissionBackend;
+    private final Provider<CurrentUser> userProvider;
+    private final AuditService auditService;
 
     @Inject
     UploadFilter(
         VisibleRefFilter.Factory refFilterFactory,
         UploadValidators.Factory uploadValidatorsFactory,
-        PermissionBackend permissionBackend) {
+        PermissionBackend permissionBackend,
+        Provider<CurrentUser> userProvider,
+        AuditService auditService) {
       this.refFilterFactory = refFilterFactory;
       this.uploadValidatorsFactory = uploadValidatorsFactory;
       this.permissionBackend = permissionBackend;
+      this.userProvider = userProvider;
+      this.auditService = auditService;
     }
 
     @Override
@@ -276,7 +311,22 @@
         return;
       } catch (PermissionBackendException e) {
         throw new ServletException(e);
+      } finally {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+        auditService.dispatch(
+            new HttpAuditEvent(
+                httpRequest.getSession().getId(),
+                userProvider.get(),
+                extractWhat(httpRequest),
+                TimeUtil.nowMs(),
+                extractParameters(httpRequest),
+                httpRequest.getMethod(),
+                httpRequest,
+                httpResponse.getStatus(),
+                httpResponse));
       }
+
       // We use getRemoteHost() here instead of getRemoteAddr() because REMOTE_ADDR
       // may have been overridden by a proxy server -- we'll try to avoid this.
       UploadValidators uploadValidators =
@@ -331,13 +381,19 @@
   static class ReceiveFilter implements Filter {
     private final Cache<AdvertisedObjectsCacheKey, Set<ObjectId>> cache;
     private final PermissionBackend permissionBackend;
+    private final Provider<CurrentUser> userProvider;
+    private final AuditService auditService;
 
     @Inject
     ReceiveFilter(
         @Named(ID_CACHE) Cache<AdvertisedObjectsCacheKey, Set<ObjectId>> cache,
-        PermissionBackend permissionBackend) {
+        PermissionBackend permissionBackend,
+        Provider<CurrentUser> userProvider,
+        AuditService auditService) {
       this.cache = cache;
       this.permissionBackend = permissionBackend;
+      this.userProvider = userProvider;
+      this.auditService = auditService;
     }
 
     @Override
@@ -365,6 +421,20 @@
         return;
       } catch (PermissionBackendException e) {
         throw new RuntimeException(e);
+      } finally {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+        auditService.dispatch(
+            new HttpAuditEvent(
+                httpRequest.getSession().getId(),
+                userProvider.get(),
+                extractWhat(httpRequest),
+                TimeUtil.nowMs(),
+                extractParameters(httpRequest),
+                httpRequest.getMethod(),
+                httpRequest,
+                httpResponse.getStatus(),
+                httpResponse));
       }
 
       Capable s = arc.canUpload();
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index edebd9a..5fd3968 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -20,6 +20,7 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.MoreObjects;
+import com.google.gerrit.audit.AuditModule;
 import com.google.gerrit.common.EventBroker;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.elasticsearch.ElasticIndexModule;
@@ -193,6 +194,7 @@
   private AbstractModule luceneModule;
   private Module emailModule;
   private Module testSysModule;
+  private Module auditEventModule;
 
   private Runnable serverStarted;
   private IndexType indexType;
@@ -310,6 +312,11 @@
   }
 
   @VisibleForTesting
+  public void setAuditEventModuleForTesting(Module module) {
+    auditEventModule = module;
+  }
+
+  @VisibleForTesting
   public void setLuceneModule(LuceneIndexModule m) {
     luceneModule = m;
     inMemoryTest = true;
@@ -421,6 +428,11 @@
     } else {
       modules.add(new SmtpEmailSender.Module());
     }
+    if (auditEventModule != null) {
+      modules.add(auditEventModule);
+    } else {
+      modules.add(new AuditModule());
+    }
     modules.add(new SignedTokenEmailTokenVerifier.Module());
     modules.add(new PluginRestApiModule());
     modules.add(new RestCacheAdminModule());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditModule.java b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditModule.java
index aedb8a7..d5bd8d8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditModule.java
@@ -23,6 +23,6 @@
   protected void configure() {
     DynamicSet.setOf(binder(), AuditListener.class);
     DynamicSet.setOf(binder(), GroupMemberAuditListener.class);
-    bind(AuditService.class);
+    bind(AuditService.class).to(AuditServiceImpl.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditService.java b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditService.java
index cc29559..171ff45 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditService.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditService.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2018 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.
@@ -14,76 +14,19 @@
 
 package com.google.gerrit.audit;
 
-import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroupById;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
 import java.util.Collection;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
-@Singleton
-public class AuditService {
-  private static final Logger log = LoggerFactory.getLogger(AuditService.class);
+public interface AuditService {
+  void dispatch(AuditEvent action);
 
-  private final DynamicSet<AuditListener> auditListeners;
-  private final DynamicSet<GroupMemberAuditListener> groupMemberAuditListeners;
+  void dispatchAddAccountsToGroup(Account.Id actor, Collection<AccountGroupMember> added);
 
-  @Inject
-  public AuditService(
-      DynamicSet<AuditListener> auditListeners,
-      DynamicSet<GroupMemberAuditListener> groupMemberAuditListeners) {
-    this.auditListeners = auditListeners;
-    this.groupMemberAuditListeners = groupMemberAuditListeners;
-  }
+  void dispatchDeleteAccountsFromGroup(Account.Id actor, Collection<AccountGroupMember> removed);
 
-  public void dispatch(AuditEvent action) {
-    for (AuditListener auditListener : auditListeners) {
-      auditListener.onAuditableAction(action);
-    }
-  }
+  void dispatchAddGroupsToGroup(Account.Id actor, Collection<AccountGroupById> added);
 
-  public void dispatchAddAccountsToGroup(Account.Id actor, Collection<AccountGroupMember> added) {
-    for (GroupMemberAuditListener auditListener : groupMemberAuditListeners) {
-      try {
-        auditListener.onAddAccountsToGroup(actor, added);
-      } catch (RuntimeException e) {
-        log.error("failed to log add accounts to group event", e);
-      }
-    }
-  }
-
-  public void dispatchDeleteAccountsFromGroup(
-      Account.Id actor, Collection<AccountGroupMember> removed) {
-    for (GroupMemberAuditListener auditListener : groupMemberAuditListeners) {
-      try {
-        auditListener.onDeleteAccountsFromGroup(actor, removed);
-      } catch (RuntimeException e) {
-        log.error("failed to log delete accounts from group event", e);
-      }
-    }
-  }
-
-  public void dispatchAddGroupsToGroup(Account.Id actor, Collection<AccountGroupById> added) {
-    for (GroupMemberAuditListener auditListener : groupMemberAuditListeners) {
-      try {
-        auditListener.onAddGroupsToGroup(actor, added);
-      } catch (RuntimeException e) {
-        log.error("failed to log add groups to group event", e);
-      }
-    }
-  }
-
-  public void dispatchDeleteGroupsFromGroup(
-      Account.Id actor, Collection<AccountGroupById> removed) {
-    for (GroupMemberAuditListener auditListener : groupMemberAuditListeners) {
-      try {
-        auditListener.onDeleteGroupsFromGroup(actor, removed);
-      } catch (RuntimeException e) {
-        log.error("failed to log delete groups from group event", e);
-      }
-    }
-  }
+  void dispatchDeleteGroupsFromGroup(Account.Id actor, Collection<AccountGroupById> removed);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditServiceImpl.java b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditServiceImpl.java
new file mode 100644
index 0000000..940742f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditServiceImpl.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2018 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.audit;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Collection;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class AuditServiceImpl implements AuditService {
+  private static final Logger log = LoggerFactory.getLogger(AuditServiceImpl.class);
+
+  private final DynamicSet<AuditListener> auditListeners;
+  private final DynamicSet<GroupMemberAuditListener> groupMemberAuditListeners;
+
+  @Inject
+  public AuditServiceImpl(
+      DynamicSet<AuditListener> auditListeners,
+      DynamicSet<GroupMemberAuditListener> groupMemberAuditListeners) {
+    this.auditListeners = auditListeners;
+    this.groupMemberAuditListeners = groupMemberAuditListeners;
+  }
+
+  @Override
+  public void dispatch(AuditEvent action) {
+    for (AuditListener auditListener : auditListeners) {
+      auditListener.onAuditableAction(action);
+    }
+  }
+
+  @Override
+  public void dispatchAddAccountsToGroup(Account.Id actor, Collection<AccountGroupMember> added) {
+    for (GroupMemberAuditListener auditListener : groupMemberAuditListeners) {
+      try {
+        auditListener.onAddAccountsToGroup(actor, added);
+      } catch (RuntimeException e) {
+        log.error("failed to log add accounts to group event", e);
+      }
+    }
+  }
+
+  @Override
+  public void dispatchDeleteAccountsFromGroup(
+      Account.Id actor, Collection<AccountGroupMember> removed) {
+    for (GroupMemberAuditListener auditListener : groupMemberAuditListeners) {
+      try {
+        auditListener.onDeleteAccountsFromGroup(actor, removed);
+      } catch (RuntimeException e) {
+        log.error("failed to log delete accounts from group event", e);
+      }
+    }
+  }
+
+  @Override
+  public void dispatchAddGroupsToGroup(Account.Id actor, Collection<AccountGroupById> added) {
+    for (GroupMemberAuditListener auditListener : groupMemberAuditListeners) {
+      try {
+        auditListener.onAddGroupsToGroup(actor, added);
+      } catch (RuntimeException e) {
+        log.error("failed to log add groups to group event", e);
+      }
+    }
+  }
+
+  @Override
+  public void dispatchDeleteGroupsFromGroup(
+      Account.Id actor, Collection<AccountGroupById> removed) {
+    for (GroupMemberAuditListener auditListener : groupMemberAuditListeners) {
+      try {
+        auditListener.onDeleteGroupsFromGroup(actor, removed);
+      } catch (RuntimeException e) {
+        log.error("failed to log delete groups from group event", e);
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 2e2d675..773d4a9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -17,7 +17,6 @@
 import static com.google.inject.Scopes.SINGLETON;
 
 import com.google.common.cache.Cache;
-import com.google.gerrit.audit.AuditModule;
 import com.google.gerrit.common.EventListener;
 import com.google.gerrit.common.UserScopedEventListener;
 import com.google.gerrit.extensions.annotations.Exports;
@@ -299,7 +298,6 @@
     bind(IdentifiedUser.GenericFactory.class).in(SINGLETON);
     bind(AccountControl.Factory.class);
 
-    install(new AuditModule());
     bind(UiActions.class);
     install(new com.google.gerrit.server.access.Module());
     install(new com.google.gerrit.server.account.Module());
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAuditService.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAuditService.java
new file mode 100644
index 0000000..1eb5bdb
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAuditService.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2018 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.testutil;
+
+import com.google.gerrit.audit.AuditEvent;
+import com.google.gerrit.audit.AuditService;
+import com.google.gerrit.audit.GroupMemberAuditListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroupById;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+@Singleton
+public class FakeAuditService implements AuditService {
+
+  private final DynamicSet<GroupMemberAuditListener> groupMemberAuditListeners;
+
+  public static class Module extends AbstractModule {
+    @Override
+    public void configure() {
+      DynamicSet.setOf(binder(), GroupMemberAuditListener.class);
+      bind(AuditService.class).to(FakeAuditService.class);
+    }
+  }
+
+  @Inject
+  public FakeAuditService(DynamicSet<GroupMemberAuditListener> groupMemberAuditListeners) {
+    this.groupMemberAuditListeners = groupMemberAuditListeners;
+  }
+
+  public List<AuditEvent> auditEvents = new ArrayList<>();
+
+  public void clearEvents() {
+    auditEvents.clear();
+  }
+
+  @Override
+  public void dispatch(AuditEvent action) {
+    auditEvents.add(action);
+  }
+
+  @Override
+  public void dispatchAddAccountsToGroup(Account.Id actor, Collection<AccountGroupMember> added) {
+    for (GroupMemberAuditListener auditListener : groupMemberAuditListeners) {
+      auditListener.onAddAccountsToGroup(actor, added);
+    }
+  }
+
+  @Override
+  public void dispatchDeleteAccountsFromGroup(
+      Account.Id actor, Collection<AccountGroupMember> removed) {
+    for (GroupMemberAuditListener auditListener : groupMemberAuditListeners) {
+      auditListener.onDeleteAccountsFromGroup(actor, removed);
+    }
+  }
+
+  @Override
+  public void dispatchAddGroupsToGroup(Account.Id actor, Collection<AccountGroupById> added) {
+    for (GroupMemberAuditListener auditListener : groupMemberAuditListeners) {
+      auditListener.onAddGroupsToGroup(actor, added);
+    }
+  }
+
+  @Override
+  public void dispatchDeleteGroupsFromGroup(
+      Account.Id actor, Collection<AccountGroupById> removed) {
+    for (GroupMemberAuditListener auditListener : groupMemberAuditListeners) {
+      auditListener.onDeleteGroupsFromGroup(actor, removed);
+    }
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
index 5f144e5..8835150 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
@@ -19,6 +19,7 @@
 
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.audit.AuditModule;
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
@@ -162,6 +163,7 @@
     install(new DefaultPermissionBackendModule());
     install(new SearchingChangeCacheImpl.Module());
     factory(GarbageCollection.Factory.class);
+    install(new AuditModule());
 
     bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);