blob: 028fd8d1462c049fd8643e45f0fe95f7cca29c30 [file] [log] [blame]
// Copyright (C) 2016 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.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.Runnables;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.metrics.Counter0;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.update.RetryHelper;
import com.google.gerrit.server.update.RetryHelper.ActionType;
import com.google.gwtorm.server.OrmDuplicateKeyException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Repository;
/**
* Updates externalIds in ReviewDb and NoteDb.
*
* <p>In NoteDb external IDs are stored in the All-Users repository in a Git Notes branch called
* refs/meta/external-ids where the sha1 of the external ID is used as note name. Each note content
* is a git config file that contains an external ID. It has 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>
*
* For NoteDb each method call results in one commit on refs/meta/external-ids branch.
*
* <p>On updating external IDs this class takes care to evict affected accounts from the account
* cache and thus triggers reindex for them.
*/
public class ExternalIdsUpdate {
/**
* Factory to create an ExternalIdsUpdate instance for updating external IDs by the Gerrit server.
*
* <p>The Gerrit server identity will be used as author and committer for all commits that update
* the external IDs.
*/
@Singleton
public static class Server {
private final GitRepositoryManager repoManager;
private final Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory;
private final AccountCache accountCache;
private final AllUsersName allUsersName;
private final MetricMaker metricMaker;
private final ExternalIds externalIds;
private final ExternalIdCache externalIdCache;
private final RetryHelper retryHelper;
@Inject
public Server(
GitRepositoryManager repoManager,
Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory,
AccountCache accountCache,
AllUsersName allUsersName,
MetricMaker metricMaker,
ExternalIds externalIds,
ExternalIdCache externalIdCache,
RetryHelper retryHelper) {
this.repoManager = repoManager;
this.metaDataUpdateServerFactory = metaDataUpdateServerFactory;
this.accountCache = accountCache;
this.allUsersName = allUsersName;
this.metricMaker = metricMaker;
this.externalIds = externalIds;
this.externalIdCache = externalIdCache;
this.retryHelper = retryHelper;
}
public ExternalIdsUpdate create() {
return new ExternalIdsUpdate(
repoManager,
() -> metaDataUpdateServerFactory.get().create(allUsersName),
accountCache,
allUsersName,
metricMaker,
externalIds,
externalIdCache,
retryHelper);
}
}
/**
* Factory to create an ExternalIdsUpdate instance for updating external IDs by the Gerrit server.
*
* <p>Using this class no reindex will be performed for the affected accounts and they will also
* not be evicted from the account cache.
*
* <p>The Gerrit server identity will be used as author and committer for all commits that update
* the external IDs.
*/
@Singleton
public static class ServerNoReindex {
private final GitRepositoryManager repoManager;
private final Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory;
private final AllUsersName allUsersName;
private final MetricMaker metricMaker;
private final ExternalIds externalIds;
private final ExternalIdCache externalIdCache;
private final RetryHelper retryHelper;
@Inject
public ServerNoReindex(
GitRepositoryManager repoManager,
Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory,
AllUsersName allUsersName,
MetricMaker metricMaker,
ExternalIds externalIds,
ExternalIdCache externalIdCache,
RetryHelper retryHelper) {
this.repoManager = repoManager;
this.metaDataUpdateServerFactory = metaDataUpdateServerFactory;
this.allUsersName = allUsersName;
this.metricMaker = metricMaker;
this.externalIds = externalIds;
this.externalIdCache = externalIdCache;
this.retryHelper = retryHelper;
}
public ExternalIdsUpdate create() {
return new ExternalIdsUpdate(
repoManager,
() -> metaDataUpdateServerFactory.get().create(allUsersName),
null,
allUsersName,
metricMaker,
externalIds,
externalIdCache,
retryHelper);
}
}
/**
* Factory to create an ExternalIdsUpdate instance for updating external IDs by the current user.
*
* <p>The identity of the current user will be used as author for all commits that update the
* external IDs. The Gerrit server identity will be used as committer.
*/
@Singleton
public static class User {
private final GitRepositoryManager repoManager;
private final Provider<MetaDataUpdate.User> metaDataUpdateUserFactory;
private final AccountCache accountCache;
private final AllUsersName allUsersName;
private final MetricMaker metricMaker;
private final ExternalIds externalIds;
private final ExternalIdCache externalIdCache;
private final RetryHelper retryHelper;
@Inject
public User(
GitRepositoryManager repoManager,
Provider<MetaDataUpdate.User> metaDataUpdateUserFactory,
AccountCache accountCache,
AllUsersName allUsersName,
MetricMaker metricMaker,
ExternalIds externalIds,
ExternalIdCache externalIdCache,
RetryHelper retryHelper) {
this.repoManager = repoManager;
this.metaDataUpdateUserFactory = metaDataUpdateUserFactory;
this.accountCache = accountCache;
this.allUsersName = allUsersName;
this.metricMaker = metricMaker;
this.externalIds = externalIds;
this.externalIdCache = externalIdCache;
this.retryHelper = retryHelper;
}
public ExternalIdsUpdate create() {
return new ExternalIdsUpdate(
repoManager,
() -> metaDataUpdateUserFactory.get().create(allUsersName),
accountCache,
allUsersName,
metricMaker,
externalIds,
externalIdCache,
retryHelper);
}
}
private final GitRepositoryManager repoManager;
private final MetaDataUpdateFactory metaDataUpdateFactory;
@Nullable private final AccountCache accountCache;
private final AllUsersName allUsersName;
private final ExternalIds externalIds;
private final ExternalIdCache externalIdCache;
private final RetryHelper retryHelper;
private final Runnable afterReadRevision;
private final Counter0 updateCount;
private ExternalIdsUpdate(
GitRepositoryManager repoManager,
MetaDataUpdateFactory metaDataUpdateFactory,
@Nullable AccountCache accountCache,
AllUsersName allUsersName,
MetricMaker metricMaker,
ExternalIds externalIds,
ExternalIdCache externalIdCache,
RetryHelper retryHelper) {
this(
repoManager,
metaDataUpdateFactory,
accountCache,
allUsersName,
metricMaker,
externalIds,
externalIdCache,
retryHelper,
Runnables.doNothing());
}
@VisibleForTesting
public ExternalIdsUpdate(
GitRepositoryManager repoManager,
MetaDataUpdateFactory metaDataUpdateFactory,
@Nullable AccountCache accountCache,
AllUsersName allUsersName,
MetricMaker metricMaker,
ExternalIds externalIds,
ExternalIdCache externalIdCache,
RetryHelper retryHelper,
Runnable afterReadRevision) {
this.repoManager = checkNotNull(repoManager, "repoManager");
this.metaDataUpdateFactory = checkNotNull(metaDataUpdateFactory, "metaDataUpdateFactory");
this.accountCache = accountCache;
this.allUsersName = checkNotNull(allUsersName, "allUsersName");
this.externalIds = checkNotNull(externalIds, "externalIds");
this.externalIdCache = checkNotNull(externalIdCache, "externalIdCache");
this.retryHelper = checkNotNull(retryHelper, "retryHelper");
this.afterReadRevision = checkNotNull(afterReadRevision, "afterReadRevision");
this.updateCount =
metricMaker.newCounter(
"notedb/external_id_update_count",
new Description("Total number of external ID updates.").setRate().setUnit("updates"));
}
/**
* Inserts a new external ID.
*
* <p>If the external ID already exists, the insert fails with {@link OrmDuplicateKeyException}.
*/
public void insert(ExternalId extId) throws IOException, ConfigInvalidException, OrmException {
insert(Collections.singleton(extId));
}
/**
* Inserts new external IDs.
*
* <p>If any of the external ID already exists, the insert fails with {@link
* OrmDuplicateKeyException}.
*/
public void insert(Collection<ExternalId> extIds)
throws IOException, ConfigInvalidException, OrmException {
updateNoteMap(n -> n.insert(extIds));
}
/**
* Inserts or updates an external ID.
*
* <p>If the external ID already exists, it is overwritten, otherwise it is inserted.
*/
public void upsert(ExternalId extId) throws IOException, ConfigInvalidException, OrmException {
upsert(Collections.singleton(extId));
}
/**
* Inserts or updates external IDs.
*
* <p>If any of the external IDs already exists, it is overwritten. New external IDs are inserted.
*/
public void upsert(Collection<ExternalId> extIds)
throws IOException, ConfigInvalidException, OrmException {
updateNoteMap(n -> n.upsert(extIds));
}
/**
* Deletes an external ID.
*
* @throws IllegalStateException is thrown if there is an existing external ID that has the same
* key, but otherwise doesn't match the specified external ID.
*/
public void delete(ExternalId extId) throws IOException, ConfigInvalidException, OrmException {
delete(Collections.singleton(extId));
}
/**
* Deletes external IDs.
*
* @throws IllegalStateException is thrown if there is an existing external ID that has the same
* key as any of the external IDs that should be deleted, but otherwise doesn't match the that
* external ID.
*/
public void delete(Collection<ExternalId> extIds)
throws IOException, ConfigInvalidException, OrmException {
updateNoteMap(n -> n.delete(extIds));
}
/**
* Delete an external ID by key.
*
* @throws IllegalStateException is thrown if the external ID does not belong to the specified
* account.
*/
public void delete(Account.Id accountId, ExternalId.Key extIdKey)
throws IOException, ConfigInvalidException, OrmException {
delete(accountId, Collections.singleton(extIdKey));
}
/**
* Delete external IDs by external ID key.
*
* @throws IllegalStateException is thrown if any of the external IDs does not belong to the
* specified account.
*/
public void delete(Account.Id accountId, Collection<ExternalId.Key> extIdKeys)
throws IOException, ConfigInvalidException, OrmException {
updateNoteMap(n -> n.delete(accountId, extIdKeys));
}
/**
* Delete external IDs by external ID key.
*
* <p>The external IDs are deleted regardless of which account they belong to.
*/
public void deleteByKeys(Collection<ExternalId.Key> extIdKeys)
throws IOException, ConfigInvalidException, OrmException {
updateNoteMap(n -> n.deleteByKeys(extIdKeys));
}
/** Deletes all external IDs of the specified account. */
public void deleteAll(Account.Id accountId)
throws IOException, ConfigInvalidException, OrmException {
delete(externalIds.byAccount(accountId));
}
/**
* Replaces external IDs for an account by external ID keys.
*
* <p>Deletion of external IDs is done before adding the new external IDs. This means if an
* external ID key 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 any of the specified external IDs does not belong to
* the specified account.
*/
public void replace(
Account.Id accountId, Collection<ExternalId.Key> toDelete, Collection<ExternalId> toAdd)
throws IOException, ConfigInvalidException, OrmException {
updateNoteMap(n -> n.replace(accountId, toDelete, toAdd));
}
/**
* Replaces external IDs for an account by external ID keys.
*
* <p>Deletion of external IDs is done before adding the new external IDs. This means if an
* external ID key 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).
*
* <p>The external IDs are replaced regardless of which account they belong to.
*/
public void replaceByKeys(Collection<ExternalId.Key> toDelete, Collection<ExternalId> toAdd)
throws IOException, ConfigInvalidException, OrmException {
updateNoteMap(n -> n.replaceByKeys(toDelete, toAdd));
}
/**
* Replaces an external ID.
*
* @throws IllegalStateException is thrown if the specified external IDs belong to different
* accounts.
*/
public void replace(ExternalId toDelete, ExternalId toAdd)
throws IOException, ConfigInvalidException, OrmException {
replace(Collections.singleton(toDelete), Collections.singleton(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)
throws IOException, ConfigInvalidException, OrmException {
updateNoteMap(n -> n.replace(toDelete, toAdd));
}
private void updateNoteMap(ExternalIdUpdater updater)
throws IOException, ConfigInvalidException, OrmException {
retryHelper.execute(
ActionType.ACCOUNT_UPDATE,
() -> {
try (Repository repo = repoManager.openRepository(allUsersName)) {
ExternalIdNotes extIdNotes =
new ExternalIdNotes(externalIdCache, accountCache, repo)
.setAfterReadRevision(afterReadRevision)
.load();
updater.update(extIdNotes);
try (MetaDataUpdate metaDataUpdate = metaDataUpdateFactory.create()) {
extIdNotes.commit(metaDataUpdate);
}
extIdNotes.updateCaches();
updateCount.increment();
return null;
}
});
}
@FunctionalInterface
private static interface ExternalIdUpdater {
void update(ExternalIdNotes extIdsNotes)
throws IOException, ConfigInvalidException, OrmException;
}
@VisibleForTesting
@FunctionalInterface
public static interface MetaDataUpdateFactory {
MetaDataUpdate create() throws IOException;
}
}