| // 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.common.collect.ImmutableSet.toImmutableSet; |
| import static com.google.gerrit.server.group.db.Groups.getExistingGroupFromReviewDb; |
| |
| import com.google.auto.value.AutoValue; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Strings; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Sets; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.common.TimeUtil; |
| import com.google.gerrit.common.data.GroupDescription; |
| import com.google.gerrit.common.errors.NoSuchGroupException; |
| import com.google.gerrit.reviewdb.client.Account; |
| import com.google.gerrit.reviewdb.client.AccountGroup; |
| import com.google.gerrit.reviewdb.client.AccountGroupById; |
| import com.google.gerrit.reviewdb.client.AccountGroupMember; |
| import com.google.gerrit.reviewdb.client.AccountGroupName; |
| import com.google.gerrit.reviewdb.client.Project; |
| import com.google.gerrit.reviewdb.server.ReviewDb; |
| import com.google.gerrit.reviewdb.server.ReviewDbUtil; |
| 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.AccountState; |
| 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.audit.AuditService; |
| import com.google.gerrit.server.config.AllUsersName; |
| import com.google.gerrit.server.config.GerritServerConfig; |
| 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.MetaDataUpdate; |
| import com.google.gerrit.server.git.RenameGroupOp; |
| import com.google.gerrit.server.group.InternalGroup; |
| import com.google.gerrit.server.notedb.GroupsMigration; |
| import com.google.gerrit.server.update.RefUpdateUtil; |
| import com.google.gwtorm.server.OrmDuplicateKeyException; |
| import com.google.gwtorm.server.OrmException; |
| import com.google.inject.Inject; |
| import com.google.inject.assistedinject.Assisted; |
| import java.io.IOException; |
| import java.sql.Timestamp; |
| 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.Config; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.PersonIdent; |
| import org.eclipse.jgit.lib.Repository; |
| |
| /** |
| * A database accessor for write calls related to groups. |
| * |
| * <p>All calls which write group related details to the database (either ReviewDb or NoteDb) 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 {@code GroupsOnInit} instead. |
| * |
| * <p>If not explicitly stated, all methods of this class refer to <em>internal</em> groups. |
| */ |
| public class GroupsUpdate { |
| public interface Factory { |
| /** |
| * Creates a {@code 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. |
| * |
| * <p><strong>Note</strong>: Please use this method with care and rather consider to use the |
| * correct annotation on the provider of a {@code GroupsUpdate} instead. |
| * |
| * @param currentUser the user to which modifications should be attributed, or {@code null} if |
| * the Gerrit server identity should be used |
| */ |
| GroupsUpdate create(@Nullable IdentifiedUser currentUser); |
| } |
| |
| private final GitRepositoryManager repoManager; |
| private final AllUsersName allUsersName; |
| private final GroupBackend groupBackend; |
| private final GroupCache groupCache; |
| private final GroupIncludeCache groupIncludeCache; |
| private final AuditService auditService; |
| private final AccountCache accountCache; |
| private final RenameGroupOp.Factory renameGroupOpFactory; |
| private final String serverId; |
| @Nullable private final IdentifiedUser currentUser; |
| private final PersonIdent authorIdent; |
| private final MetaDataUpdateFactory metaDataUpdateFactory; |
| private final GroupsMigration groupsMigration; |
| private final GitReferenceUpdated gitRefUpdated; |
| private final boolean reviewDbUpdatesAreBlocked; |
| |
| @Inject |
| GroupsUpdate( |
| GitRepositoryManager repoManager, |
| AllUsersName allUsersName, |
| GroupBackend groupBackend, |
| GroupCache groupCache, |
| GroupIncludeCache groupIncludeCache, |
| AuditService auditService, |
| AccountCache accountCache, |
| RenameGroupOp.Factory renameGroupOpFactory, |
| @GerritServerId String serverId, |
| @GerritPersonIdent PersonIdent serverIdent, |
| MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory, |
| GroupsMigration groupsMigration, |
| @GerritServerConfig Config config, |
| GitReferenceUpdated gitRefUpdated, |
| @Assisted @Nullable IdentifiedUser currentUser) { |
| this.repoManager = repoManager; |
| this.allUsersName = allUsersName; |
| this.groupBackend = groupBackend; |
| this.groupCache = groupCache; |
| this.groupIncludeCache = groupIncludeCache; |
| this.auditService = auditService; |
| this.accountCache = accountCache; |
| this.renameGroupOpFactory = renameGroupOpFactory; |
| this.serverId = serverId; |
| this.groupsMigration = groupsMigration; |
| this.gitRefUpdated = gitRefUpdated; |
| this.currentUser = currentUser; |
| metaDataUpdateFactory = |
| getMetaDataUpdateFactory(metaDataUpdateInternalFactory, currentUser, serverIdent, serverId); |
| authorIdent = getAuthorIdent(serverIdent, currentUser); |
| reviewDbUpdatesAreBlocked = config.getBoolean("user", null, "blockReviewDbGroupUpdates", false); |
| } |
| |
| private static MetaDataUpdateFactory getMetaDataUpdateFactory( |
| MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory, |
| @Nullable IdentifiedUser currentUser, |
| PersonIdent serverIdent, |
| String serverId) { |
| return (projectName, repository, batchRefUpdate) -> { |
| MetaDataUpdate metaDataUpdate = |
| metaDataUpdateInternalFactory.create(projectName, repository, batchRefUpdate); |
| metaDataUpdate.getCommitBuilder().setCommitter(serverIdent); |
| PersonIdent authorIdent; |
| if (currentUser != null) { |
| metaDataUpdate.setAuthor(currentUser); |
| authorIdent = getAuditLogAuthorIdent(currentUser.getAccount(), serverIdent, serverId); |
| } else { |
| authorIdent = serverIdent; |
| } |
| metaDataUpdate.getCommitBuilder().setAuthor(authorIdent); |
| return metaDataUpdate; |
| }; |
| } |
| |
| private static PersonIdent getAuditLogAuthorIdent( |
| Account author, PersonIdent serverIdent, String serverId) { |
| return new PersonIdent( |
| author.getName(), |
| getEmailForAuditLog(author.getId(), serverId), |
| serverIdent.getWhen(), |
| serverIdent.getTimeZone()); |
| } |
| |
| private static PersonIdent getAuthorIdent( |
| PersonIdent serverIdent, @Nullable IdentifiedUser currentUser) { |
| return currentUser != null ? createPersonIdent(serverIdent, currentUser) : serverIdent; |
| } |
| |
| private static PersonIdent createPersonIdent(PersonIdent ident, IdentifiedUser user) { |
| return user.newCommitterIdent(ident.getWhen(), ident.getTimeZone()); |
| } |
| |
| /** |
| * Creates the specified group for the specified members (accounts). |
| * |
| * @param db the {@code ReviewDb} instance to update |
| * @param groupCreation an {@code InternalGroupCreation} which specifies all mandatory properties |
| * of the group |
| * @param groupUpdate an {@code InternalGroupUpdate} which specifies optional properties of the |
| * group. If this {@code InternalGroupUpdate} updates a property which was already specified |
| * by the {@code InternalGroupCreation}, the value of this {@code InternalGroupUpdate} wins. |
| * @throws OrmException if an error occurs while reading/writing from/to ReviewDb |
| * @throws OrmDuplicateKeyException 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 {@code InternalGroup} |
| */ |
| public InternalGroup createGroup( |
| ReviewDb db, InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate) |
| throws OrmException, IOException, ConfigInvalidException { |
| if (!groupsMigration.disableGroupReviewDb()) { |
| if (!groupUpdate.getUpdatedOn().isPresent()) { |
| // Set updatedOn to a specific value so that the same timestamp is used for ReviewDb and |
| // NoteDb. |
| groupUpdate = groupUpdate.toBuilder().setUpdatedOn(TimeUtil.nowTs()).build(); |
| } |
| |
| InternalGroup createdGroupInReviewDb = |
| createGroupInReviewDb(ReviewDbUtil.unwrapDb(db), groupCreation, groupUpdate); |
| |
| if (!groupsMigration.writeToNoteDb()) { |
| updateCachesOnGroupCreation(createdGroupInReviewDb); |
| return createdGroupInReviewDb; |
| } |
| } |
| |
| // TODO(aliceks): Add retry mechanism. |
| InternalGroup createdGroup = createGroupInNoteDb(groupCreation, groupUpdate); |
| updateCachesOnGroupCreation(createdGroup); |
| return createdGroup; |
| } |
| |
| /** |
| * Updates the specified group. |
| * |
| * @param db the {@code ReviewDb} instance to update |
| * @param groupUuid the UUID of the group to update |
| * @param groupUpdate an {@code InternalGroupUpdate} which indicates the desired updates on the |
| * group |
| * @throws OrmException if an error occurs while reading/writing from/to ReviewDb |
| * @throws com.google.gwtorm.server.OrmDuplicateKeyException 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(ReviewDb db, AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate) |
| throws OrmException, IOException, NoSuchGroupException, ConfigInvalidException { |
| UpdateResult result = updateGroupInDb(db, groupUuid, groupUpdate); |
| updateCachesOnGroupUpdate(result); |
| } |
| |
| @VisibleForTesting |
| public UpdateResult updateGroupInDb( |
| ReviewDb db, AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate) |
| throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException { |
| UpdateResult reviewDbUpdateResult = null; |
| if (!groupsMigration.disableGroupReviewDb()) { |
| if (!groupUpdate.getUpdatedOn().isPresent()) { |
| // Set updatedOn to a specific value so that the same timestamp is used for ReviewDb and |
| // NoteDb. |
| groupUpdate = groupUpdate.toBuilder().setUpdatedOn(TimeUtil.nowTs()).build(); |
| } |
| |
| AccountGroup group = getExistingGroupFromReviewDb(ReviewDbUtil.unwrapDb(db), groupUuid); |
| reviewDbUpdateResult = updateGroupInReviewDb(ReviewDbUtil.unwrapDb(db), group, groupUpdate); |
| |
| if (!groupsMigration.writeToNoteDb()) { |
| return reviewDbUpdateResult; |
| } |
| } |
| |
| // TODO(aliceks): Add retry mechanism. |
| Optional<UpdateResult> noteDbUpdateResult = updateGroupInNoteDb(groupUuid, groupUpdate); |
| return noteDbUpdateResult.orElse(reviewDbUpdateResult); |
| } |
| |
| private InternalGroup createGroupInReviewDb( |
| ReviewDb db, InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate) |
| throws OrmException { |
| checkIfReviewDbUpdatesAreBlocked(); |
| |
| AccountGroupName gn = new AccountGroupName(groupCreation.getNameKey(), groupCreation.getId()); |
| // first insert the group name to validate that the group name hasn't |
| // already been used to create another group |
| db.accountGroupNames().insert(ImmutableList.of(gn)); |
| |
| Timestamp createdOn = groupUpdate.getUpdatedOn().orElseGet(TimeUtil::nowTs); |
| AccountGroup group = createAccountGroup(groupCreation, createdOn); |
| UpdateResult updateResult = updateGroupInReviewDb(db, group, groupUpdate); |
| return InternalGroup.create( |
| group, |
| updateResult.getModifiedMembers(), |
| updateResult.getModifiedSubgroups(), |
| updateResult.getRefState()); |
| } |
| |
| public static AccountGroup createAccountGroup( |
| InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate) { |
| Timestamp createdOn = groupUpdate.getUpdatedOn().orElseGet(TimeUtil::nowTs); |
| AccountGroup group = createAccountGroup(groupCreation, createdOn); |
| applyUpdate(group, groupUpdate); |
| return group; |
| } |
| |
| private static AccountGroup createAccountGroup( |
| InternalGroupCreation groupCreation, Timestamp createdOn) { |
| return new AccountGroup( |
| groupCreation.getNameKey(), groupCreation.getId(), groupCreation.getGroupUUID(), createdOn); |
| } |
| |
| private static void applyUpdate(AccountGroup group, InternalGroupUpdate groupUpdate) { |
| groupUpdate.getName().ifPresent(group::setNameKey); |
| groupUpdate.getDescription().ifPresent(d -> group.setDescription(Strings.emptyToNull(d))); |
| groupUpdate.getOwnerGroupUUID().ifPresent(group::setOwnerGroupUUID); |
| groupUpdate.getVisibleToAll().ifPresent(group::setVisibleToAll); |
| } |
| |
| private UpdateResult updateGroupInReviewDb( |
| ReviewDb db, AccountGroup group, InternalGroupUpdate groupUpdate) throws OrmException { |
| checkIfReviewDbUpdatesAreBlocked(); |
| |
| AccountGroup.NameKey originalName = group.getNameKey(); |
| applyUpdate(group, groupUpdate); |
| AccountGroup.NameKey updatedName = group.getNameKey(); |
| |
| // The name must be inserted first so that we stop early for already used names. |
| updateNameInReviewDb(db, group.getId(), originalName, updatedName); |
| db.accountGroups().upsert(ImmutableList.of(group)); |
| ImmutableSet<Account.Id> modifiedMembers = |
| updateMembersInReviewDb(db, group.getId(), groupUpdate); |
| ImmutableSet<AccountGroup.UUID> modifiedSubgroups = |
| updateSubgroupsInReviewDb(db, group.getId(), groupUpdate); |
| |
| UpdateResult.Builder resultBuilder = |
| UpdateResult.builder() |
| .setGroupUuid(group.getGroupUUID()) |
| .setGroupId(group.getId()) |
| .setGroupName(group.getNameKey()) |
| .setModifiedMembers(modifiedMembers) |
| .setModifiedSubgroups(modifiedSubgroups); |
| if (!Objects.equals(originalName, updatedName)) { |
| resultBuilder.setPreviousGroupName(originalName); |
| } |
| return resultBuilder.build(); |
| } |
| |
| private static void updateNameInReviewDb( |
| ReviewDb db, |
| AccountGroup.Id groupId, |
| AccountGroup.NameKey originalName, |
| AccountGroup.NameKey updatedName) |
| throws OrmException { |
| try { |
| AccountGroupName id = new AccountGroupName(updatedName, groupId); |
| db.accountGroupNames().insert(ImmutableList.of(id)); |
| } catch (OrmException e) { |
| AccountGroupName other = db.accountGroupNames().get(updatedName); |
| if (other != null) { |
| // If we are using this identity, don't report the exception. |
| if (other.getId().equals(groupId)) { |
| return; |
| } |
| } |
| throw e; |
| } |
| db.accountGroupNames().deleteKeys(ImmutableList.of(originalName)); |
| } |
| |
| private ImmutableSet<Account.Id> updateMembersInReviewDb( |
| ReviewDb db, AccountGroup.Id groupId, InternalGroupUpdate groupUpdate) throws OrmException { |
| Timestamp updatedOn = groupUpdate.getUpdatedOn().orElseGet(TimeUtil::nowTs); |
| ImmutableSet<Account.Id> originalMembers = |
| Groups.getMembersFromReviewDb(db, groupId).collect(toImmutableSet()); |
| ImmutableSet<Account.Id> updatedMembers = |
| ImmutableSet.copyOf(groupUpdate.getMemberModification().apply(originalMembers)); |
| |
| Set<Account.Id> addedMembers = Sets.difference(updatedMembers, originalMembers); |
| if (!addedMembers.isEmpty()) { |
| addGroupMembersInReviewDb(db, groupId, addedMembers, updatedOn); |
| } |
| |
| Set<Account.Id> removedMembers = Sets.difference(originalMembers, updatedMembers); |
| if (!removedMembers.isEmpty()) { |
| removeGroupMembersInReviewDb(db, groupId, removedMembers, updatedOn); |
| } |
| |
| return Sets.union(addedMembers, removedMembers).immutableCopy(); |
| } |
| |
| private void addGroupMembersInReviewDb( |
| ReviewDb db, AccountGroup.Id groupId, Set<Account.Id> newMemberIds, Timestamp addedOn) |
| throws OrmException { |
| Set<AccountGroupMember> newMembers = |
| newMemberIds |
| .stream() |
| .map(accountId -> new AccountGroupMember.Key(accountId, groupId)) |
| .map(AccountGroupMember::new) |
| .collect(toImmutableSet()); |
| |
| if (currentUser != null) { |
| auditService.dispatchAddAccountsToGroup(currentUser.getAccountId(), newMembers, addedOn); |
| } |
| db.accountGroupMembers().insert(newMembers); |
| } |
| |
| private void removeGroupMembersInReviewDb( |
| ReviewDb db, AccountGroup.Id groupId, Set<Account.Id> accountIds, Timestamp removedOn) |
| throws OrmException { |
| Set<AccountGroupMember> membersToRemove = |
| accountIds |
| .stream() |
| .map(accountId -> new AccountGroupMember.Key(accountId, groupId)) |
| .map(AccountGroupMember::new) |
| .collect(toImmutableSet()); |
| |
| if (currentUser != null) { |
| auditService.dispatchDeleteAccountsFromGroup( |
| currentUser.getAccountId(), membersToRemove, removedOn); |
| } |
| db.accountGroupMembers().delete(membersToRemove); |
| } |
| |
| private ImmutableSet<AccountGroup.UUID> updateSubgroupsInReviewDb( |
| ReviewDb db, AccountGroup.Id groupId, InternalGroupUpdate groupUpdate) throws OrmException { |
| Timestamp updatedOn = groupUpdate.getUpdatedOn().orElseGet(TimeUtil::nowTs); |
| ImmutableSet<AccountGroup.UUID> originalSubgroups = |
| Groups.getSubgroupsFromReviewDb(db, groupId).collect(toImmutableSet()); |
| ImmutableSet<AccountGroup.UUID> updatedSubgroups = |
| ImmutableSet.copyOf(groupUpdate.getSubgroupModification().apply(originalSubgroups)); |
| |
| Set<AccountGroup.UUID> addedSubgroups = Sets.difference(updatedSubgroups, originalSubgroups); |
| if (!addedSubgroups.isEmpty()) { |
| addSubgroupsInReviewDb(db, groupId, addedSubgroups, updatedOn); |
| } |
| |
| Set<AccountGroup.UUID> removedSubgroups = Sets.difference(originalSubgroups, updatedSubgroups); |
| if (!removedSubgroups.isEmpty()) { |
| removeSubgroupsInReviewDb(db, groupId, removedSubgroups, updatedOn); |
| } |
| |
| return Sets.union(addedSubgroups, removedSubgroups).immutableCopy(); |
| } |
| |
| private void addSubgroupsInReviewDb( |
| ReviewDb db, |
| AccountGroup.Id parentGroupId, |
| Set<AccountGroup.UUID> subgroupUuids, |
| Timestamp addedOn) |
| throws OrmException { |
| Set<AccountGroupById> newSubgroups = |
| subgroupUuids |
| .stream() |
| .map(subgroupUuid -> new AccountGroupById.Key(parentGroupId, subgroupUuid)) |
| .map(AccountGroupById::new) |
| .collect(toImmutableSet()); |
| |
| if (currentUser != null) { |
| auditService.dispatchAddGroupsToGroup(currentUser.getAccountId(), newSubgroups, addedOn); |
| } |
| db.accountGroupById().insert(newSubgroups); |
| } |
| |
| private void removeSubgroupsInReviewDb( |
| ReviewDb db, |
| AccountGroup.Id parentGroupId, |
| Set<AccountGroup.UUID> subgroupUuids, |
| Timestamp removedOn) |
| throws OrmException { |
| Set<AccountGroupById> subgroupsToRemove = |
| subgroupUuids |
| .stream() |
| .map(subgroupUuid -> new AccountGroupById.Key(parentGroupId, subgroupUuid)) |
| .map(AccountGroupById::new) |
| .collect(toImmutableSet()); |
| |
| if (currentUser != null) { |
| auditService.dispatchDeleteGroupsFromGroup( |
| currentUser.getAccountId(), subgroupsToRemove, removedOn); |
| } |
| db.accountGroupById().delete(subgroupsToRemove); |
| } |
| |
| private InternalGroup createGroupInNoteDb( |
| InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate) |
| throws IOException, ConfigInvalidException, OrmException { |
| try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) { |
| AccountGroup.NameKey groupName = groupUpdate.getName().orElseGet(groupCreation::getNameKey); |
| GroupNameNotes groupNameNotes = |
| GroupNameNotes.loadForNewGroup(allUsersRepo, groupCreation.getGroupUUID(), groupName); |
| |
| GroupConfig groupConfig = GroupConfig.createForNewGroup(allUsersRepo, groupCreation); |
| groupConfig.setGroupUpdate(groupUpdate, this::getAccountNameEmail, this::getGroupName); |
| |
| commit(allUsersRepo, groupConfig, groupNameNotes); |
| |
| return groupConfig |
| .getLoadedGroup() |
| .orElseThrow( |
| () -> new IllegalStateException("Created group wasn't automatically loaded")); |
| } |
| } |
| |
| private Optional<UpdateResult> updateGroupInNoteDb( |
| AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate) |
| throws IOException, ConfigInvalidException, OrmDuplicateKeyException, NoSuchGroupException { |
| try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) { |
| GroupConfig groupConfig = GroupConfig.loadForGroup(allUsersRepo, groupUuid); |
| groupConfig.setGroupUpdate(groupUpdate, this::getAccountNameEmail, this::getGroupName); |
| if (!groupConfig.getLoadedGroup().isPresent()) { |
| if (groupsMigration.readFromNoteDb()) { |
| throw new NoSuchGroupException(groupUuid); |
| } |
| return Optional.empty(); |
| } |
| |
| InternalGroup originalGroup = groupConfig.getLoadedGroup().get(); |
| |
| GroupNameNotes groupNameNotes = null; |
| if (groupUpdate.getName().isPresent()) { |
| AccountGroup.NameKey oldName = originalGroup.getNameKey(); |
| AccountGroup.NameKey newName = groupUpdate.getName().get(); |
| groupNameNotes = GroupNameNotes.loadForRename(allUsersRepo, groupUuid, oldName, newName); |
| } |
| |
| commit(allUsersRepo, groupConfig, groupNameNotes); |
| |
| InternalGroup updatedGroup = |
| groupConfig |
| .getLoadedGroup() |
| .orElseThrow( |
| () -> new IllegalStateException("Updated group wasn't automatically loaded")); |
| return Optional.of(getUpdateResult(originalGroup, updatedGroup)); |
| } |
| } |
| |
| private static UpdateResult getUpdateResult( |
| InternalGroup originalGroup, InternalGroup updatedGroup) { |
| Set<Account.Id> modifiedMembers = |
| Sets.symmetricDifference(originalGroup.getMembers(), updatedGroup.getMembers()); |
| Set<AccountGroup.UUID> modifiedSubgroups = |
| Sets.symmetricDifference(originalGroup.getSubgroups(), updatedGroup.getSubgroups()); |
| |
| UpdateResult.Builder resultBuilder = |
| UpdateResult.builder() |
| .setGroupUuid(updatedGroup.getGroupUUID()) |
| .setGroupId(updatedGroup.getId()) |
| .setGroupName(updatedGroup.getNameKey()) |
| .setModifiedMembers(modifiedMembers) |
| .setModifiedSubgroups(modifiedSubgroups) |
| .setRefState(updatedGroup.getRefState()); |
| if (!Objects.equals(originalGroup.getNameKey(), updatedGroup.getNameKey())) { |
| resultBuilder.setPreviousGroupName(originalGroup.getNameKey()); |
| } |
| return resultBuilder.build(); |
| } |
| |
| static String getAccountName(AccountCache accountCache, Account.Id accountId) { |
| AccountState accountState = accountCache.getOrNull(accountId); |
| return Optional.ofNullable(accountState) |
| .map(AccountState::getAccount) |
| .map(account -> account.getName()) |
| // Historically, the database did not enforce relational integrity, so it is |
| // possible for groups to have non-existing members. |
| .orElse("No Account for Id #" + accountId); |
| } |
| |
| static String getAccountNameEmail( |
| AccountCache accountCache, Account.Id accountId, String serverId) { |
| String accountName = getAccountName(accountCache, accountId); |
| return formatNameEmail(accountName, getEmailForAuditLog(accountId, serverId)); |
| } |
| |
| static String getEmailForAuditLog(Account.Id accountId, String serverId) { |
| return accountId.get() + "@" + serverId; |
| } |
| |
| private String getAccountNameEmail(Account.Id accountId) { |
| return getAccountNameEmail(accountCache, accountId, serverId); |
| } |
| |
| static String getGroupName(GroupBackend groupBackend, AccountGroup.UUID groupUuid) { |
| String uuid = groupUuid.get(); |
| GroupDescription.Basic desc = groupBackend.get(groupUuid); |
| String name = desc != null ? desc.getName() : uuid; |
| return formatNameEmail(name, uuid); |
| } |
| |
| private String getGroupName(AccountGroup.UUID groupUuid) { |
| return getGroupName(groupBackend, groupUuid); |
| } |
| |
| private static String formatNameEmail(String name, String email) { |
| StringBuilder formattedResult = new StringBuilder(); |
| PersonIdent.appendSanitized(formattedResult, name); |
| formattedResult.append(" <"); |
| PersonIdent.appendSanitized(formattedResult, email); |
| formattedResult.append(">"); |
| return formattedResult.toString(); |
| } |
| |
| 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 != null ? currentUser.getAccount() : null); |
| } |
| |
| private void updateCachesOnGroupCreation(InternalGroup createdGroup) throws IOException { |
| groupCache.onCreateGroup(createdGroup.getGroupUUID()); |
| for (Account.Id modifiedMember : createdGroup.getMembers()) { |
| groupIncludeCache.evictGroupsWithMember(modifiedMember); |
| } |
| for (AccountGroup.UUID modifiedSubgroup : createdGroup.getSubgroups()) { |
| groupIncludeCache.evictParentGroupsOf(modifiedSubgroup); |
| } |
| } |
| |
| private void updateCachesOnGroupUpdate(UpdateResult result) throws IOException { |
| if (result.getPreviousGroupName().isPresent()) { |
| AccountGroup.NameKey previousName = result.getPreviousGroupName().get(); |
| groupCache.evictAfterRename(previousName); |
| |
| // TODO(aliceks): After switching to NoteDb, consider to use a BatchRefUpdate. |
| @SuppressWarnings("unused") |
| Future<?> possiblyIgnoredError = |
| renameGroupOpFactory |
| .create( |
| authorIdent, |
| result.getGroupUuid(), |
| previousName.get(), |
| result.getGroupName().get()) |
| .start(0, TimeUnit.MILLISECONDS); |
| } |
| groupCache.evict(result.getGroupUuid(), result.getGroupId(), result.getGroupName()); |
| for (Account.Id modifiedMember : result.getModifiedMembers()) { |
| groupIncludeCache.evictGroupsWithMember(modifiedMember); |
| } |
| for (AccountGroup.UUID modifiedSubgroup : result.getModifiedSubgroups()) { |
| groupIncludeCache.evictParentGroupsOf(modifiedSubgroup); |
| } |
| } |
| |
| private void checkIfReviewDbUpdatesAreBlocked() throws OrmException { |
| if (reviewDbUpdatesAreBlocked) { |
| throw new OrmException("Updates to groups in ReviewDb are blocked"); |
| } |
| } |
| |
| @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<AccountGroup.NameKey> getPreviousGroupName(); |
| |
| abstract ImmutableSet<Account.Id> getModifiedMembers(); |
| |
| abstract ImmutableSet<AccountGroup.UUID> getModifiedSubgroups(); |
| |
| @Nullable |
| public abstract ObjectId getRefState(); |
| |
| 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 setModifiedMembers(Set<Account.Id> modifiedMembers); |
| |
| abstract Builder setModifiedSubgroups(Set<AccountGroup.UUID> modifiedSubgroups); |
| |
| public abstract Builder setRefState(ObjectId refState); |
| |
| abstract UpdateResult build(); |
| } |
| } |
| } |