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

com.google.gerrit.server.group.db.GroupsUpdate Maven / Gradle / Ivy

The 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.group.db;

import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.GROUPS_UPDATE;

import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.InternalGroup;
import com.google.gerrit.entities.Project;
import com.google.gerrit.exceptions.DuplicateKeyException;
import com.google.gerrit.exceptions.NoSuchGroupException;
import com.google.gerrit.git.RefUpdateUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.GroupIncludeCache;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.GerritServerId;
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.group.GroupAuditService;
import com.google.gerrit.server.index.group.GroupIndexer;
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.update.RetryHelper;
import com.google.gerrit.server.update.context.RefUpdateContext;
import com.google.gerrit.server.util.time.TimeUtil;
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.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;

/**
 * A database accessor for write calls related to groups.
 *
 * 

All calls which write group related details to the database are gathered here. Other classes * should always use this class instead of accessing the database directly. There are a few * exceptions though: schema classes, wrapper classes, and classes executed during init. The latter * ones should use {@link com.google.gerrit.pgm.init.GroupsOnInit} instead. * *

If not explicitly stated, all methods of this class refer to internal groups. */ public class GroupsUpdate { public interface Factory { /** * Creates a {@link GroupsUpdate} which uses the identity of the specified user to mark database * modifications executed by it. For NoteDb, this identity is used as author and committer for * all related commits. * *

Note: Please use this method with care and consider using the {@link * com.google.gerrit.server.UserInitiated} annotation on the provider of a {@link GroupsUpdate} * instead. * * @param currentUser the user to which modifications should be attributed */ GroupsUpdate create(IdentifiedUser currentUser); /** * Creates a {@link GroupsUpdate} which uses the server identity to mark database modifications * executed by it. For NoteDb, this identity is used as author and committer for all related * commits. * *

Note: Please use this method with care and consider using the {@link * com.google.gerrit.server.ServerInitiated} annotation on the provider of a {@link * GroupsUpdate} instead. */ GroupsUpdate createWithServerIdent(); } private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private final GitRepositoryManager repoManager; private final AllUsersName allUsersName; private final GroupCache groupCache; private final GroupIncludeCache groupIncludeCache; private final Provider indexer; private final GroupAuditService groupAuditService; private final RenameGroupOp.Factory renameGroupOpFactory; private final Optional currentUser; private final AuditLogFormatter auditLogFormatter; private final PersonIdent authorIdent; private final MetaDataUpdateFactory metaDataUpdateFactory; private final GitReferenceUpdated gitRefUpdated; private final RetryHelper retryHelper; @AssistedInject GroupsUpdate( GitRepositoryManager repoManager, AllUsersName allUsersName, GroupBackend groupBackend, GroupCache groupCache, GroupIncludeCache groupIncludeCache, Provider indexer, GroupAuditService auditService, AccountCache accountCache, RenameGroupOp.Factory renameGroupOpFactory, @GerritServerId String serverId, @GerritPersonIdent PersonIdent serverIdent, MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory, GitReferenceUpdated gitRefUpdated, RetryHelper retryHelper) { this( repoManager, allUsersName, groupBackend, groupCache, groupIncludeCache, indexer, auditService, accountCache, renameGroupOpFactory, serverId, serverIdent, metaDataUpdateInternalFactory, gitRefUpdated, retryHelper, Optional.empty()); } @AssistedInject GroupsUpdate( GitRepositoryManager repoManager, AllUsersName allUsersName, GroupBackend groupBackend, GroupCache groupCache, GroupIncludeCache groupIncludeCache, Provider indexer, GroupAuditService auditService, AccountCache accountCache, RenameGroupOp.Factory renameGroupOpFactory, @GerritServerId String serverId, @GerritPersonIdent PersonIdent serverIdent, MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory, GitReferenceUpdated gitRefUpdated, RetryHelper retryHelper, @Assisted IdentifiedUser currentUser) { this( repoManager, allUsersName, groupBackend, groupCache, groupIncludeCache, indexer, auditService, accountCache, renameGroupOpFactory, serverId, serverIdent, metaDataUpdateInternalFactory, gitRefUpdated, retryHelper, Optional.of(currentUser)); } private GroupsUpdate( GitRepositoryManager repoManager, AllUsersName allUsersName, GroupBackend groupBackend, GroupCache groupCache, GroupIncludeCache groupIncludeCache, Provider indexer, GroupAuditService auditService, AccountCache accountCache, RenameGroupOp.Factory renameGroupOpFactory, @GerritServerId String serverId, @GerritPersonIdent PersonIdent serverIdent, MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory, GitReferenceUpdated gitRefUpdated, RetryHelper retryHelper, Optional currentUser) { this.repoManager = repoManager; this.allUsersName = allUsersName; this.groupCache = groupCache; this.groupIncludeCache = groupIncludeCache; this.indexer = indexer; this.groupAuditService = auditService; this.renameGroupOpFactory = renameGroupOpFactory; this.gitRefUpdated = gitRefUpdated; this.retryHelper = retryHelper; this.currentUser = currentUser; auditLogFormatter = AuditLogFormatter.createBackedBy(accountCache, groupBackend, serverId); metaDataUpdateFactory = getMetaDataUpdateFactory( metaDataUpdateInternalFactory, currentUser, serverIdent, auditLogFormatter); authorIdent = getAuthorIdent(serverIdent, currentUser); } private static MetaDataUpdateFactory getMetaDataUpdateFactory( MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory, Optional currentUser, PersonIdent serverIdent, AuditLogFormatter auditLogFormatter) { return (projectName, repository, batchRefUpdate) -> { MetaDataUpdate metaDataUpdate = metaDataUpdateInternalFactory.create(projectName, repository, batchRefUpdate); metaDataUpdate.getCommitBuilder().setCommitter(serverIdent); PersonIdent authorIdent; if (currentUser.isPresent()) { metaDataUpdate.setAuthor(currentUser.get()); authorIdent = auditLogFormatter.getParsableAuthorIdent(currentUser.get().getAccount(), serverIdent); } else { authorIdent = serverIdent; } metaDataUpdate.getCommitBuilder().setAuthor(authorIdent); return metaDataUpdate; }; } private static PersonIdent getAuthorIdent( PersonIdent serverIdent, Optional currentUser) { return currentUser.map(user -> createPersonIdent(serverIdent, user)).orElse(serverIdent); } private static PersonIdent createPersonIdent(PersonIdent ident, IdentifiedUser user) { return user.newCommitterIdent(ident); } /** * Creates the specified group for the specified members (accounts). * * @param groupCreation an {@link InternalGroupCreation} which specifies all mandatory properties * of the group * @param groupDelta a {@link GroupDelta} which specifies optional properties of the group. If * this {@link GroupDelta} updates a property which was already specified by the {@link * InternalGroupCreation}, the value of this {@link GroupDelta} wins. * @throws DuplicateKeyException if a group with the chosen name already exists * @throws IOException if indexing fails, or an error occurs while reading/writing from/to NoteDb * @return the created {@link InternalGroup} */ @CanIgnoreReturnValue public InternalGroup createGroup(InternalGroupCreation groupCreation, GroupDelta groupDelta) throws DuplicateKeyException, IOException, ConfigInvalidException { try (TraceTimer ignored = TraceContext.newTimer( "Creating group", Metadata.builder() .groupName(groupDelta.getName().orElseGet(groupCreation::getNameKey).get()) .build())) { InternalGroup createdGroup = createGroupInNoteDbWithRetry(groupCreation, groupDelta); evictCachesOnGroupCreation(createdGroup); dispatchAuditEventsOnGroupCreation(createdGroup); return createdGroup; } } /** * Updates the specified group. * * @param groupUuid the UUID of the group to update * @param groupDelta a {@link GroupDelta} which indicates the desired updates on the group * @throws DuplicateKeyException if the new name of the group is used by another group * @throws IOException if indexing fails, or an error occurs while reading/writing from/to NoteDb * @throws NoSuchGroupException if the specified group doesn't exist */ public void updateGroup(AccountGroup.UUID groupUuid, GroupDelta groupDelta) throws DuplicateKeyException, IOException, NoSuchGroupException, ConfigInvalidException { try (TraceTimer ignored = TraceContext.newTimer( "Updating group", Metadata.builder().groupUuid(groupUuid.get()).build())) { Optional updatedOn = groupDelta.getUpdatedOn(); if (!updatedOn.isPresent()) { updatedOn = Optional.of(TimeUtil.now()); groupDelta = groupDelta.toBuilder().setUpdatedOn(updatedOn.get()).build(); } UpdateResult result = updateGroupInNoteDbWithRetry(groupUuid, groupDelta); updateNameInProjectConfigsIfNecessary(result); evictCachesOnGroupUpdate(result); dispatchAuditEventsOnGroupUpdate(result, updatedOn.get()); } } private InternalGroup createGroupInNoteDbWithRetry( InternalGroupCreation groupCreation, GroupDelta groupDelta) throws IOException, ConfigInvalidException, DuplicateKeyException { try (RefUpdateContext ctx = RefUpdateContext.open(GROUPS_UPDATE)) { try { return retryHelper .groupUpdate("createGroup", () -> createGroupInNoteDb(groupCreation, groupDelta)) .call(); } catch (Exception e) { Throwables.throwIfUnchecked(e); Throwables.throwIfInstanceOf(e, IOException.class); Throwables.throwIfInstanceOf(e, ConfigInvalidException.class); Throwables.throwIfInstanceOf(e, DuplicateKeyException.class); throw new IOException(e); } } } @VisibleForTesting public InternalGroup createGroupInNoteDb( InternalGroupCreation groupCreation, GroupDelta groupDelta) throws IOException, ConfigInvalidException, DuplicateKeyException { try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) { AccountGroup.NameKey groupName = groupDelta.getName().orElseGet(groupCreation::getNameKey); GroupNameNotes groupNameNotes = GroupNameNotes.forNewGroup( allUsersName, allUsersRepo, groupCreation.getGroupUUID(), groupName); GroupConfig groupConfig = GroupConfig.createForNewGroup(allUsersName, allUsersRepo, groupCreation); groupConfig.setGroupDelta(groupDelta, auditLogFormatter); commit(allUsersRepo, groupConfig, groupNameNotes); return groupConfig .getLoadedGroup() .orElseThrow( () -> new IllegalStateException("Created group wasn't automatically loaded")); } } private UpdateResult updateGroupInNoteDbWithRetry( AccountGroup.UUID groupUuid, GroupDelta groupDelta) throws IOException, ConfigInvalidException, DuplicateKeyException, NoSuchGroupException { try { return retryHelper .groupUpdate("updateGroup", () -> updateGroupInNoteDb(groupUuid, groupDelta)) .call(); } catch (Exception e) { Throwables.throwIfUnchecked(e); Throwables.throwIfInstanceOf(e, IOException.class); Throwables.throwIfInstanceOf(e, ConfigInvalidException.class); Throwables.throwIfInstanceOf(e, DuplicateKeyException.class); Throwables.throwIfInstanceOf(e, NoSuchGroupException.class); throw new IOException(e); } } @VisibleForTesting @CanIgnoreReturnValue public UpdateResult updateGroupInNoteDb(AccountGroup.UUID groupUuid, GroupDelta groupDelta) throws IOException, ConfigInvalidException, DuplicateKeyException, NoSuchGroupException { try (RefUpdateContext ctx = RefUpdateContext.open(GROUPS_UPDATE)) { try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) { GroupConfig groupConfig = GroupConfig.loadForGroup(allUsersName, allUsersRepo, groupUuid); groupConfig.setGroupDelta(groupDelta, auditLogFormatter); if (!groupConfig.getLoadedGroup().isPresent()) { throw new NoSuchGroupException(groupUuid); } InternalGroup originalGroup = groupConfig.getLoadedGroup().get(); GroupNameNotes groupNameNotes = null; if (groupDelta.getName().isPresent()) { AccountGroup.NameKey oldName = originalGroup.getNameKey(); AccountGroup.NameKey newName = groupDelta.getName().get(); groupNameNotes = GroupNameNotes.forRename(allUsersName, allUsersRepo, groupUuid, oldName, newName); } commit(allUsersRepo, groupConfig, groupNameNotes); InternalGroup updatedGroup = groupConfig .getLoadedGroup() .orElseThrow( () -> new IllegalStateException("Updated group wasn't automatically loaded")); return getUpdateResult(originalGroup, updatedGroup); } } } private static UpdateResult getUpdateResult( InternalGroup originalGroup, InternalGroup updatedGroup) { Set addedMembers = Sets.difference(updatedGroup.getMembers(), originalGroup.getMembers()); Set deletedMembers = Sets.difference(originalGroup.getMembers(), updatedGroup.getMembers()); Set addedSubgroups = Sets.difference(updatedGroup.getSubgroups(), originalGroup.getSubgroups()); Set deletedSubgroups = Sets.difference(originalGroup.getSubgroups(), updatedGroup.getSubgroups()); UpdateResult.Builder resultBuilder = UpdateResult.builder() .setGroupUuid(updatedGroup.getGroupUUID()) .setGroupId(updatedGroup.getId()) .setGroupName(updatedGroup.getNameKey()) .setAddedMembers(addedMembers) .setDeletedMembers(deletedMembers) .setAddedSubgroups(addedSubgroups) .setDeletedSubgroups(deletedSubgroups); if (!Objects.equals(originalGroup.getNameKey(), updatedGroup.getNameKey())) { resultBuilder.setPreviousGroupName(originalGroup.getNameKey()); } return resultBuilder.build(); } private void commit( Repository allUsersRepo, GroupConfig groupConfig, @Nullable GroupNameNotes groupNameNotes) throws IOException { BatchRefUpdate batchRefUpdate = allUsersRepo.getRefDatabase().newBatchUpdate(); try (MetaDataUpdate metaDataUpdate = metaDataUpdateFactory.create(allUsersName, allUsersRepo, batchRefUpdate)) { groupConfig.commit(metaDataUpdate); } if (groupNameNotes != null) { // MetaDataUpdates unfortunately can't be reused. -> Create a new one. try (MetaDataUpdate metaDataUpdate = metaDataUpdateFactory.create(allUsersName, allUsersRepo, batchRefUpdate)) { groupNameNotes.commit(metaDataUpdate); } } RefUpdateUtil.executeChecked(batchRefUpdate, allUsersRepo); gitRefUpdated.fire( allUsersName, batchRefUpdate, currentUser.map(user -> user.state()).orElse(null)); } private void evictCachesOnGroupCreation(InternalGroup createdGroup) { logger.atFine().log("evict caches on creation of group %s", createdGroup.getGroupUUID()); // By UUID is used for the index and hence should be evicted before refreshing the index. groupCache.evict(createdGroup.getGroupUUID()); indexer.get().index(createdGroup.getGroupUUID()); // These caches use the result from the index and hence must be evicted after refreshing the // index. groupCache.evict(createdGroup.getId()); groupCache.evict(createdGroup.getNameKey()); createdGroup.getMembers().forEach(groupIncludeCache::evictGroupsWithMember); createdGroup.getSubgroups().forEach(groupIncludeCache::evictParentGroupsOf); } private void evictCachesOnGroupUpdate(UpdateResult result) { logger.atFine().log("evict caches on update of group %s", result.getGroupUuid()); // By UUID is used for the index and hence should be evicted before refreshing the index. groupCache.evict(result.getGroupUuid()); indexer.get().index(result.getGroupUuid()); // These caches use the result from the index and hence must be evicted after refreshing the // index. groupCache.evict(result.getGroupId()); groupCache.evict(result.getGroupName()); result.getPreviousGroupName().ifPresent(groupCache::evict); result.getAddedMembers().forEach(groupIncludeCache::evictGroupsWithMember); result.getDeletedMembers().forEach(groupIncludeCache::evictGroupsWithMember); result.getAddedSubgroups().forEach(groupIncludeCache::evictParentGroupsOf); result.getDeletedSubgroups().forEach(groupIncludeCache::evictParentGroupsOf); } private void updateNameInProjectConfigsIfNecessary(UpdateResult result) { if (result.getPreviousGroupName().isPresent()) { AccountGroup.NameKey previousName = result.getPreviousGroupName().get(); @SuppressWarnings("unused") Future possiblyIgnoredError = renameGroupOpFactory .create( authorIdent, result.getGroupUuid(), previousName.get(), result.getGroupName().get()) .start(0, TimeUnit.MILLISECONDS); } } private void dispatchAuditEventsOnGroupCreation(InternalGroup createdGroup) { if (!currentUser.isPresent()) { return; } if (!createdGroup.getMembers().isEmpty()) { groupAuditService.dispatchAddMembers( currentUser.get().getAccountId(), createdGroup.getGroupUUID(), createdGroup.getMembers(), createdGroup.getCreatedOn()); } if (!createdGroup.getSubgroups().isEmpty()) { groupAuditService.dispatchAddSubgroups( currentUser.get().getAccountId(), createdGroup.getGroupUUID(), createdGroup.getSubgroups(), createdGroup.getCreatedOn()); } } private void dispatchAuditEventsOnGroupUpdate(UpdateResult result, Instant updatedOn) { if (!currentUser.isPresent()) { return; } if (!result.getAddedMembers().isEmpty()) { groupAuditService.dispatchAddMembers( currentUser.get().getAccountId(), result.getGroupUuid(), result.getAddedMembers(), updatedOn); } if (!result.getDeletedMembers().isEmpty()) { groupAuditService.dispatchDeleteMembers( currentUser.get().getAccountId(), result.getGroupUuid(), result.getDeletedMembers(), updatedOn); } if (!result.getAddedSubgroups().isEmpty()) { groupAuditService.dispatchAddSubgroups( currentUser.get().getAccountId(), result.getGroupUuid(), result.getAddedSubgroups(), updatedOn); } if (!result.getDeletedSubgroups().isEmpty()) { groupAuditService.dispatchDeleteSubgroups( currentUser.get().getAccountId(), result.getGroupUuid(), result.getDeletedSubgroups(), updatedOn); } } @FunctionalInterface private interface MetaDataUpdateFactory { MetaDataUpdate create( Project.NameKey projectName, Repository repository, BatchRefUpdate batchRefUpdate) throws IOException; } @AutoValue abstract static class UpdateResult { abstract AccountGroup.UUID getGroupUuid(); abstract AccountGroup.Id getGroupId(); abstract AccountGroup.NameKey getGroupName(); abstract Optional getPreviousGroupName(); abstract ImmutableSet getAddedMembers(); abstract ImmutableSet getDeletedMembers(); abstract ImmutableSet getAddedSubgroups(); abstract ImmutableSet getDeletedSubgroups(); static Builder builder() { return new AutoValue_GroupsUpdate_UpdateResult.Builder(); } @AutoValue.Builder abstract static class Builder { abstract Builder setGroupUuid(AccountGroup.UUID groupUuid); abstract Builder setGroupId(AccountGroup.Id groupId); abstract Builder setGroupName(AccountGroup.NameKey name); abstract Builder setPreviousGroupName(AccountGroup.NameKey previousName); abstract Builder setAddedMembers(Set addedMembers); abstract Builder setDeletedMembers(Set deletedMembers); abstract Builder setAddedSubgroups(Set addedSubgroups); abstract Builder setDeletedSubgroups(Set deletedSubgroups); abstract UpdateResult build(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy