Files
gerrit/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
Dave Borowitz f80d44ccff Implement group audit log in NoteDb
In the GetAuditLog endpoint, AccountGroupMemberAudit logs are
read and processed first followed by AccountGroupByIdAud logs.
In the end, logs are ordered from new -> old.

Currently, we only return logs created by real users and logs
created by server are skipped. The creator of a log is decided
by the Author of the commit. We may revisit this behavior after
groups are fully migrated to NoteDb.

Change-Id: I08e335931cefa0e99a82917e1902794980cc3d07
2017-11-17 11:59:30 -05:00

1266 lines
47 KiB
Java

// Copyright (C) 2015 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.acceptance.api.group;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.acceptance.GitUtil.deleteRef;
import static com.google.gerrit.acceptance.GitUtil.fetch;
import static com.google.gerrit.acceptance.api.group.GroupAssert.assertGroupInfo;
import static com.google.gerrit.acceptance.rest.account.AccountAssert.assertAccountInfos;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.server.notedb.NoteDbTable.GROUPS;
import static com.google.gerrit.server.notedb.NotesMigration.READ;
import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
import static com.google.gerrit.server.notedb.NotesMigration.WRITE;
import static java.util.stream.Collectors.toList;
import com.google.common.base.Throwables;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.GerritConfig;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.Sandboxed;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.data.PermissionRule.Action;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.groups.GroupApi;
import com.google.gerrit.extensions.api.groups.GroupInput;
import com.google.gerrit.extensions.api.groups.Groups.ListRequest;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.GroupAuditEventInfo;
import com.google.gerrit.extensions.common.GroupAuditEventInfo.GroupMemberAuditEventInfo;
import com.google.gerrit.extensions.common.GroupAuditEventInfo.Type;
import com.google.gerrit.extensions.common.GroupAuditEventInfo.UserMemberAuditEventInfo;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.common.GroupOptionsInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupIncludeCache;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.group.db.GroupConfig;
import com.google.gerrit.server.group.db.Groups;
import com.google.gerrit.server.index.group.GroupIndexer;
import com.google.gerrit.server.index.group.StalenessChecker;
import com.google.gerrit.server.util.MagicBranch;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import java.io.IOException;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.junit.Test;
@NoHttpd
public class GroupsIT extends AbstractDaemonTest {
@ConfigSuite.Config
public static Config noteDbConfig() {
Config config = new Config();
config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), WRITE, true);
config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), READ, true);
return config;
}
@Inject private Groups groups;
@Inject private GroupIncludeCache groupIncludeCache;
@Inject private GroupBackend groupBackend;
@Inject private StalenessChecker stalenessChecker;
@Inject private GroupIndexer groupIndexer;
@Inject
@Named("groups_byuuid")
private LoadingCache<String, Optional<InternalGroup>> groupsByUUIDCache;
@Test
public void systemGroupCanBeRetrievedFromIndex() throws Exception {
List<GroupInfo> groupInfos = gApi.groups().query("name:Administrators").get();
assertThat(groupInfos).isNotEmpty();
}
@Test
public void addToNonExistingGroup_NotFound() throws Exception {
exception.expect(ResourceNotFoundException.class);
gApi.groups().id("non-existing").addMembers("admin");
}
@Test
public void removeFromNonExistingGroup_NotFound() throws Exception {
exception.expect(ResourceNotFoundException.class);
gApi.groups().id("non-existing").removeMembers("admin");
}
@Test
public void addRemoveMember() throws Exception {
String g = createGroup("users");
gApi.groups().id(g).addMembers("user");
assertMembers(g, user);
gApi.groups().id(g).removeMembers("user");
assertNoMembers(g);
}
@Test
public void cachedGroupsForMemberAreUpdatedOnMemberAdditionAndRemoval() throws Exception {
// Fill the cache for the observed account.
groupIncludeCache.getGroupsWithMember(user.getId());
String groupName = createGroup("users");
AccountGroup.UUID groupUuid = new AccountGroup.UUID(gApi.groups().id(groupName).get().id);
gApi.groups().id(groupName).addMembers(user.fullName);
Collection<AccountGroup.UUID> groupsWithMemberAfterAddition =
groupIncludeCache.getGroupsWithMember(user.getId());
assertThat(groupsWithMemberAfterAddition).contains(groupUuid);
gApi.groups().id(groupName).removeMembers(user.fullName);
Collection<AccountGroup.UUID> groupsWithMemberAfterRemoval =
groupIncludeCache.getGroupsWithMember(user.getId());
assertThat(groupsWithMemberAfterRemoval).doesNotContain(groupUuid);
}
@Test
public void addExistingMember_OK() throws Exception {
String g = "Administrators";
assertMembers(g, admin);
gApi.groups().id("Administrators").addMembers("admin");
assertMembers(g, admin);
}
@Test
public void addNonExistingMember_UnprocessableEntity() throws Exception {
exception.expect(UnprocessableEntityException.class);
gApi.groups().id("Administrators").addMembers("non-existing");
}
@Test
public void addMultipleMembers() throws Exception {
String g = createGroup("users");
TestAccount u1 = accountCreator.create("u1", "u1@example.com", "Full Name 1");
TestAccount u2 = accountCreator.create("u2", "u2@example.com", "Full Name 2");
gApi.groups().id(g).addMembers(u1.username, u2.username);
assertMembers(g, u1, u2);
}
@Test
public void addMembersWithAtSign() throws Exception {
String g = createGroup("users");
TestAccount u10 = accountCreator.create("u10", "u10@example.com", "Full Name 10");
TestAccount u11_at =
accountCreator.create("u11@something", "u11@example.com", "Full Name 11 With At");
accountCreator.create("u11", "u11.another@example.com", "Full Name 11 Without At");
gApi.groups().id(g).addMembers(u10.username, u11_at.username);
assertMembers(g, u10, u11_at);
}
@Test
public void includeRemoveGroup() throws Exception {
String p = createGroup("parent");
String g = createGroup("newGroup");
gApi.groups().id(p).addGroups(g);
assertIncludes(p, g);
gApi.groups().id(p).removeGroups(g);
assertNoIncludes(p);
}
@Test
public void includeExistingGroup_OK() throws Exception {
String p = createGroup("parent");
String g = createGroup("newGroup");
gApi.groups().id(p).addGroups(g);
assertIncludes(p, g);
gApi.groups().id(p).addGroups(g);
assertIncludes(p, g);
}
@Test
public void addMultipleIncludes() throws Exception {
String p = createGroup("parent");
String g1 = createGroup("newGroup1");
String g2 = createGroup("newGroup2");
List<String> groups = new ArrayList<>();
groups.add(g1);
groups.add(g2);
gApi.groups().id(p).addGroups(g1, g2);
assertIncludes(p, g1, g2);
}
@Test
public void createGroup() throws Exception {
String newGroupName = name("newGroup");
GroupInfo g = gApi.groups().create(newGroupName).get();
assertGroupInfo(getFromCache(newGroupName), g);
}
@Test
public void createDuplicateInternalGroupCaseSensitiveName_Conflict() throws Exception {
String dupGroupName = name("dupGroup");
gApi.groups().create(dupGroupName);
exception.expect(ResourceConflictException.class);
exception.expectMessage("group '" + dupGroupName + "' already exists");
gApi.groups().create(dupGroupName);
}
@Test
public void createDuplicateInternalGroupCaseInsensitiveName() throws Exception {
String dupGroupName = name("dupGroupA");
String dupGroupNameLowerCase = name("dupGroupA").toLowerCase();
gApi.groups().create(dupGroupName);
gApi.groups().create(dupGroupNameLowerCase);
assertThat(gApi.groups().list().getAsMap().keySet()).contains(dupGroupName);
assertThat(gApi.groups().list().getAsMap().keySet()).contains(dupGroupNameLowerCase);
}
@Test
public void createDuplicateSystemGroupCaseSensitiveName_Conflict() throws Exception {
String newGroupName = "Registered Users";
exception.expect(ResourceConflictException.class);
exception.expectMessage("group 'Registered Users' already exists");
gApi.groups().create(newGroupName);
}
@Test
public void createDuplicateSystemGroupCaseInsensitiveName_Conflict() throws Exception {
String newGroupName = "registered users";
exception.expect(ResourceConflictException.class);
exception.expectMessage("group 'Registered Users' already exists");
gApi.groups().create(newGroupName);
}
@Test
@GerritConfig(name = "groups.global:Anonymous-Users.name", value = "All Users")
public void createGroupWithConfiguredNameOfSystemGroup_Conflict() throws Exception {
exception.expect(ResourceConflictException.class);
exception.expectMessage("group 'All Users' already exists");
gApi.groups().create("all users");
}
@Test
@GerritConfig(name = "groups.global:Anonymous-Users.name", value = "All Users")
public void createGroupWithDefaultNameOfSystemGroup_Conflict() throws Exception {
exception.expect(ResourceConflictException.class);
exception.expectMessage("group name 'Anonymous Users' is reserved");
gApi.groups().create("anonymous users");
}
@Test
public void createGroupWithProperties() throws Exception {
GroupInput in = new GroupInput();
in.name = name("newGroup");
in.description = "Test description";
in.visibleToAll = true;
in.ownerId = getFromCache("Administrators").getGroupUUID().get();
GroupInfo g = gApi.groups().create(in).detail();
assertThat(g.description).isEqualTo(in.description);
assertThat(g.options.visibleToAll).isEqualTo(in.visibleToAll);
assertThat(g.ownerId).isEqualTo(in.ownerId);
}
@Test
public void createGroupWithoutCapability_Forbidden() throws Exception {
setApiUser(user);
exception.expect(AuthException.class);
gApi.groups().create(name("newGroup"));
}
@Test
public void createdOnFieldIsPopulatedForNewGroup() throws Exception {
// NoteDb allows only second precision.
Timestamp testStartTime = Timestamp.from(Instant.now().truncatedTo(ChronoUnit.SECONDS));
String newGroupName = name("newGroup");
GroupInfo group = gApi.groups().create(newGroupName).get();
assertThat(group.createdOn).isAtLeast(testStartTime);
}
@Test
public void getGroup() throws Exception {
InternalGroup adminGroup = getFromCache("Administrators");
testGetGroup(adminGroup.getGroupUUID().get(), adminGroup);
testGetGroup(adminGroup.getName(), adminGroup);
testGetGroup(adminGroup.getId().get(), adminGroup);
}
private void testGetGroup(Object id, InternalGroup expectedGroup) throws Exception {
GroupInfo group = gApi.groups().id(id.toString()).get();
assertGroupInfo(expectedGroup, group);
}
@Test
@GerritConfig(name = "groups.global:Anonymous-Users.name", value = "All Users")
public void getSystemGroupByConfiguredName() throws Exception {
GroupReference anonymousUsersGroup = systemGroupBackend.getGroup(ANONYMOUS_USERS);
assertThat(anonymousUsersGroup.getName()).isEqualTo("All Users");
GroupInfo group = gApi.groups().id(anonymousUsersGroup.getUUID().get()).get();
assertThat(group.name).isEqualTo(anonymousUsersGroup.getName());
group = gApi.groups().id(anonymousUsersGroup.getName()).get();
assertThat(group.id).isEqualTo(Url.encode((anonymousUsersGroup.getUUID().get())));
}
@Test
public void getSystemGroupByDefaultName() throws Exception {
GroupReference anonymousUsersGroup = systemGroupBackend.getGroup(ANONYMOUS_USERS);
GroupInfo group = gApi.groups().id("Anonymous Users").get();
assertThat(group.name).isEqualTo(anonymousUsersGroup.getName());
assertThat(group.id).isEqualTo(Url.encode((anonymousUsersGroup.getUUID().get())));
}
@Test
@GerritConfig(name = "groups.global:Anonymous-Users.name", value = "All Users")
public void getSystemGroupByDefaultName_NotFound() throws Exception {
exception.expect(ResourceNotFoundException.class);
gApi.groups().id("Anonymous-Users").get();
}
@Test
public void groupIsCreatedForSpecifiedName() throws Exception {
String name = name("Users");
gApi.groups().create(name);
assertThat(gApi.groups().id(name).name()).isEqualTo(name);
}
@Test
public void groupCannotBeCreatedWithNameOfAnotherGroup() throws Exception {
String name = name("Users");
gApi.groups().create(name).get();
exception.expect(ResourceConflictException.class);
gApi.groups().create(name);
}
@Test
public void groupCanBeRenamed() throws Exception {
String name = name("Name1");
GroupInfo group = gApi.groups().create(name).get();
String newName = name("Name2");
gApi.groups().id(name).name(newName);
assertThat(gApi.groups().id(group.id).name()).isEqualTo(newName);
}
@Test
public void groupCanBeRenamedToItsCurrentName() throws Exception {
String name = name("Users");
GroupInfo group = gApi.groups().create(name).get();
gApi.groups().id(group.id).name(name);
assertThat(gApi.groups().id(group.id).name()).isEqualTo(name);
}
@Test
public void groupCannotBeRenamedToNameOfAnotherGroup() throws Exception {
String name1 = name("Name1");
GroupInfo group1 = gApi.groups().create(name1).get();
String name2 = name("Name2");
gApi.groups().create(name2);
exception.expect(ResourceConflictException.class);
gApi.groups().id(group1.id).name(name2);
}
@Test
public void renamedGroupCanBeLookedUpByNewName() throws Exception {
String name = name("Name1");
GroupInfo group = gApi.groups().create(name).get();
String newName = name("Name2");
gApi.groups().id(group.id).name(newName);
GroupInfo foundGroup = gApi.groups().id(newName).get();
assertThat(foundGroup.id).isEqualTo(group.id);
}
@Test
public void oldNameOfRenamedGroupIsNotAccessibleAnymore() throws Exception {
String name = name("Name1");
GroupInfo group = gApi.groups().create(name).get();
String newName = name("Name2");
gApi.groups().id(group.id).name(newName);
assertThat(getFromCache(name)).isNull();
exception.expect(ResourceNotFoundException.class);
gApi.groups().id(name).get();
}
@Test
public void oldNameOfRenamedGroupIsFreeForUseAgain() throws Exception {
String name = name("Name1");
GroupInfo group1 = gApi.groups().create(name).get();
String newName = name("Name2");
gApi.groups().id(group1.id).name(newName);
GroupInfo group2 = gApi.groups().create(name).get();
assertThat(group2.id).isNotEqualTo(group1.id);
}
@Test
public void groupDescription() throws Exception {
String name = name("group");
gApi.groups().create(name);
// get description
assertThat(gApi.groups().id(name).description()).isEmpty();
// set description
String desc = "New description for the group.";
gApi.groups().id(name).description(desc);
assertThat(gApi.groups().id(name).description()).isEqualTo(desc);
// set description to null
gApi.groups().id(name).description(null);
assertThat(gApi.groups().id(name).description()).isEmpty();
// set description to empty string
gApi.groups().id(name).description("");
assertThat(gApi.groups().id(name).description()).isEmpty();
}
@Test
public void groupOptions() throws Exception {
String name = name("group");
gApi.groups().create(name);
// get options
assertThat(gApi.groups().id(name).options().visibleToAll).isNull();
// set options
GroupOptionsInfo options = new GroupOptionsInfo();
options.visibleToAll = true;
gApi.groups().id(name).options(options);
assertThat(gApi.groups().id(name).options().visibleToAll).isTrue();
}
@Test
public void groupOwner() throws Exception {
String name = name("group");
GroupInfo info = gApi.groups().create(name).get();
String adminUUID = getFromCache("Administrators").getGroupUUID().get();
String registeredUUID = SystemGroupBackend.REGISTERED_USERS.get();
// get owner
assertThat(Url.decode(gApi.groups().id(name).owner().id)).isEqualTo(info.id);
// set owner by name
gApi.groups().id(name).owner("Registered Users");
assertThat(Url.decode(gApi.groups().id(name).owner().id)).isEqualTo(registeredUUID);
// set owner by UUID
gApi.groups().id(name).owner(adminUUID);
assertThat(Url.decode(gApi.groups().id(name).owner().id)).isEqualTo(adminUUID);
// set non existing owner
exception.expect(UnprocessableEntityException.class);
gApi.groups().id(name).owner("Non-Existing Group");
}
@Test
public void listNonExistingGroupIncludes_NotFound() throws Exception {
exception.expect(ResourceNotFoundException.class);
gApi.groups().id("non-existing").includedGroups();
}
@Test
public void listEmptyGroupIncludes() throws Exception {
String gx = createGroup("gx");
assertThat(gApi.groups().id(gx).includedGroups()).isEmpty();
}
@Test
public void includeNonExistingGroup() throws Exception {
String gx = createGroup("gx");
exception.expect(UnprocessableEntityException.class);
gApi.groups().id(gx).addGroups("non-existing");
}
@Test
public void listNonEmptyGroupIncludes() throws Exception {
String gx = createGroup("gx");
String gy = createGroup("gy");
String gz = createGroup("gz");
gApi.groups().id(gx).addGroups(gy);
gApi.groups().id(gx).addGroups(gz);
assertIncludes(gApi.groups().id(gx).includedGroups(), gy, gz);
}
@Test
public void listOneIncludeMember() throws Exception {
String gx = createGroup("gx");
String gy = createGroup("gy");
gApi.groups().id(gx).addGroups(gy);
assertIncludes(gApi.groups().id(gx).includedGroups(), gy);
}
@Test
public void listNonExistingGroupMembers_NotFound() throws Exception {
exception.expect(ResourceNotFoundException.class);
gApi.groups().id("non-existing").members();
}
@Test
public void listEmptyGroupMembers() throws Exception {
String group = createGroup("empty");
assertThat(gApi.groups().id(group).members()).isEmpty();
}
@Test
public void listNonEmptyGroupMembers() throws Exception {
String group = createGroup("group");
String user1 = createAccount("user1", group);
String user2 = createAccount("user2", group);
assertMembers(gApi.groups().id(group).members(), user1, user2);
}
@Test
public void listOneGroupMember() throws Exception {
String group = createGroup("group");
String user = createAccount("user1", group);
assertMembers(gApi.groups().id(group).members(), user);
}
@Test
public void listGroupMembersRecursively() throws Exception {
String gx = createGroup("gx");
String ux = createAccount("ux", gx);
String gy = createGroup("gy");
String uy = createAccount("uy", gy);
String gz = createGroup("gz");
String uz = createAccount("uz", gz);
gApi.groups().id(gx).addGroups(gy);
gApi.groups().id(gy).addGroups(gz);
assertMembers(gApi.groups().id(gx).members(), ux);
assertMembers(gApi.groups().id(gx).members(true), ux, uy, uz);
}
@Test
public void usersSeeTheirDirectMembershipWhenListingMembersRecursively() throws Exception {
String group = createGroup("group");
gApi.groups().id(group).addMembers(user.username);
setApiUser(user);
assertMembers(gApi.groups().id(group).members(true), user.fullName);
}
@Test
public void usersDoNotSeeTheirIndirectMembershipWhenListingMembersRecursively() throws Exception {
String group1 = createGroup("group1");
String group2 = createGroup("group2");
gApi.groups().id(group1).addGroups(group2);
gApi.groups().id(group2).addMembers(user.username);
setApiUser(user);
List<AccountInfo> listedMembers = gApi.groups().id(group1).members(true);
assertMembers(listedMembers);
}
@Test
public void adminsSeeTheirIndirectMembershipWhenListingMembersRecursively() throws Exception {
String ownerGroup = createGroup("ownerGroup", null);
String group1 = createGroup("group1", ownerGroup);
String group2 = createGroup("group2", ownerGroup);
gApi.groups().id(group1).addGroups(group2);
gApi.groups().id(group2).addMembers(admin.username);
List<AccountInfo> listedMembers = gApi.groups().id(group1).members(true);
assertMembers(listedMembers, admin.fullName);
}
@Test
public void ownersSeeTheirIndirectMembershipWhenListingMembersRecursively() throws Exception {
String ownerGroup = createGroup("ownerGroup", null);
String group1 = createGroup("group1", ownerGroup);
String group2 = createGroup("group2", ownerGroup);
gApi.groups().id(group1).addGroups(group2);
gApi.groups().id(ownerGroup).addMembers(user.username);
gApi.groups().id(group2).addMembers(user.username);
setApiUser(user);
List<AccountInfo> listedMembers = gApi.groups().id(group1).members(true);
assertMembers(listedMembers, user.fullName);
}
@Test
public void defaultGroupsCreated() throws Exception {
Iterable<String> names = gApi.groups().list().getAsMap().keySet();
assertThat(names).containsAllOf("Administrators", "Non-Interactive Users").inOrder();
}
@Test
public void listAllGroups() throws Exception {
List<String> expectedGroups =
groups.getAllGroupReferences(db).map(GroupReference::getName).sorted().collect(toList());
assertThat(expectedGroups.size()).isAtLeast(2);
assertThat(gApi.groups().list().getAsMap().keySet())
.containsExactlyElementsIn(expectedGroups)
.inOrder();
}
@Test
public void getGroupsByOwner() throws Exception {
String parent = createGroup("test-parent");
List<String> children =
Arrays.asList(createGroup("test-child1", parent), createGroup("test-child2", parent));
// By UUID
List<GroupInfo> owned =
gApi.groups().list().withOwnedBy(getFromCache(parent).getGroupUUID().get()).get();
assertThat(owned.stream().map(g -> g.name).collect(toList()))
.containsExactlyElementsIn(children);
// By name
owned = gApi.groups().list().withOwnedBy(parent).get();
assertThat(owned.stream().map(g -> g.name).collect(toList()))
.containsExactlyElementsIn(children);
// By group that does not own any others
owned = gApi.groups().list().withOwnedBy(owned.get(0).id).get();
assertThat(owned).isEmpty();
// By non-existing group
exception.expect(UnprocessableEntityException.class);
exception.expectMessage("Group Not Found: does-not-exist");
gApi.groups().list().withOwnedBy("does-not-exist").get();
}
@Test
public void onlyVisibleGroupsReturned() throws Exception {
String newGroupName = name("newGroup");
GroupInput in = new GroupInput();
in.name = newGroupName;
in.description = "a hidden group";
in.visibleToAll = false;
in.ownerId = getFromCache("Administrators").getGroupUUID().get();
gApi.groups().create(in);
setApiUser(user);
assertThat(gApi.groups().list().getAsMap()).doesNotContainKey(newGroupName);
setApiUser(admin);
gApi.groups().id(newGroupName).addMembers(user.username);
setApiUser(user);
assertThat(gApi.groups().list().getAsMap()).containsKey(newGroupName);
}
@Test
public void suggestGroup() throws Exception {
Map<String, GroupInfo> groups = gApi.groups().list().withSuggest("adm").getAsMap();
assertThat(groups).containsKey("Administrators");
assertThat(groups).hasSize(1);
assertBadRequest(gApi.groups().list().withSuggest("adm").withSubstring("foo"));
assertBadRequest(gApi.groups().list().withSuggest("adm").withRegex("foo.*"));
assertBadRequest(gApi.groups().list().withSuggest("adm").withUser("user"));
assertBadRequest(gApi.groups().list().withSuggest("adm").withOwned(true));
assertBadRequest(gApi.groups().list().withSuggest("adm").withVisibleToAll(true));
assertBadRequest(gApi.groups().list().withSuggest("adm").withStart(1));
}
@Test
public void withSubstring() throws Exception {
String group = name("Abcdefghijklmnop");
gApi.groups().create(group);
// Choose a substring which isn't part of any group or test method within this class.
String substring = "efghijk";
Map<String, GroupInfo> groups = gApi.groups().list().withSubstring(substring).getAsMap();
assertThat(groups).containsKey(group);
assertThat(groups).hasSize(1);
groups = gApi.groups().list().withSubstring("abcdefghi").getAsMap();
assertThat(groups).containsKey(group);
assertThat(groups).hasSize(1);
String otherGroup = name("Abcdefghijklmnop2");
gApi.groups().create(otherGroup);
groups = gApi.groups().list().withSubstring(substring).getAsMap();
assertThat(groups).hasSize(2);
assertThat(groups).containsKey(group);
assertThat(groups).containsKey(otherGroup);
groups = gApi.groups().list().withSubstring("foo").getAsMap();
assertThat(groups).isEmpty();
}
@Test
public void withRegex() throws Exception {
Map<String, GroupInfo> groups = gApi.groups().list().withRegex("Admin.*").getAsMap();
assertThat(groups).containsKey("Administrators");
assertThat(groups).hasSize(1);
groups = gApi.groups().list().withRegex("admin.*").getAsMap();
assertThat(groups).isEmpty();
groups = gApi.groups().list().withRegex(".*istrators").getAsMap();
assertThat(groups).containsKey("Administrators");
assertThat(groups).hasSize(1);
assertBadRequest(gApi.groups().list().withRegex(".*istrators").withSubstring("s"));
}
@Test
public void allGroupInfoFieldsSetCorrectly() throws Exception {
InternalGroup adminGroup = getFromCache("Administrators");
Map<String, GroupInfo> groups = gApi.groups().list().addGroup(adminGroup.getName()).getAsMap();
assertThat(groups).hasSize(1);
assertThat(groups).containsKey("Administrators");
assertGroupInfo(adminGroup, Iterables.getOnlyElement(groups.values()));
}
@Test
public void getAuditLog() throws Exception {
GroupApi g = gApi.groups().create(name("group"));
List<? extends GroupAuditEventInfo> auditEvents = g.auditLog();
assertThat(auditEvents).hasSize(1);
assertAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, admin.id);
g.addMembers(user.username);
auditEvents = g.auditLog();
assertThat(auditEvents).hasSize(2);
assertAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, user.id);
g.removeMembers(user.username);
auditEvents = g.auditLog();
assertThat(auditEvents).hasSize(3);
assertAuditEvent(auditEvents.get(0), Type.REMOVE_USER, admin.id, user.id);
String otherGroup = name("otherGroup");
gApi.groups().create(otherGroup);
g.addGroups(otherGroup);
auditEvents = g.auditLog();
assertThat(auditEvents).hasSize(4);
assertAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, otherGroup);
g.removeGroups(otherGroup);
auditEvents = g.auditLog();
assertThat(auditEvents).hasSize(5);
assertAuditEvent(auditEvents.get(0), Type.REMOVE_GROUP, admin.id, otherGroup);
/**
* Make sure the new commit is created in a different second. This is added for NoteDb since the
* resolution of Timestamp is 1s there. Adding here is enough because the sort used in {@code
* GetAuditLog} is stable and we process {@code AccountGroupMemberAudit} before {@code
* AccountGroupByIdAud}.
*/
Thread.sleep(1000);
// Add a removed member back again.
g.addMembers(user.username);
auditEvents = g.auditLog();
assertThat(auditEvents).hasSize(6);
assertAuditEvent(auditEvents.get(0), Type.ADD_USER, admin.id, user.id);
// Add a removed group back again.
g.addGroups(otherGroup);
auditEvents = g.auditLog();
assertThat(auditEvents).hasSize(7);
assertAuditEvent(auditEvents.get(0), Type.ADD_GROUP, admin.id, otherGroup);
Timestamp lastDate = null;
for (GroupAuditEventInfo auditEvent : auditEvents) {
if (lastDate != null) {
assertThat(lastDate).isAtLeast(auditEvent.date);
}
lastDate = auditEvent.date;
}
}
// reindex is tested by {@link AbstractQueryGroupsTest#reindex}
@Test
public void reindexPermissions() throws Exception {
TestAccount groupOwner = accountCreator.user2();
GroupInput in = new GroupInput();
in.name = name("group");
in.members =
Collections.singleton(groupOwner).stream().map(u -> u.id.toString()).collect(toList());
in.visibleToAll = true;
GroupInfo group = gApi.groups().create(in).get();
// admin can reindex any group
setApiUser(admin);
gApi.groups().id(group.id).index();
// group owner can reindex own group (group is owned by itself)
setApiUser(groupOwner);
gApi.groups().id(group.id).index();
// user cannot reindex any group
setApiUser(user);
exception.expect(AuthException.class);
exception.expectMessage("not allowed to index group");
gApi.groups().id(group.id).index();
}
@Test
public void pushToGroupBranchIsRejectedForAllUsersRepo() throws Exception {
String groupRef =
RefNames.refsGroups(new AccountGroup.UUID(gApi.groups().create(name("fo")).get().id));
assertPushToGroupBranch(allUsers, groupRef, !groupsInNoteDb(), "group update not allowed");
}
@Test
public void pushToGroupNamesBranchIsRejectedForAllUsersRepo() throws Exception {
assume().that(groupsInNoteDb()).isTrue(); // branch only exists when groups are in NoteDb
// refs/meta/group-names isn't usually available for fetch, so grant ACCESS_DATABASE
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
assertPushToGroupBranch(allUsers, RefNames.REFS_GROUPNAMES, false, "group update not allowed");
}
@Test
public void pushToGroupsBranchForNonAllUsersRepo() throws Exception {
assertCreateGroupBranch(project, null);
String groupRef =
RefNames.refsGroups(new AccountGroup.UUID(gApi.groups().create(name("fo")).get().id));
assertPushToGroupBranch(project, groupRef, true, null);
}
@Test
public void pushToGroupNamesBranchForNonAllUsersRepo() throws Exception {
assertPushToGroupBranch(project, RefNames.REFS_GROUPNAMES, true, null);
}
private void assertPushToGroupBranch(
Project.NameKey project, String groupRefName, boolean createRef, String expectedErrorOnUpdate)
throws Exception {
grant(project, RefNames.REFS_GROUPS + "*", Permission.CREATE, false, REGISTERED_USERS);
grant(project, RefNames.REFS_GROUPS + "*", Permission.PUSH, false, REGISTERED_USERS);
grant(project, RefNames.REFS_GROUPNAMES, Permission.PUSH, false, REGISTERED_USERS);
TestRepository<InMemoryRepository> repo = cloneProject(project);
if (createRef) {
createGroupBranch(project, groupRefName);
}
// update existing branch
fetch(repo, groupRefName + ":groupRef");
repo.reset("groupRef");
PushOneCommit.Result r =
pushFactory
.create(
db,
admin.getIdent(),
repo,
"Update group config",
GroupConfig.GROUP_CONFIG_FILE,
"some content")
.to(groupRefName);
if (expectedErrorOnUpdate != null) {
r.assertErrorStatus(expectedErrorOnUpdate);
} else {
r.assertOkStatus();
}
}
private void assertCreateGroupBranch(Project.NameKey project, String expectedErrorOnCreate)
throws Exception {
grant(project, RefNames.REFS_GROUPS + "*", Permission.CREATE, false, REGISTERED_USERS);
grant(project, RefNames.REFS_GROUPS + "*", Permission.PUSH, false, REGISTERED_USERS);
TestRepository<InMemoryRepository> repo = cloneProject(project);
PushOneCommit.Result r =
pushFactory
.create(
db,
admin.getIdent(),
repo,
"Update group config",
GroupConfig.GROUP_CONFIG_FILE,
"some content")
.setParents(ImmutableList.of())
.to(RefNames.REFS_GROUPS + name("bar"));
if (expectedErrorOnCreate != null) {
r.assertErrorStatus(expectedErrorOnCreate);
} else {
r.assertOkStatus();
}
}
@Test
public void pushToGroupBranchForReviewForAllUsersRepoIsRejectedOnSubmit() throws Exception {
pushToGroupBranchForReviewAndSubmit(allUsers, "group update not allowed");
}
@Test
public void pushToGroupBranchForReviewForNonAllUsersRepoAndSubmit() throws Exception {
pushToGroupBranchForReviewAndSubmit(project, null);
}
@Test
public void pushCustomInheritanceForAllUsersFails() throws Exception {
TestRepository<InMemoryRepository> repo = cloneProject(allUsers, RefNames.REFS_CONFIG);
String config =
gApi.projects()
.name(allUsers.get())
.branch(RefNames.REFS_CONFIG)
.file("project.config")
.asString();
Config cfg = new Config();
cfg.fromText(config);
cfg.setString("access", null, "inheritFrom", project.get());
config = cfg.toText();
PushOneCommit.Result r =
pushFactory
.create(db, admin.getIdent(), repo, "Subject", "project.config", config)
.to(RefNames.REFS_CONFIG);
r.assertErrorStatus("invalid project configuration");
r.assertMessage("All-Users must inherit from All-Projects");
}
@Test
@Sandboxed
public void cannotCreateGroupBranch() throws Exception {
grant(allUsers, RefNames.REFS_GROUPS + "*", Permission.CREATE);
grant(allUsers, RefNames.REFS_GROUPS + "*", Permission.PUSH);
String groupRef = RefNames.refsGroups(new AccountGroup.UUID(name("foo")));
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
PushOneCommit.Result r = pushFactory.create(db, admin.getIdent(), allUsersRepo).to(groupRef);
r.assertErrorStatus();
assertThat(r.getMessage()).contains("Not allowed to create group branch.");
try (Repository repo = repoManager.openRepository(allUsers)) {
assertThat(repo.exactRef(groupRef)).isNull();
}
}
@Test
@Sandboxed
public void cannotDeleteGroupBranch() throws Exception {
assume().that(groupsInNoteDb()).isTrue();
grant(allUsers, RefNames.REFS_GROUPS + "*", Permission.DELETE, true, REGISTERED_USERS);
InternalGroup adminGroup =
groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null);
assertThat(adminGroup).isNotNull();
String groupRef = RefNames.refsGroups(adminGroup.getGroupUUID());
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
PushResult r = deleteRef(allUsersRepo, groupRef);
RemoteRefUpdate refUpdate = r.getRemoteUpdate(groupRef);
assertThat(refUpdate.getStatus()).isEqualTo(RemoteRefUpdate.Status.REJECTED_OTHER_REASON);
assertThat(refUpdate.getMessage()).contains("Not allowed to delete group branch.");
try (Repository repo = repoManager.openRepository(allUsers)) {
assertThat(repo.exactRef(groupRef)).isNotNull();
}
}
@Test
public void defaultPermissionsOnGroupBranches() throws Exception {
assertPermission(
allUsers, RefNames.REFS_GROUPS + "*", Permission.READ, groupRef(REGISTERED_USERS));
}
@Test
@Sandboxed
public void blockReviewDbUpdatesOnGroupCreation() throws Exception {
assume().that(groupsInNoteDb()).isFalse();
cfg.setBoolean("user", null, "blockReviewDbGroupUpdates", true);
try {
gApi.groups().create(name("foo"));
fail("Expected RestApiException: Updates to groups in ReviewDb are blocked");
} catch (RestApiException e) {
assertWriteGroupToReviewDbBlockedException(e);
}
}
@Test
@Sandboxed
public void blockReviewDbUpdatesOnGroupUpdate() throws Exception {
assume().that(groupsInNoteDb()).isFalse();
String group1 = gApi.groups().create(name("foo")).get().id;
String group2 = gApi.groups().create(name("bar")).get().id;
cfg.setBoolean("user", null, "blockReviewDbGroupUpdates", true);
try {
gApi.groups().id(group1).addGroups(group2);
fail("Expected RestApiException: Updates to groups in ReviewDb are blocked");
} catch (RestApiException e) {
assertWriteGroupToReviewDbBlockedException(e);
}
}
private void assertWriteGroupToReviewDbBlockedException(Exception e) throws Exception {
Throwable t = Throwables.getRootCause(e);
assertThat(t).isInstanceOf(OrmException.class);
assertThat(t.getMessage()).isEqualTo("Updates to groups in ReviewDb are blocked");
}
private GroupReference groupRef(AccountGroup.UUID groupUuid) {
GroupDescription.Basic groupDescription = groupBackend.get(groupUuid);
return new GroupReference(groupDescription.getGroupUUID(), groupDescription.getName());
}
private void assertPermission(
Project.NameKey project, String ref, String permission, GroupReference groupReference)
throws IOException {
ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
AccessSection accessSection = cfg.getAccessSection(ref);
assertThat(accessSection).isNotNull();
Permission readPermission = accessSection.getPermission(permission);
assertThat(readPermission).isNotNull();
assertThat(readPermission.getName()).isEqualTo(permission);
assertThat(readPermission.getExclusiveGroup()).isTrue();
assertThat(readPermission.getLabel()).isNull();
PermissionRule rule = readPermission.getRule(groupReference);
assertThat(rule).isNotNull();
assertThat(rule.getGroup()).isEqualTo(groupReference);
assertThat(rule.getAction()).isEqualTo(Action.ALLOW);
assertThat(rule.getForce()).isFalse();
assertThat(rule.getMin()).isEqualTo(0);
assertThat(rule.getMax()).isEqualTo(0);
}
@Test
public void stalenessChecker() throws Exception {
assume().that(groupsInNoteDb()).isTrue();
// Newly created group is not stale
GroupInfo groupInfo = gApi.groups().create(name("foo")).get();
AccountGroup.UUID groupUuid = new AccountGroup.UUID(groupInfo.id);
assertThat(stalenessChecker.isStale(groupUuid)).isFalse();
// Manual update makes index document stale
String groupRef = RefNames.refsGroups(groupUuid);
try (Repository repo = repoManager.openRepository(allUsers);
ObjectInserter oi = repo.newObjectInserter();
RevWalk rw = new RevWalk(repo)) {
RevCommit commit = rw.parseCommit(repo.exactRef(groupRef).getObjectId());
PersonIdent ident = new PersonIdent(serverIdent.get(), TimeUtil.nowTs());
CommitBuilder cb = new CommitBuilder();
cb.setTreeId(commit.getTree());
cb.setCommitter(ident);
cb.setAuthor(ident);
cb.setMessage(commit.getFullMessage());
ObjectId emptyCommit = oi.insert(cb);
oi.flush();
RefUpdate updateRef = repo.updateRef(groupRef);
updateRef.setExpectedOldObjectId(commit.toObjectId());
updateRef.setNewObjectId(emptyCommit);
assertThat(updateRef.forceUpdate()).isEqualTo(RefUpdate.Result.FORCED);
}
assertStaleGroupAndReindex(groupUuid);
// Manually delete group
try (Repository repo = repoManager.openRepository(allUsers);
RevWalk rw = new RevWalk(repo)) {
RevCommit commit = rw.parseCommit(repo.exactRef(groupRef).getObjectId());
RefUpdate updateRef = repo.updateRef(groupRef);
updateRef.setExpectedOldObjectId(commit.toObjectId());
updateRef.setNewObjectId(ObjectId.zeroId());
updateRef.setForceUpdate(true);
assertThat(updateRef.delete()).isEqualTo(RefUpdate.Result.FORCED);
}
assertStaleGroupAndReindex(groupUuid);
}
private void assertStaleGroupAndReindex(AccountGroup.UUID groupUuid) throws IOException {
// Evict group from cache to be sure that we use the index state for staleness checks. This has
// to happen directly on the groupsByUUID cache because GroupsCacheImpl triggers a reindex for
// the group.
groupsByUUIDCache.invalidate(groupUuid.get());
assertThat(stalenessChecker.isStale(groupUuid)).isTrue();
// Reindex fixes staleness
groupIndexer.index(groupUuid);
assertThat(stalenessChecker.isStale(groupUuid)).isFalse();
}
private void pushToGroupBranchForReviewAndSubmit(Project.NameKey project, String expectedError)
throws Exception {
grantLabel(
"Code-Review", -2, 2, project, RefNames.REFS_GROUPS + "*", false, REGISTERED_USERS, false);
grant(project, RefNames.REFS_GROUPS + "*", Permission.SUBMIT, false, REGISTERED_USERS);
String groupRefName = RefNames.REFS_GROUPS + name("foo");
createGroupBranch(project, groupRefName);
TestRepository<InMemoryRepository> repo = cloneProject(project);
fetch(repo, groupRefName + ":groupRef");
repo.reset("groupRef");
PushOneCommit.Result r =
pushFactory
.create(
db, admin.getIdent(), repo, "Update group config", "group.config", "some content")
.to(MagicBranch.NEW_CHANGE + groupRefName);
r.assertOkStatus();
assertThat(r.getChange().change().getDest().get()).isEqualTo(groupRefName);
gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
if (expectedError != null) {
exception.expect(ResourceConflictException.class);
exception.expectMessage("group update not allowed");
}
gApi.changes().id(r.getChangeId()).current().submit();
}
private void createGroupBranch(Project.NameKey project, String ref) throws IOException {
try (Repository r = repoManager.openRepository(project);
ObjectInserter oi = r.newObjectInserter();
RevWalk rw = new RevWalk(r)) {
ObjectId emptyTree = oi.insert(Constants.OBJ_TREE, new byte[] {});
PersonIdent ident = new PersonIdent(serverIdent.get(), TimeUtil.nowTs());
CommitBuilder cb = new CommitBuilder();
cb.setTreeId(emptyTree);
cb.setCommitter(ident);
cb.setAuthor(ident);
cb.setMessage("Create group");
ObjectId emptyCommit = oi.insert(cb);
oi.flush();
RefUpdate updateRef = r.updateRef(ref);
updateRef.setExpectedOldObjectId(ObjectId.zeroId());
updateRef.setNewObjectId(emptyCommit);
assertThat(updateRef.update(rw)).isEqualTo(RefUpdate.Result.NEW);
}
}
private void assertAuditEvent(
GroupAuditEventInfo info,
Type expectedType,
Account.Id expectedUser,
Account.Id expectedMember) {
assertThat(info.user._accountId).isEqualTo(expectedUser.get());
assertThat(info.type).isEqualTo(expectedType);
assertThat(info).isInstanceOf(UserMemberAuditEventInfo.class);
assertThat(((UserMemberAuditEventInfo) info).member._accountId).isEqualTo(expectedMember.get());
}
private void assertAuditEvent(
GroupAuditEventInfo info,
Type expectedType,
Account.Id expectedUser,
String expectedMemberGroupName) {
assertThat(info.user._accountId).isEqualTo(expectedUser.get());
assertThat(info.type).isEqualTo(expectedType);
assertThat(info).isInstanceOf(GroupMemberAuditEventInfo.class);
assertThat(((GroupMemberAuditEventInfo) info).member.name).isEqualTo(expectedMemberGroupName);
}
private void assertMembers(String group, TestAccount... expectedMembers) throws Exception {
assertMembers(
gApi.groups().id(group).members(),
TestAccount.names(expectedMembers).stream().toArray(String[]::new));
assertAccountInfos(Arrays.asList(expectedMembers), gApi.groups().id(group).members());
}
private void assertMembers(Iterable<AccountInfo> members, String... expectedNames) {
assertThat(Iterables.transform(members, i -> i.name))
.containsExactlyElementsIn(Arrays.asList(expectedNames))
.inOrder();
}
private void assertNoMembers(String group) throws Exception {
assertThat(gApi.groups().id(group).members()).isEmpty();
}
private void assertIncludes(String group, String... expectedNames) throws Exception {
assertIncludes(gApi.groups().id(group).includedGroups(), expectedNames);
}
private static void assertIncludes(Iterable<GroupInfo> includes, String... expectedNames) {
assertThat(Iterables.transform(includes, i -> i.name))
.containsExactlyElementsIn(Arrays.asList(expectedNames))
.inOrder();
}
private void assertNoIncludes(String group) throws Exception {
assertThat(gApi.groups().id(group).includedGroups()).isEmpty();
}
private InternalGroup getFromCache(String name) throws Exception {
return groupCache.get(new AccountGroup.NameKey(name)).orElse(null);
}
private void assertBadRequest(ListRequest req) throws Exception {
try {
req.get();
fail("Expected BadRequestException");
} catch (BadRequestException e) {
// Expected
}
}
private boolean groupsInNoteDb() {
return cfg.getBoolean(SECTION_NOTE_DB, GROUPS.key(), WRITE, false)
&& cfg.getBoolean(SECTION_NOTE_DB, GROUPS.key(), READ, false);
}
}