Merge "DeleteZombieComments: return the number of deleted zombie comments"
diff --git a/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java b/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
index af5a14e..a056a08 100644
--- a/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
@@ -18,7 +18,7 @@
 import com.google.gerrit.pgm.init.api.InitFlags;
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdFactoryNoteDbImpl;
 import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AuthConfig;
@@ -41,7 +41,7 @@
   private final InitFlags flags;
   private final SitePaths site;
   private final AllUsersName allUsers;
-  private final ExternalIdFactory externalIdFactory;
+  private final ExternalIdFactoryNoteDbImpl externalIdFactory;
   private final AuthConfig authConfig;
 
   @Inject
@@ -49,7 +49,7 @@
       InitFlags flags,
       SitePaths site,
       AllUsersNameOnInitProvider allUsers,
-      ExternalIdFactory externalIdFactory,
+      ExternalIdFactoryNoteDbImpl externalIdFactory,
       AuthConfig authConfig) {
     this.flags = flags;
     this.site = site;
diff --git a/java/com/google/gerrit/pgm/init/InitModule.java b/java/com/google/gerrit/pgm/init/InitModule.java
index b4e427b..473e3aa 100644
--- a/java/com/google/gerrit/pgm/init/InitModule.java
+++ b/java/com/google/gerrit/pgm/init/InitModule.java
@@ -17,7 +17,10 @@
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.pgm.init.api.InitStep;
 import com.google.gerrit.pgm.init.api.Section;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdFactoryNoteDbImpl;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Singleton;
 import com.google.inject.binder.LinkedBindingBuilder;
 import com.google.inject.internal.UniqueAnnotations;
 import java.lang.annotation.Annotation;
@@ -57,6 +60,7 @@
     step().to(InitDev.class);
 
     bind(AccountsOnInit.class).to(AccountsOnInitNoteDbImpl.class);
+    bind(ExternalIdFactory.class).to(ExternalIdFactoryNoteDbImpl.class).in(Singleton.class);
   }
 
   protected LinkedBindingBuilder<InitStep> step() {
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalId.java b/java/com/google/gerrit/server/account/externalids/ExternalId.java
index ef1781e..c39f60b 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -99,10 +99,10 @@
 
   private static final long serialVersionUID = 1L;
 
-  static final String EXTERNAL_ID_SECTION = "externalId";
-  static final String ACCOUNT_ID_KEY = "accountId";
-  static final String EMAIL_KEY = "email";
-  static final String PASSWORD_KEY = "password";
+  public static final String EXTERNAL_ID_SECTION = "externalId";
+  public static final String ACCOUNT_ID_KEY = "accountId";
+  public static final String EMAIL_KEY = "email";
+  public static final String PASSWORD_KEY = "password";
 
   /**
    * Scheme used to label accounts created, when using the LDAP-based authentication types {@link
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java b/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
index cb41fb1..d226565 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdFactory.java
@@ -14,33 +14,10 @@
 
 package com.google.gerrit.server.account.externalids;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Objects.requireNonNull;
-
-import com.google.common.base.Strings;
-import com.google.common.collect.Iterables;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
-import com.google.gerrit.server.account.HashedPassword;
-import com.google.gerrit.server.config.AuthConfig;
-import java.util.Set;
-import javax.inject.Inject;
-import javax.inject.Singleton;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.ObjectId;
 
-@Singleton
-public class ExternalIdFactory {
-  private final ExternalIdKeyFactory externalIdKeyFactory;
-  private AuthConfig authConfig;
-
-  @Inject
-  public ExternalIdFactory(ExternalIdKeyFactory externalIdKeyFactory, AuthConfig authConfig) {
-    this.externalIdKeyFactory = externalIdKeyFactory;
-    this.authConfig = authConfig;
-  }
-
+public interface ExternalIdFactory {
   /**
    * Creates an external ID.
    *
@@ -50,9 +27,7 @@
    * @param accountId the ID of the account to which the external ID belongs
    * @return the created external ID
    */
-  public ExternalId create(String scheme, String id, Account.Id accountId) {
-    return create(externalIdKeyFactory.create(scheme, id), accountId, null, null);
-  }
+  ExternalId create(String scheme, String id, Account.Id accountId);
 
   /**
    * Creates an external ID.
@@ -65,14 +40,12 @@
    * @param hashedPassword the hashed password of the external ID, may be {@code null}
    * @return the created external ID
    */
-  public ExternalId create(
+  ExternalId create(
       String scheme,
       String id,
       Account.Id accountId,
       @Nullable String email,
-      @Nullable String hashedPassword) {
-    return create(externalIdKeyFactory.create(scheme, id), accountId, email, hashedPassword);
-  }
+      @Nullable String hashedPassword);
 
   /**
    * Creates an external ID.
@@ -81,9 +54,7 @@
    * @param accountId the ID of the account to which the external ID belongs
    * @return the created external ID
    */
-  public ExternalId create(ExternalId.Key key, Account.Id accountId) {
-    return create(key, accountId, null, null);
-  }
+  ExternalId create(ExternalId.Key key, Account.Id accountId);
 
   /**
    * Creates an external ID.
@@ -94,14 +65,11 @@
    * @param hashedPassword the hashed password of the external ID, may be {@code null}
    * @return the created external ID
    */
-  public ExternalId create(
+  ExternalId create(
       ExternalId.Key key,
       Account.Id accountId,
       @Nullable String email,
-      @Nullable String hashedPassword) {
-    return create(
-        key, accountId, Strings.emptyToNull(email), Strings.emptyToNull(hashedPassword), null);
-  }
+      @Nullable String hashedPassword);
 
   /**
    * Creates an external ID adding a hashed password computed from a plain password.
@@ -112,16 +80,11 @@
    * @param plainPassword the plain HTTP password, may be {@code null}
    * @return the created external ID
    */
-  public ExternalId createWithPassword(
+  ExternalId createWithPassword(
       ExternalId.Key key,
       Account.Id accountId,
       @Nullable String email,
-      @Nullable String plainPassword) {
-    plainPassword = Strings.emptyToNull(plainPassword);
-    String hashedPassword =
-        plainPassword != null ? HashedPassword.fromPassword(plainPassword).encode() : null;
-    return create(key, accountId, email, hashedPassword);
-  }
+      @Nullable String plainPassword);
 
   /**
    * Create a external ID for a username (scheme "username").
@@ -131,14 +94,7 @@
    * @param plainPassword the plain HTTP password, may be {@code null}
    * @return the created external ID
    */
-  public ExternalId createUsername(
-      String id, Account.Id accountId, @Nullable String plainPassword) {
-    return createWithPassword(
-        externalIdKeyFactory.create(ExternalId.SCHEME_USERNAME, id),
-        accountId,
-        null,
-        plainPassword);
-  }
+  ExternalId createUsername(String id, Account.Id accountId, @Nullable String plainPassword);
 
   /**
    * Creates an external ID with an email.
@@ -150,10 +106,8 @@
    * @param email the email of the external ID, may be {@code null}
    * @return the created external ID
    */
-  public ExternalId createWithEmail(
-      String scheme, String id, Account.Id accountId, @Nullable String email) {
-    return createWithEmail(externalIdKeyFactory.create(scheme, id), accountId, email);
-  }
+  ExternalId createWithEmail(
+      String scheme, String id, Account.Id accountId, @Nullable String email);
 
   /**
    * Creates an external ID with an email.
@@ -163,10 +117,7 @@
    * @param email the email of the external ID, may be {@code null}
    * @return the created external ID
    */
-  public ExternalId createWithEmail(
-      ExternalId.Key key, Account.Id accountId, @Nullable String email) {
-    return create(key, accountId, Strings.emptyToNull(email), null);
-  }
+  ExternalId createWithEmail(ExternalId.Key key, Account.Id accountId, @Nullable String email);
 
   /**
    * Creates an external ID using the `mailto`-scheme.
@@ -175,163 +126,5 @@
    * @param email the email of the external ID, may be {@code null}
    * @return the created external ID
    */
-  public ExternalId createEmail(Account.Id accountId, String email) {
-    return createWithEmail(ExternalId.SCHEME_MAILTO, email, accountId, requireNonNull(email));
-  }
-
-  // TODO(nitzan) change back to package visibility once moved to `storage.notedb` subpackage.
-  public ExternalId create(ExternalId extId, @Nullable ObjectId blobId) {
-    return create(extId.key(), extId.accountId(), extId.email(), extId.password(), blobId);
-  }
-
-  /**
-   * Creates an external ID.
-   *
-   * @param key the external Id key
-   * @param accountId the ID of the account to which the external ID belongs
-   * @param email the email of the external ID, may be {@code null}
-   * @param hashedPassword the hashed password of the external ID, may be {@code null}
-   * @param blobId the ID of the note blob in the external IDs branch that stores this external ID.
-   *     {@code null} if the external ID was created in code and is not yet stored in Git.
-   * @return the created external ID
-   */
-  public ExternalId create(
-      ExternalId.Key key,
-      Account.Id accountId,
-      @Nullable String email,
-      @Nullable String hashedPassword,
-      @Nullable ObjectId blobId) {
-    return ExternalId.create(
-        key, accountId, Strings.emptyToNull(email), Strings.emptyToNull(hashedPassword), blobId);
-  }
-
-  /**
-   * Parses an external ID from a byte array that contains the external ID as a Git config file
-   * text.
-   *
-   * <p>The Git config must have exactly one externalId subsection with an accountId and optionally
-   * email and password:
-   *
-   * <pre>
-   * [externalId "username:jdoe"]
-   *   accountId = 1003407
-   *   email = jdoe@example.com
-   *   password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
-   * </pre>
-   *
-   * @param noteId the SHA-1 sum of the external ID used as the note's ID
-   * @param raw a byte array that contains the external ID as a Git config file text.
-   * @param blobId the ID of the note blob in the external IDs branch that stores this external ID.
-   *     {@code null} if the external ID was created in code and is not yet stored in Git.
-   * @return the parsed external ID
-   */
-  public ExternalId parse(String noteId, byte[] raw, ObjectId blobId)
-      throws ConfigInvalidException {
-    requireNonNull(blobId);
-
-    Config externalIdConfig = new Config();
-    try {
-      externalIdConfig.fromText(new String(raw, UTF_8));
-    } catch (ConfigInvalidException e) {
-      throw invalidConfig(noteId, e.getMessage());
-    }
-
-    Set<String> externalIdKeys = externalIdConfig.getSubsections(ExternalId.EXTERNAL_ID_SECTION);
-    if (externalIdKeys.size() != 1) {
-      throw invalidConfig(
-          noteId,
-          String.format(
-              "Expected exactly 1 '%s' section, found %d",
-              ExternalId.EXTERNAL_ID_SECTION, externalIdKeys.size()));
-    }
-
-    String externalIdKeyStr = Iterables.getOnlyElement(externalIdKeys);
-    ExternalId.Key externalIdKey = externalIdKeyFactory.parse(externalIdKeyStr);
-    if (externalIdKey == null) {
-      throw invalidConfig(noteId, String.format("External ID %s is invalid", externalIdKeyStr));
-    }
-
-    if (!externalIdKey.sha1().getName().equals(noteId)) {
-      if (!authConfig.isUserNameCaseInsensitiveMigrationMode()) {
-        throw invalidConfig(
-            noteId,
-            String.format(
-                "SHA1 of external ID '%s' does not match note ID '%s'", externalIdKeyStr, noteId));
-      }
-
-      if (!externalIdKey.caseSensitiveSha1().getName().equals(noteId)) {
-        throw invalidConfig(
-            noteId,
-            String.format(
-                "Neither case sensitive nor case insensitive SHA1 of external ID '%s' match note ID"
-                    + " '%s'",
-                externalIdKeyStr, noteId));
-      }
-      externalIdKey =
-          externalIdKeyFactory.create(externalIdKey.scheme(), externalIdKey.id(), false);
-    }
-
-    String email =
-        externalIdConfig.getString(
-            ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.EMAIL_KEY);
-    String password =
-        externalIdConfig.getString(
-            ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.PASSWORD_KEY);
-    int accountId = readAccountId(noteId, externalIdConfig, externalIdKeyStr);
-
-    return create(
-        externalIdKey,
-        Account.id(accountId),
-        Strings.emptyToNull(email),
-        Strings.emptyToNull(password),
-        blobId);
-  }
-
-  private static int readAccountId(String noteId, Config externalIdConfig, String externalIdKeyStr)
-      throws ConfigInvalidException {
-    String accountIdStr =
-        externalIdConfig.getString(
-            ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.ACCOUNT_ID_KEY);
-    if (accountIdStr == null) {
-      throw invalidConfig(
-          noteId,
-          String.format(
-              "Value for '%s.%s.%s' is missing, expected account ID",
-              ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.ACCOUNT_ID_KEY));
-    }
-
-    try {
-      int accountId =
-          externalIdConfig.getInt(
-              ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.ACCOUNT_ID_KEY, -1);
-      if (accountId < 0) {
-        throw invalidConfig(
-            noteId,
-            String.format(
-                "Value %s for '%s.%s.%s' is invalid, expected account ID",
-                accountIdStr,
-                ExternalId.EXTERNAL_ID_SECTION,
-                externalIdKeyStr,
-                ExternalId.ACCOUNT_ID_KEY));
-      }
-      return accountId;
-    } catch (IllegalArgumentException e) {
-      ConfigInvalidException newException =
-          invalidConfig(
-              noteId,
-              String.format(
-                  "Value %s for '%s.%s.%s' is invalid, expected account ID",
-                  accountIdStr,
-                  ExternalId.EXTERNAL_ID_SECTION,
-                  externalIdKeyStr,
-                  ExternalId.ACCOUNT_ID_KEY));
-      newException.initCause(e);
-      throw newException;
-    }
-  }
-
-  private static ConfigInvalidException invalidConfig(String noteId, String message) {
-    return new ConfigInvalidException(
-        String.format("Invalid external ID config for note '%s': %s", noteId, message));
-  }
+  ExternalId createEmail(Account.Id accountId, String email);
 }
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java b/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java
index da7b357..2d3e241 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java
@@ -19,7 +19,6 @@
 public class ExternalIdModule extends AbstractModule {
   @Override
   protected void configure() {
-    bind(ExternalIdFactory.class);
     bind(ExternalIdKeyFactory.class);
     bind(PasswordVerifier.class);
   }
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheLoader.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheLoader.java
index 6f9263c..db890fc 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheLoader.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCacheLoader.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -75,7 +74,7 @@
   private final Counter1<Boolean> reloadCounter;
   private final Timer0 reloadDifferential;
   private final boolean isPersistentCache;
-  private final ExternalIdFactory externalIdFactory;
+  private final ExternalIdFactoryNoteDbImpl externalIdFactory;
 
   @Inject
   ExternalIdCacheLoader(
@@ -85,7 +84,7 @@
       @Named(ExternalIdCacheImpl.CACHE_NAME) Cache<ObjectId, AllExternalIds> externalIdCache,
       MetricMaker metricMaker,
       @GerritServerConfig Config config,
-      ExternalIdFactory externalIdFactory) {
+      ExternalIdFactoryNoteDbImpl externalIdFactory) {
     this.externalIdReader = externalIdReader;
     this.externalIdCache = externalIdCache;
     this.gitRepositoryManager = gitRepositoryManager;
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCaseSensitivityMigrator.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCaseSensitivityMigrator.java
index b3abc9a..4e1323e 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCaseSensitivityMigrator.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdCaseSensitivityMigrator.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.exceptions.DuplicateKeyException;
 import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
 import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -60,7 +59,7 @@
   private final Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory;
   private final ExternalIdNotes.FactoryNoReindex externalIdNotesFactory;
 
-  private final ExternalIdFactory externalIdFactory;
+  private final ExternalIdFactoryNoteDbImpl externalIdFactory;
   private final Boolean isUserNameCaseInsensitive;
   private final Boolean dryRun;
 
@@ -70,7 +69,7 @@
       AllUsersName allUsersName,
       Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory,
       ExternalIdNotes.FactoryNoReindex externalIdNotesFactory,
-      ExternalIdFactory externalIdFactory,
+      ExternalIdFactoryNoteDbImpl externalIdFactory,
       @Assisted("isUserNameCaseInsensitive") Boolean isUserNameCaseInsensitive,
       @Assisted("dryRun") Boolean dryRun) {
     this.repoManager = repoManager;
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdFactoryNoteDbImpl.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdFactoryNoteDbImpl.java
new file mode 100644
index 0000000..3462c76
--- /dev/null
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdFactoryNoteDbImpl.java
@@ -0,0 +1,273 @@
+// Copyright (C) 2023 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.server.account.externalids.storage.notedb;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.account.HashedPassword;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
+import com.google.gerrit.server.config.AuthConfig;
+import java.util.Set;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+
+@Singleton
+public class ExternalIdFactoryNoteDbImpl implements ExternalIdFactory {
+  private final ExternalIdKeyFactory externalIdKeyFactory;
+  private AuthConfig authConfig;
+
+  @Inject
+  @VisibleForTesting
+  public ExternalIdFactoryNoteDbImpl(
+      ExternalIdKeyFactory externalIdKeyFactory, AuthConfig authConfig) {
+    this.externalIdKeyFactory = externalIdKeyFactory;
+    this.authConfig = authConfig;
+  }
+
+  @Override
+  public ExternalId create(String scheme, String id, Account.Id accountId) {
+    return create(externalIdKeyFactory.create(scheme, id), accountId, null, null);
+  }
+
+  @Override
+  public ExternalId create(
+      String scheme,
+      String id,
+      Account.Id accountId,
+      @Nullable String email,
+      @Nullable String hashedPassword) {
+    return create(externalIdKeyFactory.create(scheme, id), accountId, email, hashedPassword);
+  }
+
+  @Override
+  public ExternalId create(ExternalId.Key key, Account.Id accountId) {
+    return create(key, accountId, null, null);
+  }
+
+  @Override
+  public ExternalId create(
+      ExternalId.Key key,
+      Account.Id accountId,
+      @Nullable String email,
+      @Nullable String hashedPassword) {
+    return create(
+        key, accountId, Strings.emptyToNull(email), Strings.emptyToNull(hashedPassword), null);
+  }
+
+  ExternalId create(ExternalId extId, @Nullable ObjectId blobId) {
+    return create(extId.key(), extId.accountId(), extId.email(), extId.password(), blobId);
+  }
+
+  /**
+   * Creates an external ID.
+   *
+   * @param key the external Id key
+   * @param accountId the ID of the account to which the external ID belongs
+   * @param email the email of the external ID, may be {@code null}
+   * @param hashedPassword the hashed password of the external ID, may be {@code null}
+   * @param blobId the ID of the note blob in the external IDs branch that stores this external ID.
+   *     {@code null} if the external ID was created in code and is not yet stored in Git.
+   * @return the created external ID
+   */
+  public ExternalId create(
+      ExternalId.Key key,
+      Account.Id accountId,
+      @Nullable String email,
+      @Nullable String hashedPassword,
+      @Nullable ObjectId blobId) {
+    return ExternalId.create(
+        key, accountId, Strings.emptyToNull(email), Strings.emptyToNull(hashedPassword), blobId);
+  }
+
+  @Override
+  public ExternalId createWithPassword(
+      ExternalId.Key key,
+      Account.Id accountId,
+      @Nullable String email,
+      @Nullable String plainPassword) {
+    plainPassword = Strings.emptyToNull(plainPassword);
+    String hashedPassword =
+        plainPassword != null ? HashedPassword.fromPassword(plainPassword).encode() : null;
+    return create(key, accountId, email, hashedPassword);
+  }
+
+  @Override
+  public ExternalId createUsername(
+      String id, Account.Id accountId, @Nullable String plainPassword) {
+    return createWithPassword(
+        externalIdKeyFactory.create(ExternalId.SCHEME_USERNAME, id),
+        accountId,
+        null,
+        plainPassword);
+  }
+
+  @Override
+  public ExternalId createWithEmail(
+      String scheme, String id, Account.Id accountId, @Nullable String email) {
+    return createWithEmail(externalIdKeyFactory.create(scheme, id), accountId, email);
+  }
+
+  @Override
+  public ExternalId createWithEmail(
+      ExternalId.Key key, Account.Id accountId, @Nullable String email) {
+    return create(key, accountId, Strings.emptyToNull(email), null);
+  }
+
+  @Override
+  public ExternalId createEmail(Account.Id accountId, String email) {
+    return createWithEmail(ExternalId.SCHEME_MAILTO, email, accountId, requireNonNull(email));
+  }
+
+  /**
+   * Parses an external ID from a byte array that contains the external ID as a Git config file
+   * text.
+   *
+   * <p>The Git config must have exactly one externalId subsection with an accountId and optionally
+   * email and password:
+   *
+   * <pre>
+   * [externalId "username:jdoe"]
+   *   accountId = 1003407
+   *   email = jdoe@example.com
+   *   password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
+   * </pre>
+   *
+   * @param noteId the SHA-1 sum of the external ID used as the note's ID
+   * @param raw a byte array that contains the external ID as a Git config file text.
+   * @param blobId the ID of the note blob in the external IDs branch that stores this external ID.
+   *     {@code null} if the external ID was created in code and is not yet stored in Git.
+   * @return the parsed external ID
+   */
+  public ExternalId parse(String noteId, byte[] raw, ObjectId blobId)
+      throws ConfigInvalidException {
+    requireNonNull(blobId);
+
+    Config externalIdConfig = new Config();
+    try {
+      externalIdConfig.fromText(new String(raw, UTF_8));
+    } catch (ConfigInvalidException e) {
+      throw invalidConfig(noteId, e.getMessage());
+    }
+
+    Set<String> externalIdKeys = externalIdConfig.getSubsections(ExternalId.EXTERNAL_ID_SECTION);
+    if (externalIdKeys.size() != 1) {
+      throw invalidConfig(
+          noteId,
+          String.format(
+              "Expected exactly 1 '%s' section, found %d",
+              ExternalId.EXTERNAL_ID_SECTION, externalIdKeys.size()));
+    }
+
+    String externalIdKeyStr = Iterables.getOnlyElement(externalIdKeys);
+    ExternalId.Key externalIdKey = externalIdKeyFactory.parse(externalIdKeyStr);
+    if (externalIdKey == null) {
+      throw invalidConfig(noteId, String.format("External ID %s is invalid", externalIdKeyStr));
+    }
+
+    if (!externalIdKey.sha1().getName().equals(noteId)) {
+      if (!authConfig.isUserNameCaseInsensitiveMigrationMode()) {
+        throw invalidConfig(
+            noteId,
+            String.format(
+                "SHA1 of external ID '%s' does not match note ID '%s'", externalIdKeyStr, noteId));
+      }
+
+      if (!externalIdKey.caseSensitiveSha1().getName().equals(noteId)) {
+        throw invalidConfig(
+            noteId,
+            String.format(
+                "Neither case sensitive nor case insensitive SHA1 of external ID '%s' match note ID"
+                    + " '%s'",
+                externalIdKeyStr, noteId));
+      }
+      externalIdKey =
+          externalIdKeyFactory.create(externalIdKey.scheme(), externalIdKey.id(), false);
+    }
+
+    String email =
+        externalIdConfig.getString(
+            ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.EMAIL_KEY);
+    String password =
+        externalIdConfig.getString(
+            ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.PASSWORD_KEY);
+    int accountId = readAccountId(noteId, externalIdConfig, externalIdKeyStr);
+
+    return create(
+        externalIdKey,
+        Account.id(accountId),
+        Strings.emptyToNull(email),
+        Strings.emptyToNull(password),
+        blobId);
+  }
+
+  private static int readAccountId(String noteId, Config externalIdConfig, String externalIdKeyStr)
+      throws ConfigInvalidException {
+    String accountIdStr =
+        externalIdConfig.getString(
+            ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.ACCOUNT_ID_KEY);
+    if (accountIdStr == null) {
+      throw invalidConfig(
+          noteId,
+          String.format(
+              "Value for '%s.%s.%s' is missing, expected account ID",
+              ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.ACCOUNT_ID_KEY));
+    }
+
+    try {
+      int accountId =
+          externalIdConfig.getInt(
+              ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.ACCOUNT_ID_KEY, -1);
+      if (accountId < 0) {
+        throw invalidConfig(
+            noteId,
+            String.format(
+                "Value %s for '%s.%s.%s' is invalid, expected account ID",
+                accountIdStr,
+                ExternalId.EXTERNAL_ID_SECTION,
+                externalIdKeyStr,
+                ExternalId.ACCOUNT_ID_KEY));
+      }
+      return accountId;
+    } catch (IllegalArgumentException e) {
+      ConfigInvalidException newException =
+          invalidConfig(
+              noteId,
+              String.format(
+                  "Value %s for '%s.%s.%s' is invalid, expected account ID",
+                  accountIdStr,
+                  ExternalId.EXTERNAL_ID_SECTION,
+                  externalIdKeyStr,
+                  ExternalId.ACCOUNT_ID_KEY));
+      newException.initCause(e);
+      throw newException;
+    }
+  }
+
+  private static ConfigInvalidException invalidConfig(String noteId, String message) {
+    return new ConfigInvalidException(
+        String.format("Invalid external ID config for note '%s': %s", noteId, message));
+  }
+}
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNoteDbStorageModule.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNoteDbStorageModule.java
index de881f7..99ac568 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNoteDbStorageModule.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNoteDbStorageModule.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.account.externalids.storage.notedb;
 
 import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.account.externalids.ExternalIdUpsertPreprocessor;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.account.externalids.ExternalIdsConsistencyChecker;
@@ -30,5 +31,6 @@
     bind(ExternalIdsConsistencyChecker.class)
         .to(ExternalIdsConsistencyCheckerNoteDbImpl.class)
         .in(Singleton.class);
+    bind(ExternalIdFactory.class).to(ExternalIdFactoryNoteDbImpl.class).in(Singleton.class);
   }
 }
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNotes.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNotes.java
index aac78a4..5346252 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNotes.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNotes.java
@@ -40,7 +40,6 @@
 import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIdCache;
-import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.account.externalids.ExternalIdUpsertPreprocessor;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.config.AllUsersName;
@@ -108,7 +107,7 @@
     protected final MetricMaker metricMaker;
     protected final AllUsersName allUsersName;
     protected final DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors;
-    protected final ExternalIdFactory externalIdFactory;
+    protected final ExternalIdFactoryNoteDbImpl externalIdFactory;
     protected final AuthConfig authConfig;
 
     protected ExternalIdNotesLoader(
@@ -116,7 +115,7 @@
         MetricMaker metricMaker,
         AllUsersName allUsersName,
         DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors,
-        ExternalIdFactory externalIdFactory,
+        ExternalIdFactoryNoteDbImpl externalIdFactory,
         AuthConfig authConfig) {
       this.externalIdCache = externalIdCache;
       this.metricMaker = metricMaker;
@@ -204,7 +203,7 @@
         MetricMaker metricMaker,
         AllUsersName allUsersName,
         DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors,
-        ExternalIdFactory externalIdFactory,
+        ExternalIdFactoryNoteDbImpl externalIdFactory,
         AuthConfig authConfig) {
       super(
           externalIdCache,
@@ -257,7 +256,7 @@
         MetricMaker metricMaker,
         AllUsersName allUsersName,
         DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors,
-        ExternalIdFactory externalIdFactory,
+        ExternalIdFactoryNoteDbImpl externalIdFactory,
         AuthConfig authConfig) {
       super(
           externalIdCache,
@@ -316,7 +315,7 @@
       AllUsersName allUsersName,
       Repository allUsersRepo,
       @Nullable ObjectId rev,
-      ExternalIdFactory externalIdFactory,
+      ExternalIdFactoryNoteDbImpl externalIdFactory,
       boolean isUserNameCaseInsensitiveMigrationMode)
       throws IOException, ConfigInvalidException {
     return new ExternalIdNotes(
@@ -344,7 +343,7 @@
   public static ExternalIdNotes load(
       AllUsersName allUsersName,
       Repository allUsersRepo,
-      ExternalIdFactory externalIdFactory,
+      ExternalIdFactoryNoteDbImpl externalIdFactory,
       boolean isUserNameCaseInsensitiveMigrationMode)
       throws IOException, ConfigInvalidException {
     return new ExternalIdNotes(
@@ -363,7 +362,7 @@
   private final Repository repo;
   private final DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors;
   private final CallerFinder callerFinder;
-  private final ExternalIdFactory externalIdFactory;
+  private final ExternalIdFactoryNoteDbImpl externalIdFactory;
 
   private NoteMap noteMap;
   private ObjectId oldRev;
@@ -407,7 +406,7 @@
       AllUsersName allUsersName,
       Repository allUsersRepo,
       DynamicMap<ExternalIdUpsertPreprocessor> upsertPreprocessors,
-      ExternalIdFactory externalIdFactory,
+      ExternalIdFactoryNoteDbImpl externalIdFactory,
       boolean isUserNameCaseInsensitiveMigrationMode) {
     this.updateCount =
         metricMaker.newCounter(
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdReader.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdReader.java
index 41923d3..dbaed04 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdReader.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdReader.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -72,7 +71,7 @@
   private boolean failOnLoad = false;
   private final Timer0 readAllLatency;
   private final Timer0 readSingleLatency;
-  private final ExternalIdFactory externalIdFactory;
+  private final ExternalIdFactoryNoteDbImpl externalIdFactory;
   private final AuthConfig authConfig;
 
   @VisibleForTesting
@@ -81,7 +80,7 @@
       GitRepositoryManager repoManager,
       AllUsersName allUsersName,
       MetricMaker metricMaker,
-      ExternalIdFactory externalIdFactory,
+      ExternalIdFactoryNoteDbImpl externalIdFactory,
       AuthConfig authConfig) {
     this.repoManager = repoManager;
     this.allUsersName = allUsersName;
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsConsistencyCheckerNoteDbImpl.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsConsistencyCheckerNoteDbImpl.java
index 847b9c5..83c72f1 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsConsistencyCheckerNoteDbImpl.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsConsistencyCheckerNoteDbImpl.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.account.externalids.storage.notedb;
 
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
 import static java.util.stream.Collectors.joining;
 
@@ -45,7 +46,7 @@
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsers;
   private final OutgoingEmailValidator validator;
-  private final ExternalIdFactory externalIdFactory;
+  private final ExternalIdFactoryNoteDbImpl externalIdFactory;
 
   @Inject
   ExternalIdsConsistencyCheckerNoteDbImpl(
@@ -56,7 +57,10 @@
     this.repoManager = repoManager;
     this.allUsers = allUsers;
     this.validator = validator;
-    this.externalIdFactory = externalIdFactory;
+    checkState(
+        externalIdFactory instanceof ExternalIdFactoryNoteDbImpl,
+        "ExternalIdsConsistencyCheckerNoteDbImpl must be initiated with ExternalIdFactoryNoteDbImpl.");
+    this.externalIdFactory = (ExternalIdFactoryNoteDbImpl) externalIdFactory;
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index ba292e6..a3a041b 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -445,12 +445,15 @@
         if (logger.atFine().isEnabled() || LoggingContext.getInstance().isAclLogging()) {
           String logMessage =
               String.format(
-                  "'%s' can perform '%s' with force=%s on project '%s' for ref '%s'",
+                  "'%s' can perform '%s' with force=%s on project '%s' for ref '%s'"
+                      + " (allowed for group '%s' by rule '%s')",
                   getUser().getLoggableName(),
                   permissionName,
                   withForce,
                   projectControl.getProject().getName(),
-                  refName);
+                  refName,
+                  pr.getGroup().getUUID().get(),
+                  pr);
           LoggingContext.getInstance().addAclLogRecord(logMessage);
           logger.atFine().log("%s (caller: %s)", logMessage, callerFinder.findCallerLazy());
         }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 72e5b05..e814138 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -141,8 +141,8 @@
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
 import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdFactoryNoteDbImpl;
 import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes;
 import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdsNoteDbImpl;
 import com.google.gerrit.server.account.storage.notedb.AccountsUpdateNoteDbImpl;
@@ -249,7 +249,7 @@
   @Inject private ExtensionRegistry extensionRegistry;
   @Inject private PluginSetContext<ExceptionHook> exceptionHooks;
   @Inject private ExternalIdKeyFactory externalIdKeyFactory;
-  @Inject private ExternalIdFactory externalIdFactory;
+  @Inject private ExternalIdFactoryNoteDbImpl externalIdFactory;
   @Inject private AuthConfig authConfig;
   @Inject private AccountControl.Factory accountControlFactory;
 
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
index 5c46fec..27193dd 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
@@ -59,6 +59,7 @@
   private Project.NameKey normalProject;
   private Project.NameKey secretProject;
   private Project.NameKey secretRefProject;
+  private AccountGroup.UUID privilegedGroupUuid;
   private TestAccount privilegedUser;
 
   @Before
@@ -66,8 +67,7 @@
     normalProject = projectOperations.newProject().create();
     secretProject = projectOperations.newProject().create();
     secretRefProject = projectOperations.newProject().create();
-    AccountGroup.UUID privilegedGroupUuid =
-        groupOperations.newGroup().name(name("privilegedGroup")).create();
+    privilegedGroupUuid = groupOperations.newGroup().name(name("privilegedGroup")).create();
 
     privilegedUser = accountCreator.create("privilegedUser", "snowden@nsa.gov", "Ed Snowden", null);
     groupOperations.group(privilegedGroupUuid).forUpdate().addMember(privilegedUser.id()).update();
@@ -239,7 +239,9 @@
                 ImmutableList.of(
                     "'user1' can perform 'read' with force=false on project '"
                         + normalProject.get()
-                        + "' for ref 'refs/heads/*'",
+                        + "' for ref 'refs/heads/*' (allowed for group '"
+                        + SystemGroupBackend.ANONYMOUS_USERS.get()
+                        + "' by rule 'group Anonymous Users')",
                     "'user1' cannot perform 'viewPrivateChanges' with force=false on project '"
                         + normalProject.get()
                         + "' for ref 'refs/heads/master'")),
@@ -251,7 +253,9 @@
                 ImmutableList.of(
                     "'user1' can perform 'read' with force=false on project '"
                         + normalProject.get()
-                        + "' for ref 'refs/heads/*'")),
+                        + "' for ref 'refs/heads/*' (allowed for group '"
+                        + SystemGroupBackend.ANONYMOUS_USERS.get()
+                        + "' by rule 'group Anonymous Users')")),
             // Test 3
             TestCase.project(
                 user.email(),
@@ -273,7 +277,13 @@
                 ImmutableList.of(
                     "'user1' can perform 'read' with force=false on project '"
                         + secretRefProject.get()
-                        + "' for ref 'refs/heads/*'",
+                        + "' for ref 'refs/heads/*' (allowed for group '"
+                        + SystemGroupBackend.REGISTERED_USERS.get()
+                        // if the permission was assigned through ProjectOperations the local group
+                        // name is set to the UUID
+                        + "' by rule 'group "
+                        + SystemGroupBackend.REGISTERED_USERS.get()
+                        + "')",
                     "'user1' cannot perform 'read' with force=false on project '"
                         + secretRefProject.get()
                         + "' for ref 'refs/heads/secret/master' because this permission is blocked")),
@@ -286,10 +296,22 @@
                 ImmutableList.of(
                     "'privilegedUser' can perform 'read' with force=false on project '"
                         + secretRefProject.get()
-                        + "' for ref 'refs/heads/*'",
+                        + "' for ref 'refs/heads/*' (allowed for group '"
+                        + SystemGroupBackend.REGISTERED_USERS.get()
+                        // if the permission was assigned through ProjectOperations the local group
+                        // name is set to the UUID
+                        + "' by rule 'group "
+                        + SystemGroupBackend.REGISTERED_USERS.get()
+                        + "')",
                     "'privilegedUser' can perform 'read' with force=false on project '"
                         + secretRefProject.get()
-                        + "' for ref 'refs/heads/secret/master'")),
+                        + "' for ref 'refs/heads/secret/master' (allowed for group '"
+                        + privilegedGroupUuid.get()
+                        // if the permission was assigned through ProjectOperations the local group
+                        // name is set to the UUID
+                        + "' by rule 'group "
+                        + privilegedGroupUuid.get()
+                        + "')")),
             // Test 6
             TestCase.projectRef(
                 privilegedUser.email(),
@@ -299,7 +321,9 @@
                 ImmutableList.of(
                     "'privilegedUser' can perform 'read' with force=false on project '"
                         + normalProject.get()
-                        + "' for ref 'refs/heads/*'")),
+                        + "' for ref 'refs/heads/*' (allowed for group '"
+                        + SystemGroupBackend.ANONYMOUS_USERS.get()
+                        + "' by rule 'group Anonymous Users')")),
             // Test 7
             TestCase.projectRef(
                 privilegedUser.email(),
@@ -309,7 +333,13 @@
                 ImmutableList.of(
                     "'privilegedUser' can perform 'read' with force=false on project '"
                         + secretProject.get()
-                        + "' for ref 'refs/*'")),
+                        + "' for ref 'refs/*' (allowed for group '"
+                        + privilegedGroupUuid.get()
+                        // if the permission was assigned through ProjectOperations the local group
+                        // name is set to the UUID
+                        + "' by rule 'group "
+                        + privilegedGroupUuid.get()
+                        + "')")),
             // Test 8
             TestCase.projectRefPerm(
                 privilegedUser.email(),
@@ -320,10 +350,18 @@
                 ImmutableList.of(
                     "'privilegedUser' can perform 'read' with force=false on project '"
                         + normalProject.get()
-                        + "' for ref 'refs/heads/*'",
+                        + "' for ref 'refs/heads/*' (allowed for group '"
+                        + SystemGroupBackend.ANONYMOUS_USERS.get()
+                        + "' by rule 'group Anonymous Users')",
                     "'privilegedUser' can perform 'viewPrivateChanges' with force=false on project '"
                         + normalProject.get()
-                        + "' for ref 'refs/heads/master'")),
+                        + "' for ref 'refs/heads/master' (allowed for group '"
+                        + privilegedGroupUuid.get()
+                        // if the permission was assigned through ProjectOperations the local group
+                        // name is set to the UUID
+                        + "' by rule 'group "
+                        + privilegedGroupUuid.get()
+                        + "')")),
             // Test 9
             TestCase.projectRefPerm(
                 privilegedUser.email(),
@@ -334,10 +372,18 @@
                 ImmutableList.of(
                     "'privilegedUser' can perform 'read' with force=false on project '"
                         + normalProject.get()
-                        + "' for ref 'refs/heads/*'",
+                        + "' for ref 'refs/heads/*' (allowed for group '"
+                        + SystemGroupBackend.ANONYMOUS_USERS.get()
+                        + "' by rule 'group Anonymous Users')",
                     "'privilegedUser' can perform 'forgeServerAsCommitter' with force=false on project '"
                         + normalProject.get()
-                        + "' for ref 'refs/heads/master'")));
+                        + "' for ref 'refs/heads/master' (allowed for group '"
+                        + privilegedGroupUuid.get()
+                        // if the permission was assigned through ProjectOperations the local group
+                        // name is set to the UUID
+                        + "' by rule 'group "
+                        + privilegedGroupUuid.get()
+                        + "')")));
 
     for (TestCase tc : inputs) {
       String in = newGson().toJson(tc.input);
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
index f825ed0..a76bf47 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -59,9 +59,9 @@
 import com.google.gerrit.server.account.AccountsUpdate;
 import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
 import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdFactoryNoteDbImpl;
 import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes;
 import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdReader;
 import com.google.gerrit.server.config.AllUsersName;
@@ -105,7 +105,7 @@
   @Inject private ProjectOperations projectOperations;
   @Inject private RequestScopeOperations requestScopeOperations;
   @Inject private ExternalIdKeyFactory externalIdKeyFactory;
-  @Inject private ExternalIdFactory externalIdFactory;
+  @Inject private ExternalIdFactoryNoteDbImpl externalIdFactory;
   @Inject private AllUsersName allUsersName;
 
   @Test
diff --git a/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java b/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java
index 04f9827..0389c4f 100644
--- a/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java
+++ b/javatests/com/google/gerrit/httpd/ProjectBasicAuthFilterTest.java
@@ -38,6 +38,7 @@
 import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.PasswordVerifier;
+import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdFactoryNoteDbImpl;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
 import com.google.gerrit.util.http.testutil.FakeHttpServletResponse;
@@ -100,7 +101,7 @@
     res = new FakeHttpServletResponse();
 
     extIdKeyFactory = new ExternalIdKeyFactory(new ExternalIdKeyFactory.ConfigImpl(authConfig));
-    extIdFactory = new ExternalIdFactory(extIdKeyFactory, authConfig);
+    extIdFactory = new ExternalIdFactoryNoteDbImpl(extIdKeyFactory, authConfig);
     authRequestFactory = new AuthRequest.Factory(extIdKeyFactory);
     pwdVerifier = new PasswordVerifier(extIdKeyFactory, authConfig);
 
diff --git a/javatests/com/google/gerrit/server/account/externalids/storage/notedb/AllExternalIdsTest.java b/javatests/com/google/gerrit/server/account/externalids/storage/notedb/AllExternalIdsTest.java
index ccfcf4a..d36f62b 100644
--- a/javatests/com/google/gerrit/server/account/externalids/storage/notedb/AllExternalIdsTest.java
+++ b/javatests/com/google/gerrit/server/account/externalids/storage/notedb/AllExternalIdsTest.java
@@ -23,7 +23,6 @@
 import com.google.common.collect.ImmutableSetMultimap;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.storage.notedb.AllExternalIds.Serializer;
 import com.google.gerrit.server.cache.proto.Cache.AllExternalIdsProto;
@@ -38,14 +37,14 @@
 import org.mockito.Mock;
 
 public class AllExternalIdsTest {
-  private ExternalIdFactory externalIdFactory;
+  private ExternalIdFactoryNoteDbImpl externalIdFactory;
 
   @Mock AuthConfig authConfig;
 
   @Before
   public void setUp() throws Exception {
     externalIdFactory =
-        new ExternalIdFactory(
+        new ExternalIdFactoryNoteDbImpl(
             new ExternalIdKeyFactory(
                 new ExternalIdKeyFactory.Config() {
                   @Override
diff --git a/javatests/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIDCacheLoaderTest.java b/javatests/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIDCacheLoaderTest.java
index 945e6bd..bf38148 100644
--- a/javatests/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIDCacheLoaderTest.java
+++ b/javatests/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIDCacheLoaderTest.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.metrics.DisabledMetricMaker;
 import com.google.gerrit.server.account.externalids.ExternalId;
-import com.google.gerrit.server.account.externalids.ExternalIdFactory;
 import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
 import com.google.gerrit.server.account.externalids.testing.ExternalIdTestUtil;
 import com.google.gerrit.server.config.AllUsersName;
@@ -66,12 +65,13 @@
   private ExternalIdReader externalIdReader;
   private ExternalIdReader externalIdReaderSpy;
 
-  private ExternalIdFactory externalIdFactory;
+  private ExternalIdFactoryNoteDbImpl externalIdFactory;
   @Mock private AuthConfig authConfig;
 
   @Before
   public void setUp() throws Exception {
-    externalIdFactory = new ExternalIdFactory(new ExternalIdKeyFactory(() -> false), authConfig);
+    externalIdFactory =
+        new ExternalIdFactoryNoteDbImpl(new ExternalIdKeyFactory(() -> false), authConfig);
     externalIdCache = CacheBuilder.newBuilder().build();
     repoManager.createRepository(ALL_USERS).close();
     externalIdReader =