All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.account.AccountsUpdate Maven / Gradle / Ivy

There is a newer version: 3.11.0-rc3
Show newest version
// Copyright (C) 2017 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.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toSet;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Runnables;
import com.google.gerrit.entities.Account;
import com.google.gerrit.exceptions.DuplicateKeyException;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.git.LockFailureException;
import com.google.gerrit.git.RefUpdateUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.externalids.ExternalIdNotes;
import com.google.gerrit.server.account.externalids.ExternalIdNotes.ExternalIdNotesLoader;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.CachedPreferences;
import com.google.gerrit.server.config.VersionedDefaultPreferences;
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.change.ReindexAfterRefUpdate;
import com.google.gerrit.server.notedb.Sequences;
import com.google.gerrit.server.update.RetryHelper;
import com.google.gerrit.server.update.RetryableAction.Action;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;

/**
 * Creates and updates accounts.
 *
 * 

This class should be used for all account updates. It supports updating account properties, * external IDs, preferences (general, diff and edit preferences) and project watches. * *

Updates to one account are always atomic. Batch updating several accounts within one * transaction is not supported. * *

For any account update the caller must provide a commit message, the account ID and an {@link * AccountUpdater}. The account updater allows to read the current {@link AccountState} and to * prepare updates to the account by calling setters on the provided {@link * InternalAccountUpdate.Builder}. If the current account state is of no interest the caller may * also provide a {@link Consumer} for {@link InternalAccountUpdate.Builder} instead of the account * updater. * *

The provided commit message is used for the update of the user branch. Using a precise and * unique commit message allows to identify the code from which an update was made when looking at a * commit in the user branch, and thus help debugging. * *

For creating a new account a new account ID can be retrieved from {@link * Sequences#nextAccountId()}. * *

The account updates are written to NoteDb. In NoteDb accounts are represented as user branches * in the {@code All-Users} repository. Optionally a user branch can contain a 'account.config' file * that stores account properties, such as full name, display name, preferred email, status and the * active flag. The timestamp of the first commit on a user branch denotes the registration date. * The initial commit on the user branch may be empty (since having an 'account.config' is * optional). See {@link AccountConfig} for details of the 'account.config' file format. In addition * the user branch can contain a 'preferences.config' config file to store preferences (see {@link * StoredPreferences}) and a 'watch.config' config file to store project watches (see {@link * ProjectWatches}). External IDs are stored separately in the {@code refs/meta/external-ids} notes * branch (see {@link ExternalIdNotes}). * *

On updating an account the account is evicted from the account cache and reindexed. The * eviction from the account cache and the reindexing is done by the {@link ReindexAfterRefUpdate} * class which receives the event about updating the user branch that is triggered by this class. * *

If external IDs are updated, the ExternalIdCache is automatically updated by {@link * ExternalIdNotes}. In addition {@link ExternalIdNotes} takes care about evicting and reindexing * corresponding accounts. This is needed because external ID updates don't touch the user branches. * Hence in this case the accounts are not evicted and reindexed via {@link ReindexAfterRefUpdate}. * *

Reindexing and flushing accounts from the account cache can be disabled by * *

    *
  • binding {@link GitReferenceUpdated#DISABLED} and *
  • passing an {@link * com.google.gerrit.server.account.externalids.ExternalIdNotes.FactoryNoReindex} factory as * parameter of {@link AccountsUpdate.Factory#create(IdentifiedUser, * ExternalIdNotes.ExternalIdNotesLoader)} *
* *

If there are concurrent account updates updating the user branch in NoteDb may fail with * {@link LockFailureException}. In this case the account update is automatically retried and the * account updater is invoked once more with the updated account state. This means the whole * read-modify-write sequence is atomic. Retrying is limited by a timeout. If the timeout is * exceeded the account update can still fail with {@link LockFailureException}. */ public class AccountsUpdate { public interface Factory { /** * Creates an {@code AccountsUpdate} which uses the identity of the specified user as author for * all commits related to accounts. The server identity will be used as committer. * *

Note: Please use this method with care and consider using the {@link * com.google.gerrit.server.UserInitiated} annotation on the provider of an {@code * AccountsUpdate} instead. * * @param currentUser the user to which modifications should be attributed * @param externalIdNotesLoader the loader that should be used to load external ID notes */ AccountsUpdate create(IdentifiedUser currentUser, ExternalIdNotesLoader externalIdNotesLoader); /** * Creates an {@code AccountsUpdate} which uses the server identity as author and committer for * all commits related to accounts. * *

Note: Please use this method with care and consider using the {@link * com.google.gerrit.server.ServerInitiated} annotation on the provider of an {@code * AccountsUpdate} instead. * * @param externalIdNotesLoader the loader that should be used to load external ID notes */ AccountsUpdate createWithServerIdent(ExternalIdNotesLoader externalIdNotesLoader); } /** * Updater for an account. * *

Allows to read the current state of an account and to prepare updates to it. */ @FunctionalInterface public interface AccountUpdater { /** * Prepare updates to an account. * *

Use the provided account only to read the current state of the account. Don't do updates * to the account. For updates use the provided account update builder. * * @param accountState the account that is being updated * @param update account update builder */ void update(AccountState accountState, InternalAccountUpdate.Builder update) throws IOException; static AccountUpdater join(List updaters) { return (accountState, update) -> { for (AccountUpdater updater : updaters) { updater.update(accountState, update); } }; } static AccountUpdater joinConsumers(List> consumers) { return join(Lists.transform(consumers, AccountUpdater::fromConsumer)); } static AccountUpdater fromConsumer(Consumer consumer) { return (a, u) -> consumer.accept(u); } } private final GitRepositoryManager repoManager; private final GitReferenceUpdated gitRefUpdated; private final Optional currentUser; private final AllUsersName allUsersName; private final ExternalIds externalIds; private final Provider metaDataUpdateInternalFactory; private final RetryHelper retryHelper; private final ExternalIdNotesLoader extIdNotesLoader; private final PersonIdent committerIdent; private final PersonIdent authorIdent; // Invoked after reading the account config. private final Runnable afterReadRevision; // Invoked after updating the account but before committing the changes. private final Runnable beforeCommit; @AssistedInject AccountsUpdate( GitRepositoryManager repoManager, GitReferenceUpdated gitRefUpdated, AllUsersName allUsersName, ExternalIds externalIds, Provider metaDataUpdateInternalFactory, RetryHelper retryHelper, @GerritPersonIdent PersonIdent serverIdent, @Assisted ExternalIdNotesLoader extIdNotesLoader) { this( repoManager, gitRefUpdated, Optional.empty(), allUsersName, externalIds, metaDataUpdateInternalFactory, retryHelper, extIdNotesLoader, serverIdent, createPersonIdent(serverIdent, Optional.empty()), Runnables.doNothing(), Runnables.doNothing()); } @AssistedInject AccountsUpdate( GitRepositoryManager repoManager, GitReferenceUpdated gitRefUpdated, AllUsersName allUsersName, ExternalIds externalIds, Provider metaDataUpdateInternalFactory, RetryHelper retryHelper, @GerritPersonIdent PersonIdent serverIdent, @Assisted IdentifiedUser currentUser, @Assisted ExternalIdNotesLoader extIdNotesLoader) { this( repoManager, gitRefUpdated, Optional.of(currentUser), allUsersName, externalIds, metaDataUpdateInternalFactory, retryHelper, extIdNotesLoader, serverIdent, createPersonIdent(serverIdent, Optional.of(currentUser)), Runnables.doNothing(), Runnables.doNothing()); } @VisibleForTesting public AccountsUpdate( GitRepositoryManager repoManager, GitReferenceUpdated gitRefUpdated, Optional currentUser, AllUsersName allUsersName, ExternalIds externalIds, Provider metaDataUpdateInternalFactory, RetryHelper retryHelper, ExternalIdNotesLoader extIdNotesLoader, PersonIdent committerIdent, PersonIdent authorIdent, Runnable afterReadRevision, Runnable beforeCommit) { this.repoManager = requireNonNull(repoManager, "repoManager"); this.gitRefUpdated = requireNonNull(gitRefUpdated, "gitRefUpdated"); this.currentUser = currentUser; this.allUsersName = requireNonNull(allUsersName, "allUsersName"); this.externalIds = requireNonNull(externalIds, "externalIds"); this.metaDataUpdateInternalFactory = requireNonNull(metaDataUpdateInternalFactory, "metaDataUpdateInternalFactory"); this.retryHelper = requireNonNull(retryHelper, "retryHelper"); this.extIdNotesLoader = requireNonNull(extIdNotesLoader, "extIdNotesLoader"); this.committerIdent = requireNonNull(committerIdent, "committerIdent"); this.authorIdent = requireNonNull(authorIdent, "authorIdent"); this.afterReadRevision = requireNonNull(afterReadRevision, "afterReadRevision"); this.beforeCommit = requireNonNull(beforeCommit, "beforeCommit"); } private static PersonIdent createPersonIdent( PersonIdent serverIdent, Optional user) { if (!user.isPresent()) { return serverIdent; } return user.get().newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone()); } /** * Inserts a new account. * * @param message commit message for the account creation, must not be {@code null or empty} * @param accountId ID of the new account * @param init consumer to populate the new account * @return the newly created account * @throws DuplicateKeyException if the account already exists * @throws IOException if creating the user branch fails due to an IO error * @throws ConfigInvalidException if any of the account fields has an invalid value */ public AccountState insert( String message, Account.Id accountId, Consumer init) throws IOException, ConfigInvalidException { return insert(message, accountId, AccountUpdater.fromConsumer(init)); } /** * Inserts a new account. * * @param message commit message for the account creation, must not be {@code null or empty} * @param accountId ID of the new account * @param updater updater to populate the new account * @return the newly created account * @throws DuplicateKeyException if the account already exists * @throws IOException if creating the user branch fails due to an IO error * @throws ConfigInvalidException if any of the account fields has an invalid value */ public AccountState insert(String message, Account.Id accountId, AccountUpdater updater) throws IOException, ConfigInvalidException { return updateAccount( r -> { AccountConfig accountConfig = read(r, accountId); Account account = accountConfig.getNewAccount(new Timestamp(committerIdent.getWhen().getTime())); AccountState accountState = AccountState.forAccount(account); InternalAccountUpdate.Builder updateBuilder = InternalAccountUpdate.builder(); updater.update(accountState, updateBuilder); InternalAccountUpdate update = updateBuilder.build(); accountConfig.setAccountUpdate(update); ExternalIdNotes extIdNotes = createExternalIdNotes(r, accountConfig.getExternalIdsRev(), accountId, update); CachedPreferences defaultPreferences = CachedPreferences.fromConfig(VersionedDefaultPreferences.get(r, allUsersName)); UpdatedAccount updatedAccounts = new UpdatedAccount( externalIds, message, accountConfig, extIdNotes, defaultPreferences); updatedAccounts.setCreated(true); return updatedAccounts; }) .get(); } /** * Gets the account and updates it atomically. * *

Changing the registration date of an account is not supported. * * @param message commit message for the account update, must not be {@code null or empty} * @param accountId ID of the account * @param update consumer to update the account, only invoked if the account exists * @return the updated account, {@link Optional#empty()} if the account doesn't exist * @throws IOException if updating the user branch fails due to an IO error * @throws LockFailureException if updating the user branch still fails due to concurrent updates * after the retry timeout exceeded * @throws ConfigInvalidException if any of the account fields has an invalid value */ public Optional update( String message, Account.Id accountId, Consumer update) throws LockFailureException, IOException, ConfigInvalidException { return update(message, accountId, AccountUpdater.fromConsumer(update)); } /** * Gets the account and updates it atomically. * *

Changing the registration date of an account is not supported. * * @param message commit message for the account update, must not be {@code null or empty} * @param accountId ID of the account * @param updater updater to update the account, only invoked if the account exists * @return the updated account, {@link Optional#empty} if the account doesn't exist * @throws IOException if updating the user branch fails due to an IO error * @throws LockFailureException if updating the user branch still fails due to concurrent updates * after the retry timeout exceeded * @throws ConfigInvalidException if any of the account fields has an invalid value */ public Optional update(String message, Account.Id accountId, AccountUpdater updater) throws LockFailureException, IOException, ConfigInvalidException { return updateAccount( r -> { AccountConfig accountConfig = read(r, accountId); CachedPreferences defaultPreferences = CachedPreferences.fromConfig(VersionedDefaultPreferences.get(r, allUsersName)); Optional account = AccountState.fromAccountConfig(externalIds, accountConfig, defaultPreferences); if (!account.isPresent()) { return null; } InternalAccountUpdate.Builder updateBuilder = InternalAccountUpdate.builder(); updater.update(account.get(), updateBuilder); InternalAccountUpdate update = updateBuilder.build(); accountConfig.setAccountUpdate(update); ExternalIdNotes extIdNotes = createExternalIdNotes(r, accountConfig.getExternalIdsRev(), accountId, update); CachedPreferences cachedDefaultPreferences = CachedPreferences.fromConfig(VersionedDefaultPreferences.get(r, allUsersName)); UpdatedAccount updatedAccounts = new UpdatedAccount( externalIds, message, accountConfig, extIdNotes, cachedDefaultPreferences); return updatedAccounts; }); } private AccountConfig read(Repository allUsersRepo, Account.Id accountId) throws IOException, ConfigInvalidException { AccountConfig accountConfig = new AccountConfig(accountId, allUsersName, allUsersRepo).load(); afterReadRevision.run(); return accountConfig; } private Optional updateAccount(AccountUpdate accountUpdate) throws IOException, ConfigInvalidException { return executeAccountUpdate( () -> { try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) { UpdatedAccount updatedAccount = accountUpdate.update(allUsersRepo); if (updatedAccount == null) { return Optional.empty(); } commit(allUsersRepo, updatedAccount); return Optional.of(updatedAccount.getAccount()); } }); } private Optional executeAccountUpdate(Action> action) throws IOException, ConfigInvalidException { try { return retryHelper.accountUpdate("updateAccount", action).call(); } catch (Exception e) { Throwables.throwIfUnchecked(e); Throwables.throwIfInstanceOf(e, IOException.class); Throwables.throwIfInstanceOf(e, ConfigInvalidException.class); throw new StorageException(e); } } private ExternalIdNotes createExternalIdNotes( Repository allUsersRepo, Optional rev, Account.Id accountId, InternalAccountUpdate update) throws IOException, ConfigInvalidException, DuplicateKeyException { ExternalIdNotes.checkSameAccount( Iterables.concat( update.getCreatedExternalIds(), update.getUpdatedExternalIds(), update.getDeletedExternalIds()), accountId); ExternalIdNotes extIdNotes = extIdNotesLoader.load(allUsersRepo, rev.orElse(ObjectId.zeroId())); extIdNotes.replace(update.getDeletedExternalIds(), update.getCreatedExternalIds()); extIdNotes.upsert(update.getUpdatedExternalIds()); return extIdNotes; } private void commit(Repository allUsersRepo, UpdatedAccount updatedAccount) throws IOException { beforeCommit.run(); BatchRefUpdate batchRefUpdate = allUsersRepo.getRefDatabase().newBatchUpdate(); if (updatedAccount.isCreated()) { commitNewAccountConfig( updatedAccount.getMessage(), allUsersRepo, batchRefUpdate, updatedAccount.getAccountConfig()); } else { commitAccountConfig( updatedAccount.getMessage(), allUsersRepo, batchRefUpdate, updatedAccount.getAccountConfig()); } commitExternalIdUpdates( updatedAccount.getMessage(), allUsersRepo, batchRefUpdate, updatedAccount.getExternalIdNotes()); RefUpdateUtil.executeChecked(batchRefUpdate, allUsersRepo); // Skip accounts that are updated when evicting the account cache via ExternalIdNotes to avoid // double reindexing. The updated accounts will already be reindexed by ReindexAfterRefUpdate. Set accountsThatWillBeReindexByReindexAfterRefUpdate = getUpdatedAccounts(batchRefUpdate); updatedAccount .getExternalIdNotes() .updateCaches(accountsThatWillBeReindexByReindexAfterRefUpdate); gitRefUpdated.fire( allUsersName, batchRefUpdate, currentUser.map(user -> user.state()).orElse(null)); } private static Set getUpdatedAccounts(BatchRefUpdate batchRefUpdate) { return batchRefUpdate.getCommands().stream() .map(c -> Account.Id.fromRef(c.getRefName())) .filter(Objects::nonNull) .collect(toSet()); } private void commitNewAccountConfig( String message, Repository allUsersRepo, BatchRefUpdate batchRefUpdate, AccountConfig accountConfig) throws IOException { // When creating a new account we must allow empty commits so that the user branch gets created // with an empty commit when no account properties are set and hence no 'account.config' file // will be created. commitAccountConfig(message, allUsersRepo, batchRefUpdate, accountConfig, true); } private void commitAccountConfig( String message, Repository allUsersRepo, BatchRefUpdate batchRefUpdate, AccountConfig accountConfig) throws IOException { commitAccountConfig(message, allUsersRepo, batchRefUpdate, accountConfig, false); } private void commitAccountConfig( String message, Repository allUsersRepo, BatchRefUpdate batchRefUpdate, AccountConfig accountConfig, boolean allowEmptyCommit) throws IOException { try (MetaDataUpdate md = createMetaDataUpdate(message, allUsersRepo, batchRefUpdate)) { md.setAllowEmpty(allowEmptyCommit); accountConfig.commit(md); } } private void commitExternalIdUpdates( String message, Repository allUsersRepo, BatchRefUpdate batchRefUpdate, ExternalIdNotes extIdNotes) throws IOException { try (MetaDataUpdate md = createMetaDataUpdate(message, allUsersRepo, batchRefUpdate)) { extIdNotes.commit(md); } } private MetaDataUpdate createMetaDataUpdate( String message, Repository allUsersRepo, BatchRefUpdate batchRefUpdate) { MetaDataUpdate metaDataUpdate = metaDataUpdateInternalFactory.get().create(allUsersName, allUsersRepo, batchRefUpdate); if (!message.endsWith("\n")) { message = message + "\n"; } metaDataUpdate.getCommitBuilder().setMessage(message); metaDataUpdate.getCommitBuilder().setCommitter(committerIdent); metaDataUpdate.getCommitBuilder().setAuthor(authorIdent); return metaDataUpdate; } @FunctionalInterface private static interface AccountUpdate { UpdatedAccount update(Repository allUsersRepo) throws IOException, ConfigInvalidException; } private static class UpdatedAccount { private final ExternalIds externalIds; private final String message; private final AccountConfig accountConfig; private final ExternalIdNotes extIdNotes; private final CachedPreferences defaultPreferences; private boolean created; private UpdatedAccount( ExternalIds externalIds, String message, AccountConfig accountConfig, ExternalIdNotes extIdNotes, CachedPreferences defaultPreferences) { checkState(!Strings.isNullOrEmpty(message), "message for account update must be set"); this.externalIds = requireNonNull(externalIds); this.message = requireNonNull(message); this.accountConfig = requireNonNull(accountConfig); this.extIdNotes = requireNonNull(extIdNotes); this.defaultPreferences = defaultPreferences; } public String getMessage() { return message; } public AccountConfig getAccountConfig() { return accountConfig; } public AccountState getAccount() throws IOException { return AccountState.fromAccountConfig( externalIds, accountConfig, extIdNotes, defaultPreferences) .get(); } public ExternalIdNotes getExternalIdNotes() { return extIdNotes; } public void setCreated(boolean created) { this.created = created; } public boolean isCreated() { return created; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy