| // 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; |
| |
| import static com.google.common.collect.ImmutableSet.toImmutableSet; |
| import static com.google.gerrit.server.group.Groups.getExistingGroupFromReviewDb; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.gerrit.audit.AuditService; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.common.errors.NameAlreadyUsedException; |
| 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.server.ReviewDb; |
| import com.google.gerrit.server.GerritPersonIdent; |
| import com.google.gerrit.server.IdentifiedUser; |
| import com.google.gerrit.server.account.GroupCache; |
| import com.google.gerrit.server.account.GroupIncludeCache; |
| import com.google.gerrit.server.git.RenameGroupOp; |
| import com.google.gwtorm.server.OrmException; |
| import com.google.inject.Inject; |
| import com.google.inject.assistedinject.Assisted; |
| import java.io.IOException; |
| import java.util.HashSet; |
| import java.util.Set; |
| import java.util.concurrent.Future; |
| import java.util.concurrent.TimeUnit; |
| import java.util.function.Consumer; |
| import org.eclipse.jgit.lib.PersonIdent; |
| |
| /** |
| * 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 Groups groups; |
| private final GroupCache groupCache; |
| private final GroupIncludeCache groupIncludeCache; |
| private final AuditService auditService; |
| private final RenameGroupOp.Factory renameGroupOpFactory; |
| @Nullable private final IdentifiedUser currentUser; |
| private final PersonIdent committerIdent; |
| |
| @Inject |
| GroupsUpdate( |
| Groups groups, |
| GroupCache groupCache, |
| GroupIncludeCache groupIncludeCache, |
| AuditService auditService, |
| RenameGroupOp.Factory renameGroupOpFactory, |
| @GerritPersonIdent PersonIdent serverIdent, |
| @Assisted @Nullable IdentifiedUser currentUser) { |
| this.groups = groups; |
| this.groupCache = groupCache; |
| this.groupIncludeCache = groupIncludeCache; |
| this.auditService = auditService; |
| this.renameGroupOpFactory = renameGroupOpFactory; |
| this.currentUser = currentUser; |
| committerIdent = getCommitterIdent(serverIdent, currentUser); |
| } |
| |
| private static PersonIdent getCommitterIdent( |
| 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()); |
| } |
| |
| /** |
| * Adds/Creates the specified group for the specified members (accounts). |
| * |
| * @param db the {@code ReviewDb} instance to update |
| * @param group the group to add |
| * @param memberIds the IDs of the accounts which should be members of the created group |
| * @throws OrmException if an error occurs while reading/writing from/to ReviewDb |
| * @throws IOException if the cache entry of one of the new members couldn't be invalidated, or |
| * the new group couldn't be indexed |
| */ |
| public void addGroup(ReviewDb db, AccountGroup group, Set<Account.Id> memberIds) |
| throws OrmException, IOException { |
| addNewGroup(db, group); |
| addNewGroupMembers(db, group, memberIds); |
| groupCache.onCreateGroup(group); |
| } |
| |
| /** |
| * Adds the specified group. |
| * |
| * <p><strong>Note</strong>: This method doesn't update the index! It just adds the group to the |
| * database. Use this method with care. |
| * |
| * @param db the {@code ReviewDb} instance to update |
| * @param group the group to add |
| * @throws OrmException if an error occurs while reading/writing from/to ReviewDb |
| */ |
| public static void addNewGroup(ReviewDb db, AccountGroup group) throws OrmException { |
| AccountGroupName gn = new AccountGroupName(group); |
| // 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)); |
| db.accountGroups().insert(ImmutableList.of(group)); |
| } |
| |
| /** |
| * Updates the specified group. |
| * |
| * @param db the {@code ReviewDb} instance to update |
| * @param groupUuid the UUID of the group to update |
| * @param groupConsumer a {@code Consumer} which performs the desired updates on the group |
| * @throws OrmException if an error occurs while reading/writing from/to ReviewDb |
| * @throws IOException if the cache entry for the group couldn't be invalidated |
| * @throws NoSuchGroupException if the specified group doesn't exist |
| */ |
| public void updateGroup( |
| ReviewDb db, AccountGroup.UUID groupUuid, Consumer<AccountGroup> groupConsumer) |
| throws OrmException, IOException, NoSuchGroupException { |
| AccountGroup updatedGroup = updateGroupInDb(db, groupUuid, groupConsumer); |
| groupCache.evict(updatedGroup.getGroupUUID(), updatedGroup.getId(), updatedGroup.getNameKey()); |
| } |
| |
| @VisibleForTesting |
| public AccountGroup updateGroupInDb( |
| ReviewDb db, AccountGroup.UUID groupUuid, Consumer<AccountGroup> groupConsumer) |
| throws OrmException, NoSuchGroupException { |
| AccountGroup group = getExistingGroupFromReviewDb(db, groupUuid); |
| groupConsumer.accept(group); |
| db.accountGroups().update(ImmutableList.of(group)); |
| return group; |
| } |
| |
| /** |
| * Renames the specified group. |
| * |
| * @param db the {@code ReviewDb} instance to update |
| * @param groupUuid the UUID of the group to rename |
| * @param newName the new name of the group |
| * @throws OrmException if an error occurs while reading/writing from/to ReviewDb |
| * @throws IOException if the cache entry for the group couldn't be invalidated |
| * @throws NoSuchGroupException if the specified group doesn't exist |
| * @throws NameAlreadyUsedException if another group has the name {@code newName} |
| */ |
| public void renameGroup(ReviewDb db, AccountGroup.UUID groupUuid, AccountGroup.NameKey newName) |
| throws OrmException, IOException, NameAlreadyUsedException, NoSuchGroupException { |
| AccountGroup group = getExistingGroupFromReviewDb(db, groupUuid); |
| AccountGroup.NameKey oldName = group.getNameKey(); |
| |
| try { |
| AccountGroupName id = new AccountGroupName(newName, group.getId()); |
| db.accountGroupNames().insert(ImmutableList.of(id)); |
| } catch (OrmException e) { |
| AccountGroupName other = db.accountGroupNames().get(newName); |
| if (other != null) { |
| // If we are using this identity, don't report the exception. |
| if (other.getId().equals(group.getId())) { |
| return; |
| } |
| |
| // Otherwise, someone else has this identity. |
| throw new NameAlreadyUsedException("group with name " + newName + " already exists"); |
| } |
| throw e; |
| } |
| |
| group.setNameKey(newName); |
| db.accountGroups().update(ImmutableList.of(group)); |
| |
| db.accountGroupNames().deleteKeys(ImmutableList.of(oldName)); |
| |
| groupCache.evictAfterRename(oldName); |
| groupCache.evict(group.getGroupUUID(), group.getId(), group.getNameKey()); |
| |
| @SuppressWarnings("unused") |
| Future<?> possiblyIgnoredError = |
| renameGroupOpFactory |
| .create(committerIdent, groupUuid, oldName.get(), newName.get()) |
| .start(0, TimeUnit.MILLISECONDS); |
| } |
| |
| /** |
| * Adds an account as member to a group. The account is only added as a new member if it isn't |
| * already a member of the group. |
| * |
| * <p><strong>Note</strong>: This method doesn't check whether the account exists! |
| * |
| * @param db the {@code ReviewDb} instance to update |
| * @param groupUuid the UUID of the group |
| * @param accountId the ID of the account to add |
| * @throws OrmException if an error occurs while reading/writing from/to ReviewDb |
| * @throws IOException if the cache entry of the new member couldn't be invalidated |
| * @throws NoSuchGroupException if the specified group doesn't exist |
| */ |
| public void addGroupMember(ReviewDb db, AccountGroup.UUID groupUuid, Account.Id accountId) |
| throws OrmException, IOException, NoSuchGroupException { |
| addGroupMembers(db, groupUuid, ImmutableSet.of(accountId)); |
| } |
| |
| /** |
| * Adds several accounts as members to a group. Only accounts which currently aren't members of |
| * the group are added. |
| * |
| * <p><strong>Note</strong>: This method doesn't check whether the accounts exist! |
| * |
| * @param db the {@code ReviewDb} instance to update |
| * @param groupUuid the UUID of the group |
| * @param accountIds a set of IDs of accounts to add |
| * @throws OrmException if an error occurs while reading/writing from/to ReviewDb |
| * @throws IOException if the group or one of the new members couldn't be indexed |
| * @throws NoSuchGroupException if the specified group doesn't exist |
| */ |
| public void addGroupMembers(ReviewDb db, AccountGroup.UUID groupUuid, Set<Account.Id> accountIds) |
| throws OrmException, IOException, NoSuchGroupException { |
| AccountGroup group = getExistingGroupFromReviewDb(db, groupUuid); |
| Set<Account.Id> newMemberIds = new HashSet<>(); |
| for (Account.Id accountId : accountIds) { |
| boolean isMember = groups.isMember(db, groupUuid, accountId); |
| if (!isMember) { |
| newMemberIds.add(accountId); |
| } |
| } |
| |
| if (newMemberIds.isEmpty()) { |
| return; |
| } |
| |
| addNewGroupMembers(db, group, newMemberIds); |
| } |
| |
| private void addNewGroupMembers(ReviewDb db, AccountGroup group, Set<Account.Id> newMemberIds) |
| throws OrmException, IOException { |
| Set<AccountGroupMember> newMembers = |
| newMemberIds.stream() |
| .map(accountId -> new AccountGroupMember.Key(accountId, group.getId())) |
| .map(AccountGroupMember::new) |
| .collect(toImmutableSet()); |
| |
| if (currentUser != null) { |
| auditService.dispatchAddAccountsToGroup(currentUser.getAccountId(), newMembers); |
| } |
| db.accountGroupMembers().insert(newMembers); |
| groupCache.evict(group.getGroupUUID(), group.getId(), group.getNameKey()); |
| for (AccountGroupMember newMember : newMembers) { |
| groupIncludeCache.evictGroupsWithMember(newMember.getAccountId()); |
| } |
| } |
| |
| /** |
| * Removes several members (accounts) from a group. Only accounts which currently are members of |
| * the group are removed. |
| * |
| * @param db the {@code ReviewDb} instance to update |
| * @param groupUuid the UUID of the group |
| * @param accountIds a set of IDs of accounts to remove |
| * @throws OrmException if an error occurs while reading/writing from/to ReviewDb |
| * @throws IOException if the group or one of the removed members couldn't be indexed |
| * @throws NoSuchGroupException if the specified group doesn't exist |
| */ |
| public void removeGroupMembers( |
| ReviewDb db, AccountGroup.UUID groupUuid, Set<Account.Id> accountIds) |
| throws OrmException, IOException, NoSuchGroupException { |
| AccountGroup group = getExistingGroupFromReviewDb(db, groupUuid); |
| AccountGroup.Id groupId = group.getId(); |
| Set<AccountGroupMember> membersToRemove = new HashSet<>(); |
| for (Account.Id accountId : accountIds) { |
| boolean isMember = groups.isMember(db, groupUuid, accountId); |
| if (isMember) { |
| AccountGroupMember.Key key = new AccountGroupMember.Key(accountId, groupId); |
| membersToRemove.add(new AccountGroupMember(key)); |
| } |
| } |
| |
| if (membersToRemove.isEmpty()) { |
| return; |
| } |
| |
| if (currentUser != null) { |
| auditService.dispatchDeleteAccountsFromGroup(currentUser.getAccountId(), membersToRemove); |
| } |
| db.accountGroupMembers().delete(membersToRemove); |
| groupCache.evict(group.getGroupUUID(), group.getId(), group.getNameKey()); |
| for (AccountGroupMember member : membersToRemove) { |
| groupIncludeCache.evictGroupsWithMember(member.getAccountId()); |
| } |
| } |
| |
| /** |
| * Adds several groups as subgroups to a group. Only groups which currently aren't subgroups of |
| * the group are added. |
| * |
| * <p>The parent group must be an internal group whereas the subgroups can either be internal or |
| * external groups. |
| * |
| * <p><strong>Note</strong>: This method doesn't check whether the subgroups exist! |
| * |
| * @param db the {@code ReviewDb} instance to update |
| * @param parentGroupUuid the UUID of the parent group |
| * @param subgroupUuids a set of IDs of the groups to add as subgroups |
| * @throws OrmException if an error occurs while reading/writing from/to ReviewDb |
| * @throws IOException if the parent group couldn't be indexed |
| * @throws NoSuchGroupException if the specified parent group doesn't exist |
| */ |
| public void addSubgroups( |
| ReviewDb db, AccountGroup.UUID parentGroupUuid, Set<AccountGroup.UUID> subgroupUuids) |
| throws OrmException, NoSuchGroupException, IOException { |
| AccountGroup parentGroup = getExistingGroupFromReviewDb(db, parentGroupUuid); |
| AccountGroup.Id parentGroupId = parentGroup.getId(); |
| Set<AccountGroupById> newSubgroups = new HashSet<>(); |
| for (AccountGroup.UUID includedGroupUuid : subgroupUuids) { |
| boolean isSubgroup = groups.isSubgroup(db, parentGroupUuid, includedGroupUuid); |
| if (!isSubgroup) { |
| AccountGroupById.Key key = new AccountGroupById.Key(parentGroupId, includedGroupUuid); |
| newSubgroups.add(new AccountGroupById(key)); |
| } |
| } |
| |
| if (newSubgroups.isEmpty()) { |
| return; |
| } |
| |
| if (currentUser != null) { |
| auditService.dispatchAddGroupsToGroup(currentUser.getAccountId(), newSubgroups); |
| } |
| db.accountGroupById().insert(newSubgroups); |
| groupCache.evict(parentGroup.getGroupUUID(), parentGroup.getId(), parentGroup.getNameKey()); |
| for (AccountGroupById newIncludedGroup : newSubgroups) { |
| groupIncludeCache.evictParentGroupsOf(newIncludedGroup.getIncludeUUID()); |
| } |
| groupIncludeCache.evictSubgroupsOf(parentGroupUuid); |
| } |
| |
| /** |
| * Removes several subgroups from a parent group. Only groups which currently are subgroups of the |
| * group are removed. |
| * |
| * <p>The parent group must be an internal group whereas the subgroups can either be internal or |
| * external groups. |
| * |
| * @param db the {@code ReviewDb} instance to update |
| * @param parentGroupUuid the UUID of the parent group |
| * @param subgroupUuids a set of IDs of the subgroups to remove from the parent group |
| * @throws OrmException if an error occurs while reading/writing from/to ReviewDb |
| * @throws IOException if the parent group couldn't be indexed |
| * @throws NoSuchGroupException if the specified parent group doesn't exist |
| */ |
| public void removeSubgroups( |
| ReviewDb db, AccountGroup.UUID parentGroupUuid, Set<AccountGroup.UUID> subgroupUuids) |
| throws OrmException, NoSuchGroupException, IOException { |
| AccountGroup parentGroup = getExistingGroupFromReviewDb(db, parentGroupUuid); |
| AccountGroup.Id parentGroupId = parentGroup.getId(); |
| Set<AccountGroupById> subgroupsToRemove = new HashSet<>(); |
| for (AccountGroup.UUID subgroupUuid : subgroupUuids) { |
| boolean isSubgroup = groups.isSubgroup(db, parentGroupUuid, subgroupUuid); |
| if (isSubgroup) { |
| AccountGroupById.Key key = new AccountGroupById.Key(parentGroupId, subgroupUuid); |
| subgroupsToRemove.add(new AccountGroupById(key)); |
| } |
| } |
| |
| if (subgroupsToRemove.isEmpty()) { |
| return; |
| } |
| |
| if (currentUser != null) { |
| auditService.dispatchDeleteGroupsFromGroup(currentUser.getAccountId(), subgroupsToRemove); |
| } |
| db.accountGroupById().delete(subgroupsToRemove); |
| groupCache.evict(parentGroup.getGroupUUID(), parentGroup.getId(), parentGroup.getNameKey()); |
| for (AccountGroupById groupToRemove : subgroupsToRemove) { |
| groupIncludeCache.evictParentGroupsOf(groupToRemove.getIncludeUUID()); |
| } |
| groupIncludeCache.evictSubgroupsOf(parentGroupUuid); |
| } |
| } |