Add online migration to migrate external IDs to work case insensitively
Add ssh command to trigger online convertion of the name of notes of
external IDs to be the sha sums of the lowercase external ID key while
the node is up and serving the traffic.
Bug: Issue 15136
Change-Id: I3aef027e7907400836912bf1e59d47e1276a691a
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index ab341e8..ce3a024 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -157,6 +157,9 @@
link:cmd-ls-user-refs.html[gerrit ls-user-refs]::
Lists refs visible for a specified user.
+link:cmd-migrate-externalids-to-insensitive.html[gerrit migrate-externalids-to-insensitive]::
+ Migrate external-ids to case insensitive.
+
link:cmd-plugin-install.html[gerrit plugin add]::
Alias for 'gerrit plugin install'.
diff --git a/Documentation/cmd-migrate-externalids-to-insensitive.txt b/Documentation/cmd-migrate-externalids-to-insensitive.txt
new file mode 100644
index 0000000..b023089
--- /dev/null
+++ b/Documentation/cmd-migrate-externalids-to-insensitive.txt
@@ -0,0 +1,44 @@
+= gerrit migrate-externalids-to-insensitive
+
+== NAME
+gerrit migrate-externalids-to-insensitive - Migrate external-ids to case insensitive.
+
+== SYNOPSIS
+[verse]
+--
+_ssh_ -p <port> <host> _gerrit migrate-externalids-to-insensitive_
+--
+
+== DESCRIPTION
+This command allows to trigger online conversion of `username` and
+`gerrit` external IDs to be handled case insensitively. This is done by
+recomputing the name of the note from the sha1 sum of the all lowercase
+external ID key, instead of preserving the key capitalization.
+
+The command requires link:#auth.userNameCaseInsensitive[auth.userNameCaseInsensitive] and
+link:#auth.userNameCaseInsensitiveMigrationMode[auth.userNameCaseInsensitiveMigrationMode] to
+be set to true to perform the migration.
+
+After the successful migration
+link:#auth.userNameCaseInsensitiveMigrationMode[auth.userNameCaseInsensitiveMigrationMode] is
+set to false.
+
+== ACCESS
+Caller must be a member of the privileged 'Administrators' group.
+
+== SCRIPTING
+This command is intended to be used in scripts.
+
+== EXAMPLES
+Start the online external ids migration:
+
+----
+$ ssh -p 29418 review.example.com gerrit migrate-externalids-to-insensitive
+----
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index 13df520..483bff9 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -53,6 +53,7 @@
import com.google.gerrit.server.StartupChecks.StartupChecksModule;
import com.google.gerrit.server.account.AccountDeactivator.AccountDeactivatorModule;
import com.google.gerrit.server.account.InternalAccountDirectory.InternalAccountDirectoryModule;
+import com.google.gerrit.server.account.externalids.ExternalIdCaseSensitivityMigrator;
import com.google.gerrit.server.api.GerritApiModule;
import com.google.gerrit.server.api.PluginApiModule;
import com.google.gerrit.server.audit.AuditModule;
@@ -106,6 +107,7 @@
import com.google.gerrit.sshd.SshModule;
import com.google.gerrit.sshd.SshSessionFactoryInitializer;
import com.google.gerrit.sshd.commands.DefaultCommandModule;
+import com.google.gerrit.sshd.commands.ExternalIdCommandsModule;
import com.google.gerrit.sshd.commands.IndexCommandsModule;
import com.google.gerrit.sshd.commands.SequenceCommandsModule;
import com.google.gerrit.sshd.plugin.LfsPluginAuthCommand.LfsPluginAuthCommandModule;
@@ -357,6 +359,7 @@
modules.add(new ChangeCleanupRunnerModule());
modules.add(new AccountDeactivatorModule());
modules.add(new DefaultProjectNameLockManagerModule());
+ modules.add(new ExternalIdCaseSensitivityMigrator.ExternalIdCaseSensitivityMigratorModule());
return dbInjector.createChildInjector(
ModuleOverloader.override(
modules, LibModuleLoader.loadModules(cfgInjector, LibModuleType.SYS_MODULE)));
@@ -400,6 +403,7 @@
sysInjector.getInstance(LfsPluginAuthCommandModule.class)));
modules.add(new IndexCommandsModule(sysInjector));
modules.add(new SequenceCommandsModule());
+ modules.add(new ExternalIdCommandsModule());
return sysInjector.createChildInjector(modules);
}
diff --git a/java/com/google/gerrit/pgm/ChangeExternalIdCaseSensitivity.java b/java/com/google/gerrit/pgm/ChangeExternalIdCaseSensitivity.java
index 01c76c1..2b7f23e 100644
--- a/java/com/google/gerrit/pgm/ChangeExternalIdCaseSensitivity.java
+++ b/java/com/google/gerrit/pgm/ChangeExternalIdCaseSensitivity.java
@@ -14,11 +14,7 @@
package com.google.gerrit.pgm;
-import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
-import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
-
import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.exceptions.DuplicateKeyException;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.lifecycle.LifecycleManager;
@@ -26,28 +22,23 @@
import com.google.gerrit.pgm.util.SiteProgram;
import com.google.gerrit.server.account.externalids.DisabledExternalIdCache;
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.ExternalIdNotes;
+import com.google.gerrit.server.account.externalids.ExternalIdCaseSensitivityMigrator;
import com.google.gerrit.server.account.externalids.ExternalIdUpsertPreprocessor;
import com.google.gerrit.server.account.externalids.ExternalIds;
-import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
import com.google.gerrit.server.schema.NoteDbSchemaVersionCheck;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Key;
-import com.google.inject.Provider;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
import java.io.IOException;
import java.util.Collection;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ProgressMonitor;
-import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.TextProgressMonitor;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.FS;
@@ -73,12 +64,8 @@
private boolean isUserNameCaseInsensitive;
private ConsoleUI ui;
- @Inject private GitRepositoryManager repoManager;
- @Inject private AllUsersName allUsersName;
- @Inject private Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory;
- @Inject private ExternalIdNotes.FactoryNoReindex externalIdNotesFactory;
@Inject private ExternalIds externalIds;
- @Inject private ExternalIdFactory externalIdFactory;
+ @Inject private ExternalIdCaseSensitivityMigrator.Factory migratorFactory;
@Override
public int run() throws Exception {
@@ -93,6 +80,9 @@
@Override
protected void configure() {
bind(GitReferenceUpdated.class).toInstance(GitReferenceUpdated.DISABLED);
+ install(
+ new FactoryModuleBuilder()
+ .build(ExternalIdCaseSensitivityMigrator.Factory.class));
factory(MetaDataUpdate.InternalFactory.class);
DynamicMap.mapOf(binder(), ExternalIdUpsertPreprocessor.class);
@@ -122,23 +112,9 @@
manager.start();
try {
- try (Repository repo = repoManager.openRepository(allUsersName)) {
- ExternalIdNotes extIdNotes = externalIdNotesFactory.load(repo);
- for (ExternalId extId : todo) {
- recomputeExternalIdNoteId(extIdNotes, extId);
- monitor.update(1);
- }
- if (!dryrun) {
- try (MetaDataUpdate metaDataUpdate =
- metaDataUpdateServerFactory.get().create(allUsersName)) {
- metaDataUpdate.setMessage(
- String.format(
- "Migration to case %ssensitive usernames",
- isUserNameCaseInsensitive ? "" : "in"));
- extIdNotes.commit(metaDataUpdate);
- }
- }
- }
+ migratorFactory
+ .create(!isUserNameCaseInsensitive, dryrun)
+ .migrate(todo, () -> monitor.update(1));
} finally {
manager.stop();
monitor.endTask();
@@ -155,28 +131,6 @@
return exitCode;
}
- private void recomputeExternalIdNoteId(ExternalIdNotes extIdNotes, ExternalId extId)
- throws DuplicateKeyException, IOException {
- if (extId.isScheme(SCHEME_GERRIT) || extId.isScheme(SCHEME_USERNAME)) {
- ExternalIdKeyFactory keyFactory =
- new ExternalIdKeyFactory(
- new ExternalIdKeyFactory.Config() {
- @Override
- public boolean isUserNameCaseInsensitive() {
- return !isUserNameCaseInsensitive;
- }
- });
- ExternalId.Key updatedKey = keyFactory.create(extId.key().scheme(), extId.key().id());
- if (!extId.key().sha1().getName().equals(updatedKey.sha1().getName())) {
- logger.atInfo().log("Converting note name of external ID: %s", extId.key());
- ExternalId updatedExtId =
- externalIdFactory.create(
- updatedKey, extId.accountId(), extId.email(), extId.password(), extId.blobId());
- extIdNotes.replace(extId, updatedExtId);
- }
- }
- }
-
private void updateGerritConfig() throws IOException, ConfigInvalidException {
logger.atInfo().log("Setting auth.userNameCaseInsensitive to true in gerrit.config.");
FileBasedConfig config =
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index f3691ed..8374210 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -62,6 +62,7 @@
import com.google.gerrit.server.StartupChecks.StartupChecksModule;
import com.google.gerrit.server.account.AccountDeactivator.AccountDeactivatorModule;
import com.google.gerrit.server.account.InternalAccountDirectory.InternalAccountDirectoryModule;
+import com.google.gerrit.server.account.externalids.ExternalIdCaseSensitivityMigrator;
import com.google.gerrit.server.api.GerritApiModule;
import com.google.gerrit.server.api.PluginApiModule;
import com.google.gerrit.server.audit.AuditModule;
@@ -118,6 +119,7 @@
import com.google.gerrit.sshd.SshModule;
import com.google.gerrit.sshd.SshSessionFactoryInitializer;
import com.google.gerrit.sshd.commands.DefaultCommandModule;
+import com.google.gerrit.sshd.commands.ExternalIdCommandsModule;
import com.google.gerrit.sshd.commands.IndexCommandsModule;
import com.google.gerrit.sshd.commands.SequenceCommandsModule;
import com.google.gerrit.sshd.plugin.LfsPluginAuthCommand.LfsPluginAuthCommandModule;
@@ -526,6 +528,8 @@
AuthConfig authConfig = cfgInjector.getInstance(AuthConfig.class);
modules.add(new AuthModule(authConfig));
+ modules.add(new ExternalIdCaseSensitivityMigrator.ExternalIdCaseSensitivityMigratorModule());
+
return cfgInjector.createChildInjector(ModuleOverloader.override(modules, libModules));
}
@@ -578,6 +582,7 @@
if (!replica) {
modules.add(new IndexCommandsModule(sysInjector));
modules.add(new SequenceCommandsModule());
+ modules.add(new ExternalIdCommandsModule());
}
return sysInjector.createChildInjector(modules);
}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCaseSensitivityMigrator.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCaseSensitivityMigrator.java
new file mode 100644
index 0000000..204ec2d
--- /dev/null
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCaseSensitivityMigrator.java
@@ -0,0 +1,137 @@
+// Copyright (C) 2021 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;
+
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.exceptions.DuplicateKeyException;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Repository;
+
+public class ExternalIdCaseSensitivityMigrator {
+
+ public static class ExternalIdCaseSensitivityMigratorModule extends AbstractModule {
+ @Override
+ public void configure() {
+ install(new FactoryModuleBuilder().build(ExternalIdCaseSensitivityMigrator.Factory.class));
+ }
+ }
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ public interface Factory {
+ ExternalIdCaseSensitivityMigrator create(
+ @Assisted("isUserNameCaseInsensitive") Boolean isUserNameCaseInsensitive,
+ @Assisted("dryRun") Boolean dryRun);
+ }
+
+ private GitRepositoryManager repoManager;
+ private AllUsersName allUsersName;
+ private Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory;
+ private ExternalIdNotes.FactoryNoReindex externalIdNotesFactory;
+
+ private ExternalIdFactory externalIdFactory;
+ private Boolean isUserNameCaseInsensitive;
+ private Boolean dryRun;
+
+ @Inject
+ public ExternalIdCaseSensitivityMigrator(
+ GitRepositoryManager repoManager,
+ AllUsersName allUsersName,
+ Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory,
+ ExternalIdNotes.FactoryNoReindex externalIdNotesFactory,
+ ExternalIdFactory externalIdFactory,
+ @Assisted("isUserNameCaseInsensitive") Boolean isUserNameCaseInsensitive,
+ @Assisted("dryRun") Boolean dryRun) {
+ this.repoManager = repoManager;
+ this.allUsersName = allUsersName;
+ this.metaDataUpdateServerFactory = metaDataUpdateServerFactory;
+ this.externalIdNotesFactory = externalIdNotesFactory;
+ this.externalIdFactory = externalIdFactory;
+
+ this.isUserNameCaseInsensitive = isUserNameCaseInsensitive;
+ this.dryRun = dryRun;
+ }
+
+ private void recomputeExternalIdNoteId(ExternalIdNotes extIdNotes, ExternalId extId)
+ throws DuplicateKeyException, IOException {
+ if (extId.isScheme(SCHEME_GERRIT) || extId.isScheme(SCHEME_USERNAME)) {
+ ExternalIdKeyFactory keyFactory =
+ new ExternalIdKeyFactory(
+ new ExternalIdKeyFactory.Config() {
+ @Override
+ public boolean isUserNameCaseInsensitive() {
+ return isUserNameCaseInsensitive;
+ }
+ });
+ ExternalId.Key updatedKey = keyFactory.create(extId.key().scheme(), extId.key().id());
+ ExternalId.Key oldKey =
+ keyFactory.create(extId.key().scheme(), extId.key().id(), !isUserNameCaseInsensitive);
+ if (!oldKey.sha1().getName().equals(updatedKey.sha1().getName())) {
+ logger.atInfo().log("Converting note name of external ID: %s", oldKey);
+ ExternalId updatedExtId =
+ externalIdFactory.create(
+ updatedKey, extId.accountId(), extId.email(), extId.password(), extId.blobId());
+ ExternalId oldExtId =
+ externalIdFactory.create(
+ oldKey, extId.accountId(), extId.email(), extId.password(), extId.blobId());
+ extIdNotes.replace(
+ Collections.singleton(oldExtId),
+ Collections.singleton(updatedExtId),
+ (externalId) -> externalId.key().sha1());
+ }
+ }
+ }
+
+ public void migrate(Collection<ExternalId> todo, Runnable monitor)
+ throws RepositoryNotFoundException, IOException, ConfigInvalidException {
+ try (Repository repo = repoManager.openRepository(allUsersName)) {
+ ExternalIdNotes extIdNotes = externalIdNotesFactory.load(repo);
+ for (ExternalId extId : todo) {
+ recomputeExternalIdNoteId(extIdNotes, extId);
+ monitor.run();
+ }
+ if (!dryRun) {
+ try (MetaDataUpdate metaDataUpdate =
+ metaDataUpdateServerFactory.get().create(allUsersName)) {
+ metaDataUpdate.setMessage(
+ String.format(
+ "Migration to case %ssensitive usernames",
+ isUserNameCaseInsensitive ? "" : "in"));
+ extIdNotes.commit(metaDataUpdate);
+ } catch (Exception e) {
+ logger.atSevere().withCause(e).log(e.getMessage());
+ }
+ }
+ } catch (DuplicateExternalIdKeyException e) {
+ logger.atSevere().withCause(e).log(e.getMessage());
+ throw e;
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
index 6c3a73b7..aa37451 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdNotes.java
@@ -54,6 +54,7 @@
import java.util.List;
import java.util.Optional;
import java.util.Set;
+import java.util.function.Function;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.BlobBasedConfig;
import org.eclipse.jgit.lib.CommitBuilder;
@@ -395,6 +396,18 @@
private boolean noCacheUpdate = false;
private boolean noReindex = false;
private boolean isUserNameCaseInsensitiveMigrationMode = false;
+ protected final Function<ExternalId, ObjectId> defaultNoteIdResolver =
+ (extId) -> {
+ ObjectId noteId = extId.key().sha1();
+ try {
+ if (isUserNameCaseInsensitiveMigrationMode && !noteMap.contains(noteId)) {
+ noteId = extId.key().caseSensitiveSha1();
+ }
+ } catch (IOException e) {
+ return noteId;
+ }
+ return noteId;
+ };
private ExternalIdNotes(
MetricMaker metricMaker,
@@ -708,6 +721,12 @@
cacheUpdates.add(cu -> cu.remove(removedExtIds));
}
+ public void replace(
+ Account.Id accountId, Collection<ExternalId.Key> toDelete, Collection<ExternalId> toAdd)
+ throws IOException, DuplicateExternalIdKeyException {
+ replace(accountId, toDelete, toAdd, defaultNoteIdResolver);
+ }
+
/**
* Replaces external IDs for an account by external ID keys.
*
@@ -720,7 +739,10 @@
* the specified account.
*/
public void replace(
- Account.Id accountId, Collection<ExternalId.Key> toDelete, Collection<ExternalId> toAdd)
+ Account.Id accountId,
+ Collection<ExternalId.Key> toDelete,
+ Collection<ExternalId> toAdd,
+ Function<ExternalId, ObjectId> noteIdResolver)
throws IOException, DuplicateExternalIdKeyException {
checkLoaded();
checkSameAccount(toAdd, accountId);
@@ -738,7 +760,7 @@
}
for (ExternalId extId : toAdd) {
- ExternalId insertedExtId = upsert(rw, inserter, noteMap, extId);
+ ExternalId insertedExtId = upsert(rw, inserter, noteMap, extId, noteIdResolver);
preprocessUpsert(insertedExtId);
updatedExtIds.add(insertedExtId);
}
@@ -814,6 +836,32 @@
replace(accountId, toDelete.stream().map(ExternalId::key).collect(toSet()), toAdd);
}
+ /**
+ * Replaces external IDs.
+ *
+ * <p>Deletion of external IDs is done before adding the new external IDs. This means if an
+ * external ID is specified for deletion and an external ID with the same key is specified to be
+ * added, the old external ID with that key is deleted first and then the new external ID is added
+ * (so the external ID for that key is replaced).
+ *
+ * @throws IllegalStateException is thrown if the specified external IDs belong to different
+ * accounts.
+ */
+ public void replace(
+ Collection<ExternalId> toDelete,
+ Collection<ExternalId> toAdd,
+ Function<ExternalId, ObjectId> noteIdResolver)
+ throws IOException, DuplicateExternalIdKeyException {
+ Account.Id accountId = checkSameAccount(Iterables.concat(toDelete, toAdd));
+ if (accountId == null) {
+ // toDelete and toAdd are empty -> nothing to do
+ return;
+ }
+
+ replace(
+ accountId, toDelete.stream().map(ExternalId::key).collect(toSet()), toAdd, noteIdResolver);
+ }
+
@Override
protected void onLoad() throws IOException, ConfigInvalidException {
if (revision != null) {
@@ -922,9 +970,26 @@
*/
private ExternalId upsert(RevWalk rw, ObjectInserter ins, NoteMap noteMap, ExternalId extId)
throws IOException, ConfigInvalidException {
+ return upsert(rw, ins, noteMap, extId, defaultNoteIdResolver);
+ }
+
+ /**
+ * Inserts or updates a new external ID and sets it in the note map.
+ *
+ * <p>If the external ID already exists, it is overwritten.
+ */
+ private ExternalId upsert(
+ RevWalk rw,
+ ObjectInserter ins,
+ NoteMap noteMap,
+ ExternalId extId,
+ Function<ExternalId, ObjectId> noteIdResolver)
+ throws IOException, ConfigInvalidException {
ObjectId noteId = extId.key().sha1();
Config c = new Config();
- if (noteMap.contains(noteId)) {
+ ObjectId resolvedNoteId = noteIdResolver.apply(extId);
+ if (noteMap.contains(resolvedNoteId)) {
+ noteId = resolvedNoteId;
ObjectId noteDataId = noteMap.get(noteId);
byte[] raw = readNoteData(rw, noteDataId);
try {
diff --git a/java/com/google/gerrit/server/account/externalids/OnlineExternalIdCaseSensivityMigratiorExecutor.java b/java/com/google/gerrit/server/account/externalids/OnlineExternalIdCaseSensivityMigratiorExecutor.java
new file mode 100644
index 0000000..8a3e4f1
--- /dev/null
+++ b/java/com/google/gerrit/server/account/externalids/OnlineExternalIdCaseSensivityMigratiorExecutor.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2021 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;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+import java.lang.annotation.Retention;
+
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface OnlineExternalIdCaseSensivityMigratiorExecutor {}
diff --git a/java/com/google/gerrit/server/account/externalids/OnlineExternalIdCaseSensivityMigrator.java b/java/com/google/gerrit/server/account/externalids/OnlineExternalIdCaseSensivityMigrator.java
new file mode 100644
index 0000000..e52991b
--- /dev/null
+++ b/java/com/google/gerrit/server/account/externalids/OnlineExternalIdCaseSensivityMigrator.java
@@ -0,0 +1,117 @@
+// Copyright (C) 2021 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;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePath;
+import com.google.gerrit.server.index.ReindexerAlreadyRunningException;
+import com.google.gerrit.server.index.VersionManager;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+@Singleton
+public class OnlineExternalIdCaseSensivityMigrator {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private Executor executor;
+ private ExternalIdCaseSensitivityMigrator.Factory migratorFactory;
+ private ExternalIds externalIds;
+ private VersionManager versionManager;
+ private Config globalConfig;
+ private Path sitePath;
+ private final TextProgressMonitor monitor = new TextProgressMonitor();
+ private boolean isUserNameCaseInsensitive;
+ private boolean isUserNameCaseInsensitiveMigrationMode;
+
+ @Inject
+ public OnlineExternalIdCaseSensivityMigrator(
+ @OnlineExternalIdCaseSensivityMigratiorExecutor ExecutorService executor,
+ ExternalIdCaseSensitivityMigrator.Factory migratorFactory,
+ ExternalIds externalIds,
+ VersionManager versionManager,
+ @GerritServerConfig Config globalConfig,
+ @SitePath Path sitePath) {
+ this.migratorFactory = migratorFactory;
+ this.externalIds = externalIds;
+ this.versionManager = versionManager;
+ this.globalConfig = globalConfig;
+ this.sitePath = sitePath;
+ this.executor = executor;
+ this.isUserNameCaseInsensitiveMigrationMode =
+ globalConfig.getBoolean("auth", "userNameCaseInsensitiveMigrationMode", false);
+ this.isUserNameCaseInsensitive =
+ globalConfig.getBoolean("auth", "userNameCaseInsensitive", false);
+ }
+
+ public void migrate() {
+ if (!isUserNameCaseInsensitive || !isUserNameCaseInsensitiveMigrationMode) {
+ logger.atSevere().log(
+ "External IDs online migration requires auth.userNameCaseInsensitive and auth.userNameCaseInsensitiveMigrationMode to be set to true. Skipping migration!");
+ return;
+ }
+ executor.execute(
+ () -> {
+ try {
+ Collection<ExternalId> todo = externalIds.all();
+ try {
+ monitor.beginTask("Converting external ID note names", todo.size());
+ migratorFactory
+ .create(isUserNameCaseInsensitive, false)
+ .migrate(todo, () -> monitor.update(1));
+ } finally {
+ monitor.endTask();
+ }
+ try {
+ updateGerritConfig();
+ monitor.beginTask("Reindex accounts", ProgressMonitor.UNKNOWN);
+ versionManager.startReindexer("accounts", true);
+ } finally {
+ monitor.endTask();
+ }
+ logger.atInfo().log("External IDs migration completed!");
+ } catch (IOException | ConfigInvalidException e) {
+ logger.atSevere().withCause(e).log(
+ "Exception during the external ids migration, cause %s", e.getMessage());
+ } catch (ReindexerAlreadyRunningException e) {
+ logger.atSevere().log("Failed to reindex external ids: %s", e.getMessage());
+ }
+ });
+ }
+
+ private void updateGerritConfig() throws IOException, ConfigInvalidException {
+ logger.atInfo().log(
+ "Setting auth.userNameCaseInsensitiveMigrationMode to false in gerrit.config.");
+
+ FileBasedConfig config =
+ new FileBasedConfig(
+ globalConfig, sitePath.resolve("etc/gerrit.config").toFile(), FS.DETECTED);
+ config.load();
+ config.setBoolean("auth", null, "userNameCaseInsensitiveMigrationMode", false);
+
+ config.save();
+ }
+}
diff --git a/java/com/google/gerrit/sshd/commands/ExternalIdCaseSensitivityMigrationCommand.java b/java/com/google/gerrit/sshd/commands/ExternalIdCaseSensitivityMigrationCommand.java
new file mode 100644
index 0000000..29cc1cf
--- /dev/null
+++ b/java/com/google/gerrit/sshd/commands/ExternalIdCaseSensitivityMigrationCommand.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2021 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.commands;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.server.account.externalids.OnlineExternalIdCaseSensivityMigrator;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import org.eclipse.jgit.lib.Config;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(
+ name = "migrate-externalids-to-insensitive",
+ description = "Migrate external-ids to case insensitive")
+public class ExternalIdCaseSensitivityMigrationCommand extends SshCommand {
+
+ @Inject OnlineExternalIdCaseSensivityMigrator onlineExternalIdCaseSensivityMigrator;
+ @Inject @GerritServerConfig private Config globalConfig;
+
+ @Override
+ public void run() throws UnloggedFailure, Failure, Exception {
+ Boolean isUserNameCaseInsensitiveMigrationMode =
+ globalConfig.getBoolean("auth", "userNameCaseInsensitiveMigrationMode", false);
+ Boolean isUserNameCaseInsensitive =
+ globalConfig.getBoolean("auth", "userNameCaseInsensitive", false);
+
+ if (!isUserNameCaseInsensitive || !isUserNameCaseInsensitiveMigrationMode) {
+ die(
+ "External IDs online migration requires auth.userNameCaseInsensitive and auth.userNameCaseInsensitiveMigrationMode to be set to true. Cannot start migration!");
+ }
+ onlineExternalIdCaseSensivityMigrator.migrate();
+ stdout.println(
+ "External ids case insensitivity migration started. To check if it's completed look for \"External IDs migration completed!\" message in the Gerrit server logs");
+ }
+}
diff --git a/java/com/google/gerrit/sshd/commands/ExternalIdCommandsModule.java b/java/com/google/gerrit/sshd/commands/ExternalIdCommandsModule.java
new file mode 100644
index 0000000..57bf9e5
--- /dev/null
+++ b/java/com/google/gerrit/sshd/commands/ExternalIdCommandsModule.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2021 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.commands;
+
+import com.google.gerrit.server.account.externalids.OnlineExternalIdCaseSensivityMigratiorExecutor;
+import com.google.gerrit.server.account.externalids.OnlineExternalIdCaseSensivityMigrator;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.sshd.CommandModule;
+import com.google.gerrit.sshd.Commands;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import java.util.concurrent.ExecutorService;
+
+public class ExternalIdCommandsModule extends CommandModule {
+
+ @Override
+ protected void configure() {
+ bind(OnlineExternalIdCaseSensivityMigrator.class);
+ command(Commands.named("gerrit"), ExternalIdCaseSensitivityMigrationCommand.class);
+ }
+
+ @Provides
+ @Singleton
+ @OnlineExternalIdCaseSensivityMigratiorExecutor
+ public ExecutorService OnlineExternalIdCaseSensivityMigratiorExecutor(WorkQueue queues) {
+ return queues.createQueue(1, "MigrateExternalIdCase", true);
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/account/externalids/BUILD b/javatests/com/google/gerrit/acceptance/server/account/externalids/BUILD
new file mode 100644
index 0000000..32676fe
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/account/externalids/BUILD
@@ -0,0 +1,7 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+ srcs = glob(["*IT.java"]),
+ group = "server_externalids",
+ labels = ["server"],
+)
diff --git a/javatests/com/google/gerrit/acceptance/server/account/externalids/OnlineExternalIdCaseSensivityMigratorIT.java b/javatests/com/google/gerrit/acceptance/server/account/externalids/OnlineExternalIdCaseSensivityMigratorIT.java
new file mode 100644
index 0000000..998f579
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/account/externalids/OnlineExternalIdCaseSensivityMigratorIT.java
@@ -0,0 +1,267 @@
+// Copyright (C) 2021 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.server.account.externalids;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.account.externalids.DuplicateExternalIdKeyException;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdCaseSensitivityMigrator;
+import com.google.gerrit.server.account.externalids.ExternalIdFactory;
+import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
+import com.google.gerrit.server.account.externalids.ExternalIdNotes;
+import com.google.gerrit.server.account.externalids.OnlineExternalIdCaseSensivityMigratiorExecutor;
+import com.google.gerrit.server.account.externalids.OnlineExternalIdCaseSensivityMigrator;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+import java.io.IOException;
+import java.util.Optional;
+import java.util.concurrent.ExecutorService;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Test;
+
+public class OnlineExternalIdCaseSensivityMigratorIT extends AbstractDaemonTest {
+ private Account.Id accountId = Account.id(66);
+ private boolean isUserNameCaseInsensitive = false;
+
+ @Inject private ExternalIdNotes.Factory externalIdNotesFactory;
+ @Inject private ExternalIdKeyFactory externalIdKeyFactory;
+ @Inject private ExternalIdFactory externalIdFactory;
+ @Inject private OnlineExternalIdCaseSensivityMigrator objectUnderTest;
+
+ @Override
+ public Module createModule() {
+ return new AbstractModule() {
+ @Override
+ protected void configure() {
+ install(new FactoryModuleBuilder().build(ExternalIdCaseSensitivityMigrator.Factory.class));
+ bind(ExecutorService.class)
+ .annotatedWith(OnlineExternalIdCaseSensivityMigratiorExecutor.class)
+ .toInstance(MoreExecutors.newDirectExecutorService());
+ }
+ };
+ }
+
+ @Test
+ @GerritConfig(name = "auth.userNameCaseInsensitive", value = "true")
+ @GerritConfig(name = "auth.userNameCaseInsensitiveMigrationMode", value = "true")
+ public void shouldMigrateExternalId() throws IOException, ConfigInvalidException {
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+ ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+
+ createExternalId(
+ md, extIdNotes, SCHEME_GERRIT, "JonDoe", accountId, isUserNameCaseInsensitive);
+ createExternalId(
+ md, extIdNotes, SCHEME_USERNAME, "JonDoe", accountId, isUserNameCaseInsensitive);
+
+ assertThat(getExactExternalId(extIdNotes, SCHEME_GERRIT, "JonDoe").isPresent()).isTrue();
+ assertThat(getExactExternalId(extIdNotes, SCHEME_GERRIT, "jondoe").isPresent()).isFalse();
+
+ assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "JonDoe").isPresent()).isTrue();
+ assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "jondoe").isPresent()).isFalse();
+
+ objectUnderTest.migrate();
+
+ extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+ assertThat(getExactExternalId(extIdNotes, SCHEME_GERRIT, "JonDoe").isPresent()).isFalse();
+ assertThat(getExactExternalId(extIdNotes, SCHEME_GERRIT, "jondoe").isPresent()).isTrue();
+
+ assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "JonDoe").isPresent()).isFalse();
+ assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "jondoe").isPresent()).isTrue();
+ }
+ }
+
+ @Test
+ @GerritConfig(name = "auth.userNameCaseInsensitive", value = "true")
+ @GerritConfig(name = "auth.userNameCaseInsensitiveMigrationMode", value = "true")
+ public void shouldNotCreateDuplicateExternaIdNotesWhenUpdatingAccount()
+ throws IOException, ConfigInvalidException {
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+ ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+
+ createExternalId(
+ md, extIdNotes, SCHEME_GERRIT, "JonDoe", accountId, isUserNameCaseInsensitive);
+ createExternalId(
+ md, extIdNotes, SCHEME_USERNAME, "JonDoe", accountId, isUserNameCaseInsensitive);
+
+ extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+ ExternalId extId =
+ externalIdFactory.create(
+ externalIdKeyFactory.create(SCHEME_USERNAME, "JonDoe", true),
+ accountId,
+ "test@email.com",
+ "w1m9Bg85GQ4hijLNxW+6xAfj4r9wyk9rzVQelIHxuQ");
+ extIdNotes.upsert(extId);
+ extIdNotes.commit(md);
+
+ extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+
+ assertThat(getExactExternalId(extIdNotes, SCHEME_GERRIT, "JonDoe").isPresent()).isTrue();
+ assertThat(getExactExternalId(extIdNotes, SCHEME_GERRIT, "jondoe").isPresent()).isFalse();
+
+ assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "JonDoe").isPresent()).isTrue();
+ assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "jondoe").isPresent()).isFalse();
+
+ assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "JonDoe").get().email())
+ .isEqualTo("test@email.com");
+
+ objectUnderTest.migrate();
+
+ extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+ assertThat(getExactExternalId(extIdNotes, SCHEME_GERRIT, "JonDoe").isPresent()).isFalse();
+ assertThat(getExactExternalId(extIdNotes, SCHEME_GERRIT, "jondoe").isPresent()).isTrue();
+
+ assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "JonDoe").isPresent()).isFalse();
+ assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "jondoe").isPresent()).isTrue();
+
+ assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "jondoe").get().email())
+ .isEqualTo("test@email.com");
+ }
+ }
+
+ @Test
+ @GerritConfig(name = "auth.userNameCaseInsensitive", value = "true")
+ @GerritConfig(name = "auth.userNameCaseInsensitiveMigrationMode", value = "true")
+ public void caseInsensitivityShouldWorkAfterMigration()
+ throws IOException, ConfigInvalidException {
+
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+ ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+
+ createExternalId(
+ md, extIdNotes, SCHEME_USERNAME, "JonDoe", accountId, isUserNameCaseInsensitive);
+
+ assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "JonDoe").isPresent()).isTrue();
+ assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "jondoe").isPresent()).isFalse();
+
+ objectUnderTest.migrate();
+
+ extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+ assertThat(
+ getExternalIdWithCaseInsensitive(extIdNotes, SCHEME_USERNAME, "JonDoe").isPresent())
+ .isTrue();
+ assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "jondoe").isPresent()).isTrue();
+ }
+ }
+
+ @Test
+ @GerritConfig(name = "auth.userNameCaseInsensitive", value = "true")
+ @GerritConfig(name = "auth.userNameCaseInsensitiveMigrationMode", value = "true")
+ public void shouldThrowExceptionWhenDuplicateKeys() throws IOException, ConfigInvalidException {
+
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+ ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+
+ createExternalId(
+ md, extIdNotes, SCHEME_USERNAME, "JonDoe", accountId, isUserNameCaseInsensitive);
+
+ createExternalId(
+ md, extIdNotes, SCHEME_USERNAME, "jondoe", Account.id(67), isUserNameCaseInsensitive);
+
+ assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "JonDoe").isPresent()).isTrue();
+ assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "jondoe").isPresent()).isTrue();
+
+ assertThrows(DuplicateExternalIdKeyException.class, () -> objectUnderTest.migrate());
+ }
+ }
+
+ @Test
+ @GerritConfig(name = "auth.userNameCaseInsensitive", value = "false")
+ @GerritConfig(name = "auth.userNameCaseInsensitiveMigrationMode", value = "true")
+ public void shouldSkipMigrationWhenUserNameCaseInsensitiveIsSetToFalse()
+ throws RepositoryNotFoundException, IOException, ConfigInvalidException {
+
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+ ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+
+ createExternalId(
+ md, extIdNotes, SCHEME_USERNAME, "JonDoe", accountId, isUserNameCaseInsensitive);
+
+ assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "JonDoe").isPresent()).isTrue();
+ assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "jondoe").isPresent()).isFalse();
+
+ objectUnderTest.migrate();
+
+ extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+
+ assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "JonDoe").isPresent()).isTrue();
+ assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "jondoe").isPresent()).isFalse();
+ }
+ }
+
+ @Test
+ @GerritConfig(name = "auth.userNameCaseInsensitive", value = "true")
+ @GerritConfig(name = "auth.userNameCaseInsensitiveMigrationMode", value = "false")
+ public void shouldSkipMigrationWhenUserNameCaseInsensitiveMigrationModeIsSetToFalse()
+ throws RepositoryNotFoundException, IOException, ConfigInvalidException {
+
+ try (Repository allUsersRepo = repoManager.openRepository(allUsers);
+ MetaDataUpdate md = metaDataUpdateFactory.create(allUsers)) {
+ ExternalIdNotes extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+
+ createExternalId(
+ md, extIdNotes, SCHEME_USERNAME, "JonDoe", accountId, isUserNameCaseInsensitive);
+
+ objectUnderTest.migrate();
+
+ extIdNotes = externalIdNotesFactory.load(allUsersRepo);
+ assertThat(getExactExternalId(extIdNotes, SCHEME_USERNAME, "jondoe").isPresent()).isFalse();
+ }
+ }
+
+ protected Optional<ExternalId> getExternalIdWithCaseInsensitive(
+ ExternalIdNotes extIdNotes, String scheme, String id)
+ throws IOException, ConfigInvalidException {
+ return extIdNotes.get(externalIdKeyFactory.create(scheme, id, true));
+ }
+
+ protected Optional<ExternalId> getExactExternalId(
+ ExternalIdNotes extIdNotes, String scheme, String id)
+ throws IOException, ConfigInvalidException {
+ return extIdNotes.get(externalIdKeyFactory.create(scheme, id, false));
+ }
+
+ protected void createExternalId(
+ MetaDataUpdate md,
+ ExternalIdNotes extIdNotes,
+ String scheme,
+ String id,
+ Account.Id accountId,
+ boolean isUserNameCaseInsensitive)
+ throws IOException {
+ ExternalId extId =
+ externalIdFactory.create(
+ externalIdKeyFactory.create(scheme, id, isUserNameCaseInsensitive), accountId);
+ extIdNotes.insert(extId);
+ extIdNotes.commit(md);
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
index 2b37cfd..7224e19 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
@@ -81,7 +81,8 @@
"set-reviewers",
"set-topic",
"stream-events",
- "test-submit");
+ "test-submit",
+ "migrate-externalids-to-insensitive");
private static final ImmutableList<String> EMPTY = ImmutableList.of();
private static final ImmutableMap<String, List<String>> MASTER_COMMANDS =