blob: ad475b32d37a252a789bb276f63da1ed80a6056c [file] [log] [blame]
// 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);
}
}