blob: 38c3df5d7db51d79adb8ef9473904eb9b71c2221 [file] [log] [blame]
// Copyright (C) 2025 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;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
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.ExternalIdFactory;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNotes;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MultiProgressMonitor;
import com.google.gerrit.server.git.MultiProgressMonitor.Task;
import com.google.gerrit.server.git.MultiProgressMonitor.TaskKind;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.logging.TraceContext.TraceTimer;
import com.google.gerrit.server.project.LockManager;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.stream.Collectors;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.util.io.NullOutputStream;
public class PasswordMigrator implements Runnable {
static final FluentLogger logger = FluentLogger.forEnclosingClass();
@VisibleForTesting public static final String DEFAULT_ID = "default";
private final GitRepositoryManager repoManager;
private final MultiProgressMonitor.Factory multiProgressMonitorFactory;
private final AuthTokenAccessor tokenAccessor;
private final ExternalIds externalIds;
private final ExternalIdFactory externalIdFactory;
private final Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory;
private final AllUsersName allUsers;
private final ExternalIdNotes.FactoryNoReindex externalIdNotesFactory;
private final Optional<Instant> expirationDate;
private final LockManager lockManager;
private MultiProgressMonitor mpm;
private Task doneTask;
private Task failedTask;
public interface Factory {
public PasswordMigrator create(Optional<Instant> expiryDate);
}
@AssistedInject
public PasswordMigrator(
GitRepositoryManager repoManager,
MultiProgressMonitor.Factory multiProgressMonitorFactory,
AuthTokenAccessor tokenAccessor,
ExternalIds externalIds,
ExternalIdFactory externalIdFactory,
AllUsersName allUsers,
ExternalIdNotes.FactoryNoReindex externalIdNotesFactory,
Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory,
@Assisted Optional<Instant> expirationDate,
LockManager lockManager) {
this.repoManager = repoManager;
this.multiProgressMonitorFactory = multiProgressMonitorFactory;
this.tokenAccessor = tokenAccessor;
this.externalIds = externalIds;
this.externalIdFactory = externalIdFactory;
this.allUsers = allUsers;
this.externalIdNotesFactory = externalIdNotesFactory;
this.metaDataUpdateServerFactory = metaDataUpdateServerFactory;
this.expirationDate = expirationDate;
this.lockManager = lockManager;
}
@Override
public void run() {
Lock lock = lockManager.getLock("MigratePasswordsToTokens");
if (!lock.tryLock()) {
logger.atWarning().log("Migration of passwords to tokens already running.");
return;
}
try {
ImmutableSet<ExternalId> todo;
try {
todo = getAllUsernameExternalIds();
} catch (IOException | ConfigInvalidException e) {
logger.atSevere().withCause(e).log("Unable to read external IDs.");
return;
}
if (todo.isEmpty()) {
logger.atInfo().log("No accounts with HTTP passwords exist. Nothing to do.");
return;
}
Stopwatch sw = Stopwatch.createStarted();
mpm =
multiProgressMonitorFactory.create(
NullOutputStream.INSTANCE, TaskKind.MIGRATION, "Migrating HTTP passwords", true);
doneTask = mpm.beginSubTask("passwords", todo.size());
failedTask = mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN);
List<ExternalId> failedToMigrate = new ArrayList<>();
for (ExternalId extId : todo) {
if (!createToken(extId, expirationDate)) {
failedToMigrate.add(extId);
}
logger.atInfo().atMostEvery(30, TimeUnit.SECONDS).log(
"Migrated %d/%d HTTP passwords (%d failed).",
doneTask.getCount(), todo.size(), failedTask.getCount());
}
doneTask.end();
failedTask.end();
mpm.end();
logger.atInfo().log(
"Finished creating tokens. %d/%d finished successfully in %d min. %d tasks" + " failed.",
doneTask.getCount(), todo.size(), sw.elapsed(TimeUnit.MINUTES), failedTask.getCount());
logger.atInfo().log("Starting to delete HTTP passwords from External IDs.");
try {
deletePasswordsInExternalId(failedToMigrate);
} catch (ConfigInvalidException e) {
logger.atSevere().withCause(e).log(
"Migration of HTTP passwords failed. Unable to load External IDs.");
}
sw.stop();
logger.atInfo().log(
"Finished deleting HTTP passwords from External IDs. Total migration to tokens took %d"
+ " min.",
sw.elapsed(TimeUnit.MINUTES));
} finally {
lock.unlock();
}
}
private boolean createToken(ExternalId extId, Optional<Instant> expirationDate) {
String hashedPassword = extId.password();
if (hashedPassword == null) {
return true;
}
Account.Id accountId = extId.accountId();
try (TraceTimer traceTimer =
TraceContext.newTimer(
"Creating token from HTTP password",
Metadata.builder().projectName("All-Users").accountId(accountId.get()).build())) {
if (tokenAccessor.getToken(accountId, PasswordMigrator.DEFAULT_ID).isPresent()) {
logger.atFine().log("HTTP password of account %d was already migrated.", accountId.get());
} else {
@SuppressWarnings("unused")
var unused =
tokenAccessor.addToken(
accountId, PasswordMigrator.DEFAULT_ID, hashedPassword, expirationDate);
}
doneTask.update(1);
} catch (IOException | ConfigInvalidException | InvalidAuthTokenException e) {
logger.atSevere().withCause(e).log(
"Failed to migrate HTTP password to token for account %d", accountId.get());
failedTask.update(1);
return false;
}
return true;
}
private void deletePasswordsInExternalId(List<ExternalId> exclude) throws ConfigInvalidException {
try (TraceTimer traceTimer =
TraceContext.newTimer(
"Deleting Passwords from external IDs",
Metadata.builder()
.projectName("All-Users")
.noteDbRefName("refs/meta/external-ids")
.build())) {
ImmutableSet<ExternalId> extIdsWithPassword;
try {
extIdsWithPassword = getAllUsernameExternalIds();
} catch (IOException | ConfigInvalidException e) {
logger.atSevere().withCause(e).log(
"Unable to read external IDs. Can't delete passwords from external IDs.");
return;
}
Set<ExternalId> extIdsToUpdate =
extIdsWithPassword.stream().filter(e -> !exclude.contains(e)).collect(Collectors.toSet());
Set<ExternalId> updatedExtIds =
extIdsToUpdate.stream()
.map(e -> externalIdFactory.createWithEmail(e.key(), e.accountId(), e.email()))
.collect(Collectors.toSet());
try (Repository repo = repoManager.openRepository(allUsers)) {
ExternalIdNotes extIdNotes = externalIdNotesFactory.load(repo);
extIdNotes.replaceByKeys(
extIdsToUpdate.stream().map(e -> e.key()).collect(Collectors.toSet()), updatedExtIds);
try (MetaDataUpdate metaDataUpdate = metaDataUpdateServerFactory.get().create(allUsers)) {
metaDataUpdate.setMessage("Migrate HTTP passwords to tokens");
extIdNotes.commit(metaDataUpdate);
}
} catch (IOException | DuplicateExternalIdKeyException e) {
logger.atSevere().withCause(e).log("Unable to replace External IDs.");
}
}
}
private ImmutableSet<ExternalId> getAllUsernameExternalIds()
throws IOException, ConfigInvalidException {
return ImmutableSet.copyOf(
externalIds.all().stream()
.filter(e -> e.key().scheme().equals(SCHEME_USERNAME) && e.password() != null)
.collect(Collectors.toSet()));
}
}