blob: 4cbc36b5811fae8a3cbab09bef335920c490dadc [file] [log] [blame]
// 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.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
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.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
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.testing.GerritJUnit.assertThrows;
import static com.google.gerrit.truth.MapSubject.assertThatMap;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.truth.Correspondence;
import com.google.common.util.concurrent.AtomicLongMap;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.ExtensionRegistry;
import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.ProjectResetter;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.Sandboxed;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.UseClockStep;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.auth.ldap.FakeLdapGroupBackend;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.GroupReference;
import com.google.gerrit.entities.InternalGroup;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
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.UserMemberAuditEventInfo;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.common.GroupOptionsInfo;
import com.google.gerrit.extensions.events.GroupIndexedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
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.UnprocessableEntityException;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.server.ServerInitiated;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupIncludeCache;
import com.google.gerrit.server.account.GroupsSnapshotReader;
import com.google.gerrit.server.account.ServiceUserClassifier;
import com.google.gerrit.server.group.PeriodicGroupIndexer;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.group.db.GroupDelta;
import com.google.gerrit.server.group.db.Groups;
import com.google.gerrit.server.group.db.GroupsConsistencyChecker;
import com.google.gerrit.server.group.db.GroupsUpdate;
import com.google.gerrit.server.group.db.InternalGroupCreation;
import com.google.gerrit.server.index.group.GroupIndexer;
import com.google.gerrit.server.index.group.StalenessChecker;
import com.google.gerrit.server.notedb.Sequences;
import com.google.gerrit.server.util.MagicBranch;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.testing.GerritJUnit.ThrowingRunnable;
import com.google.gerrit.truth.NullAwareCorrespondence;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Module;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
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.Ref;
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.After;
import org.junit.Test;
@NoHttpd
@UseClockStep
public class GroupsIT extends AbstractDaemonTest {
@Inject @ServerInitiated private GroupsUpdate groupsUpdate;
@Inject private AccountOperations accountOperations;
@Inject private GroupIncludeCache groupIncludeCache;
@Inject private GroupIndexer groupIndexer;
@Inject private GroupOperations groupOperations;
@Inject private Groups groups;
@Inject private GroupsConsistencyChecker consistencyChecker;
@Inject private PeriodicGroupIndexer slaveGroupIndexer;
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private Sequences seq;
@Inject private StalenessChecker stalenessChecker;
@Inject private ExtensionRegistry extensionRegistry;
@Inject private GroupsSnapshotReader groupsSnapshotReader;
@Override
public Module createModule() {
return new AbstractModule() {
@Override
protected void configure() {
/** Binding a {@link FakeLdapGroupBackend} to test adding external groups * */
DynamicSet.bind(binder(), GroupBackend.class).to(FakeLdapGroupBackend.class);
}
};
}
@After
public void consistencyCheck() throws Exception {
if (description.getAnnotation(IgnoreGroupInconsistencies.class) == null) {
assertThat(consistencyChecker.check()).isEmpty();
}
}
@Override
protected ProjectResetter.Config resetProjects() {
// Don't reset All-Users since deleting users makes groups inconsistent (e.g. groups would
// contain members that no longer exist) and as result of this the group consistency checker
// that is executed after each test would fail.
return new ProjectResetter.Config().reset(allProjects, RefNames.REFS_CONFIG);
}
@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 {
assertThrows(
ResourceNotFoundException.class,
() -> gApi.groups().id("non-existing").addMembers("admin"));
}
@Test
public void removeFromNonExistingGroup_NotFound() throws Exception {
assertThrows(
ResourceNotFoundException.class,
() -> gApi.groups().id("non-existing").removeMembers("admin"));
}
@Test
public void addRemoveMember() throws Exception {
AccountGroup.UUID group = groupOperations.newGroup().create();
gApi.groups().id(group.get()).addMembers("user1");
assertMembers(group.get(), user);
gApi.groups().id(group.get()).removeMembers("user1");
ImmutableSet<Account.Id> members = groupOperations.group(group).get().members();
assertThat(members).isEmpty();
}
@Test
public void addExternalGroups() throws Exception {
AccountGroup.UUID group1 = groupOperations.newGroup().create();
AccountGroup.UUID group2 = groupOperations.newGroup().create();
String g1RefName = RefNames.refsGroups(group1);
String g2RefName = RefNames.refsGroups(group2);
gApi.groups().id(group1.get()).addGroups("ldap:external_g1");
gApi.groups().id(group2.get()).addGroups("ldap:external_g2");
assertThat(groupIncludeCache.allExternalMembers())
.containsAtLeastElementsIn(
ImmutableList.of(
AccountGroup.UUID.parse("ldap:external_g1"),
AccountGroup.UUID.parse("ldap:external_g2")));
assertThat(groupIncludeCache.parentGroupsOf(AccountGroup.UUID.parse("ldap:external_g1")))
.containsExactly(group1);
assertThat(groupIncludeCache.parentGroupsOf(AccountGroup.UUID.parse("ldap:external_g2")))
.containsExactly(group2);
GroupsSnapshotReader.Snapshot snapshot = groupsSnapshotReader.getSnapshot();
gApi.groups().id(group1.get()).removeGroups("ldap:external_g1");
GroupsSnapshotReader.Snapshot newSnapshot = groupsSnapshotReader.getSnapshot();
/** Make sure groups snapshots are consistent */
ObjectId g1ObjectId = getObjectIdFromSnapshot(snapshot, g1RefName);
ObjectId g2ObjectId = getObjectIdFromSnapshot(snapshot, g2RefName);
assertThat(snapshot.hash()).isNotEqualTo(newSnapshot.hash());
assertThat(g1ObjectId).isNotEqualTo(getObjectIdFromSnapshot(newSnapshot, g1RefName));
assertThat(g2ObjectId).isEqualTo(getObjectIdFromSnapshot(newSnapshot, g2RefName));
assertThat(snapshot.groupsRefs().stream().map(Ref::getName).collect(toList()))
.containsAtLeastElementsIn(ImmutableList.of(g1RefName, g2RefName));
assertThat(newSnapshot.groupsRefs().stream().map(Ref::getName).collect(toList()))
.containsAtLeastElementsIn(ImmutableList.of(g1RefName, g2RefName));
/** GroupIncludeCache should return ldap:external_g2 only */
assertThat(groupIncludeCache.allExternalMembers())
.contains(AccountGroup.UUID.parse("ldap:external_g2"));
/** Testing groups.getExternalGroups() with the old Snapshot */
assertThat(groups.getExternalGroups(snapshot.groupsRefs()))
.containsAtLeastElementsIn(
ImmutableList.of(
AccountGroup.UUID.parse("ldap:external_g1"),
AccountGroup.UUID.parse("ldap:external_g2")));
}
private ObjectId getObjectIdFromSnapshot(GroupsSnapshotReader.Snapshot snapshot, String refName) {
return snapshot.groupsRefs().stream()
.filter(r -> r.getName().equals(refName))
.map(Ref::getObjectId)
.collect(onlyElement());
}
@Test
public void removeMember_nullInMemberInputDoesNotCauseFailure() throws Exception {
AccountGroup.UUID group =
groupOperations.newGroup().addMember(admin.id()).addMember(user.id()).create();
gApi.groups().id(group.get()).removeMembers(user.id().toString(), null);
ImmutableSet<Account.Id> members = groupOperations.group(group).get().members();
assertThat(members).containsExactly(admin.id());
}
@Test
public void removeMember_emptyStringInMemberInputDoesNotCauseFailure() throws Exception {
AccountGroup.UUID group =
groupOperations.newGroup().addMember(admin.id()).addMember(user.id()).create();
gApi.groups().id(group.get()).removeMembers(user.id().toString(), "");
ImmutableSet<Account.Id> members = groupOperations.group(group).get().members();
assertThat(members).containsExactly(admin.id());
}
@Test
public void cachedGroupsForMemberAreUpdatedOnMemberAdditionAndRemoval() throws Exception {
String username = name("user");
Account.Id accountId = accountOperations.newAccount().username(username).create();
// Fill the cache for the observed account.
groupIncludeCache.getGroupsWithMember(accountId);
AccountGroup.UUID groupUuid = groupOperations.newGroup().create();
gApi.groups().id(groupUuid.get()).addMembers(username);
Collection<AccountGroup.UUID> groupsWithMemberAfterAddition =
groupIncludeCache.getGroupsWithMember(accountId);
assertThat(groupsWithMemberAfterAddition).contains(groupUuid);
gApi.groups().id(groupUuid.get()).removeMembers(username);
Collection<AccountGroup.UUID> groupsWithMemberAfterRemoval =
groupIncludeCache.getGroupsWithMember(accountId);
assertThat(groupsWithMemberAfterRemoval).doesNotContain(groupUuid);
}
@Test
public void cachedGroupByNameIsUpdatedOnCreation() throws Exception {
String newGroupName = name("newGroup");
AccountGroup.NameKey nameKey = AccountGroup.nameKey(newGroupName);
assertThat(groupCache.get(nameKey)).isEmpty();
gApi.groups().create(newGroupName);
assertThat(groupCache.get(nameKey)).isPresent();
}
@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 {
assertThrows(
UnprocessableEntityException.class,
() -> gApi.groups().id("Administrators").addMembers("non-existing"));
}
@Test
public void addMultipleMembers() throws Exception {
AccountGroup.UUID group = groupOperations.newGroup().create();
String u1 = name("u1");
accountOperations.newAccount().username(u1).create();
String u2 = name("u2");
accountOperations.newAccount().username(u2).create();
gApi.groups().id(group.get()).addMembers(u1, u2);
List<AccountInfo> members = gApi.groups().id(group.get()).members();
assertThat(members)
.comparingElementsUsing(getAccountToUsernameCorrespondence())
.containsExactly(u1, u2);
}
@Test
public void membersWithAtSignInUsernameCanBeAdded() throws Exception {
AccountGroup.UUID group = groupOperations.newGroup().create();
String usernameWithAt = name("u1@something");
accountOperations.newAccount().username(usernameWithAt).create();
gApi.groups().id(group.get()).addMembers(usernameWithAt);
List<AccountInfo> members = gApi.groups().id(group.get()).members();
assertThat(members)
.comparingElementsUsing(getAccountToUsernameCorrespondence())
.containsExactly(usernameWithAt);
}
@Test
public void membersWithAtSignInUsernameAreNotConfusedWithSimilarUsernames() throws Exception {
AccountGroup.UUID group = groupOperations.newGroup().create();
String usernameWithAt = name("u1@something");
accountOperations.newAccount().username(usernameWithAt).create();
String usernameWithoutAt = name("u1something");
accountOperations.newAccount().username(usernameWithoutAt).create();
String usernameOnlyPrefix = name("u1");
accountOperations.newAccount().username(usernameOnlyPrefix).create();
String usernameOnlySuffix = name("something");
accountOperations.newAccount().username(usernameOnlySuffix).create();
gApi.groups()
.id(group.get())
.addMembers(usernameWithAt, usernameWithoutAt, usernameOnlyPrefix, usernameOnlySuffix);
List<AccountInfo> members = gApi.groups().id(group.get()).members();
assertThat(members)
.comparingElementsUsing(getAccountToUsernameCorrespondence())
.containsExactly(usernameWithAt, usernameWithoutAt, usernameOnlyPrefix, usernameOnlySuffix);
}
@Test
public void includeRemoveGroup() throws Exception {
AccountGroup.UUID parent = groupOperations.newGroup().create();
AccountGroup.UUID group = groupOperations.newGroup().create();
gApi.groups().id(parent.get()).addGroups(group.get());
assertThat(groupOperations.group(parent).get().subgroups()).containsExactly(group);
gApi.groups().id(parent.get()).removeGroups(group.get());
assertThat(groupOperations.group(parent).get().subgroups()).isEmpty();
}
@Test
public void includeExternalGroup() throws Exception {
AccountGroup.UUID group = groupOperations.newGroup().create();
String subgroupUuid = SystemGroupBackend.REGISTERED_USERS.get();
gApi.groups().id(group.get()).addGroups(subgroupUuid);
List<GroupInfo> subgroups = gApi.groups().id(group.get()).includedGroups();
assertThat(subgroups).hasSize(1);
assertThat(subgroups.get(0).id).isEqualTo(subgroupUuid.replace(":", "%3A"));
assertThat(subgroups.get(0).name).isEqualTo("Registered Users");
assertThat(subgroups.get(0).groupId).isNull();
List<? extends GroupAuditEventInfo> auditEvents = gApi.groups().id(group.get()).auditLog();
assertThat(auditEvents).hasSize(1);
assertSubgroupAuditEvent(
auditEvents.get(0), GroupAuditEventInfo.Type.ADD_GROUP, admin.id(), "Registered Users");
}
@Test
public void includeExistingGroup_OK() throws Exception {
AccountGroup.UUID parent = groupOperations.newGroup().create();
AccountGroup.UUID group = groupOperations.newGroup().create();
groupOperations.group(parent).forUpdate().addSubgroup(group);
gApi.groups().id(parent.get()).addGroups(group.get());
ImmutableSet<AccountGroup.UUID> subgroups = groupOperations.group(parent).get().subgroups();
assertThat(subgroups).containsExactly(group);
}
@Test
public void addMultipleIncludes() throws Exception {
AccountGroup.UUID parent = groupOperations.newGroup().create();
AccountGroup.UUID group1 = groupOperations.newGroup().create();
AccountGroup.UUID group2 = groupOperations.newGroup().create();
gApi.groups().id(parent.get()).addGroups(group1.get(), group2.get());
ImmutableSet<AccountGroup.UUID> subgroups = groupOperations.group(parent).get().subgroups();
assertThat(subgroups).containsExactly(group1, group2);
}
@Test
public void createGroup() throws Exception {
String newGroupName = name("newGroup");
GroupInfo g = gApi.groups().create(newGroupName).get();
assertGroupInfo(group(newGroupName), g);
}
@Test
public void createGroupNameIsTrimmed() throws Exception {
String newGroupName = name("newGroup");
GroupInfo g = gApi.groups().create(" " + newGroupName + " ").get();
assertGroupInfo(group(newGroupName), g);
}
@Test
public void createDuplicateInternalGroupCaseSensitiveName_Conflict() throws Exception {
String dupGroupName = name("dupGroup");
gApi.groups().create(dupGroupName);
ResourceConflictException thrown =
assertThrows(ResourceConflictException.class, () -> gApi.groups().create(dupGroupName));
assertThat(thrown).hasMessageThat().contains("group '" + dupGroupName + "' already exists");
}
@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";
ResourceConflictException thrown =
assertThrows(ResourceConflictException.class, () -> gApi.groups().create(newGroupName));
assertThat(thrown).hasMessageThat().contains("group 'Registered Users' already exists");
}
@Test
public void createDuplicateSystemGroupCaseInsensitiveName_Conflict() throws Exception {
String newGroupName = "registered users";
ResourceConflictException thrown =
assertThrows(ResourceConflictException.class, () -> gApi.groups().create(newGroupName));
assertThat(thrown).hasMessageThat().contains("group 'Registered Users' already exists");
}
@Test
@GerritConfig(name = "groups.global:Anonymous-Users.name", value = "All Users")
public void createGroupWithConfiguredNameOfSystemGroup_Conflict() throws Exception {
ResourceConflictException thrown =
assertThrows(ResourceConflictException.class, () -> gApi.groups().create("all users"));
assertThat(thrown).hasMessageThat().contains("group 'All Users' already exists");
}
@Test
@GerritConfig(name = "groups.global:Anonymous-Users.name", value = "All Users")
public void createGroupWithDefaultNameOfSystemGroup_Conflict() throws Exception {
ResourceConflictException thrown =
assertThrows(
ResourceConflictException.class, () -> gApi.groups().create("anonymous users"));
assertThat(thrown).hasMessageThat().contains("group name 'Anonymous Users' is reserved");
}
@Test
public void createGroupWithUuid() throws Exception {
AccountGroup.UUID uuid = AccountGroup.UUID.parse("4eb25d1cca562f53b9356117f33840706a36a349");
GroupInput input = new GroupInput();
input.uuid = uuid.get();
input.name = name("new-group");
GroupInfo info = gApi.groups().create(input).get();
assertThat(info.name).isEqualTo(input.name);
assertThat(info.id).isEqualTo(input.uuid);
}
@Test
public void createGroupWithExistingUuid_Conflict() throws Exception {
GroupInfo existingGroup = gApi.groups().create(name("new-group")).get();
GroupInput input = new GroupInput();
input.uuid = existingGroup.id;
input.name = name("another-new-group");
ResourceConflictException thrown =
assertThrows(ResourceConflictException.class, () -> gApi.groups().create(input).get());
assertThat(thrown)
.hasMessageThat()
.isEqualTo(String.format("group with UUID '%s' already exists", input.uuid));
}
@Test
public void createGroupWithInvalidUuid_BadRequest() throws Exception {
AccountGroup.UUID uuid = AccountGroup.UUID.parse("foo:bar");
GroupInput input = new GroupInput();
input.uuid = uuid.get();
input.name = name("new-group");
BadRequestException thrown =
assertThrows(BadRequestException.class, () -> gApi.groups().create(input).get());
assertThat(thrown)
.hasMessageThat()
.isEqualTo(String.format("invalid group UUID '%s'", input.uuid));
}
@Test
public void createGroupWithProperties() throws Exception {
GroupInput in = new GroupInput();
in.name = name("newGroup");
in.description = "Test description";
in.visibleToAll = true;
in.ownerId = adminGroupUuid().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 {
requestScopeOperations.setApiUser(user.id());
assertThrows(AuthException.class, () -> gApi.groups().create(name("newGroup")));
}
@Test
public void createdOnFieldIsPopulatedForNewGroup() throws Exception {
// NoteDb allows only second precision.
Timestamp testStartTime = TimeUtil.truncateToSecond(TimeUtil.nowTs());
String newGroupName = name("newGroup");
GroupInfo group = gApi.groups().create(newGroupName).get();
assertThat(group.createdOn).isAtLeast(testStartTime);
}
@Test
public void cachedGroupsForMemberAreUpdatedOnGroupCreation() throws Exception {
Account.Id accountId = accountOperations.newAccount().create();
// Fill the cache for the observed account.
groupIncludeCache.getGroupsWithMember(accountId);
GroupInput groupInput = new GroupInput();
groupInput.name = name("Users");
groupInput.members = ImmutableList.of(String.valueOf(accountId.get()));
GroupInfo group = gApi.groups().create(groupInput).get();
Collection<AccountGroup.UUID> groups = groupIncludeCache.getGroupsWithMember(accountId);
assertThat(groups).containsExactly(AccountGroup.uuid(group.id));
}
@Test
public void getGroup() throws Exception {
InternalGroup adminGroup = adminGroup();
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 {
assertThrows(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();
assertThrows(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);
assertThrows(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);
assertGroupDoesNotExist(name);
assertThrows(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 = adminGroupUuid().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
assertThrows(
UnprocessableEntityException.class,
() -> gApi.groups().id(name).owner("Non-Existing Group"));
}
@Test
public void listNonExistingGroupIncludes_NotFound() throws Exception {
assertThrows(
ResourceNotFoundException.class, () -> gApi.groups().id("non-existing").includedGroups());
}
@Test
public void listEmptyGroupIncludes() throws Exception {
AccountGroup.UUID gx = groupOperations.newGroup().create();
assertThat(gApi.groups().id(gx.get()).includedGroups()).isEmpty();
}
@Test
public void includeNonExistingGroup() throws Exception {
AccountGroup.UUID gx = groupOperations.newGroup().create();
assertThrows(
UnprocessableEntityException.class,
() -> gApi.groups().id(gx.get()).addGroups("non-existing"));
}
@Test
public void listNonEmptyGroupIncludes() throws Exception {
AccountGroup.UUID gz = groupOperations.newGroup().create();
AccountGroup.UUID gy = groupOperations.newGroup().create();
AccountGroup.UUID gx = groupOperations.newGroup().subgroups(gy, gz).create();
List<GroupInfo> includes = gApi.groups().id(gx.get()).includedGroups();
String gyName = groupOperations.group(gy).get().name();
String gzName = groupOperations.group(gz).get().name();
assertIncludes(includes, gyName, gzName);
}
@Test
public void listOneIncludeMember() throws Exception {
AccountGroup.UUID gy = groupOperations.newGroup().create();
AccountGroup.UUID gx = groupOperations.newGroup().subgroups(gy).create();
List<GroupInfo> includes = gApi.groups().id(gx.get()).includedGroups();
String gyName = groupOperations.group(gy).get().name();
assertIncludes(includes, gyName);
}
@Test
public void listNonExistingGroupMembers_NotFound() throws Exception {
assertThrows(ResourceNotFoundException.class, () -> gApi.groups().id("non-existing").members());
}
@Test
public void listEmptyGroupMembers() throws Exception {
AccountGroup.UUID group = groupOperations.newGroup().create();
assertThat(gApi.groups().id(group.get()).members()).isEmpty();
}
@Test
public void listNonEmptyGroupMembers() throws Exception {
AccountGroup.UUID group = groupOperations.newGroup().create();
String user1 = name("user1");
accountOperations.newAccount().username(user1).create();
String user2 = name("user2");
accountOperations.newAccount().username(user2).create();
gApi.groups().id(group.get()).addMembers(user1, user2);
assertMembers(gApi.groups().id(group.get()).members(), user1, user2);
}
@Test
public void listOneGroupMember() throws Exception {
AccountGroup.UUID group = groupOperations.newGroup().create();
String user = name("user1");
accountOperations.newAccount().username(user).create();
gApi.groups().id(group.get()).addMembers(user);
assertMembers(gApi.groups().id(group.get()).members(), user);
}
@Test
public void listGroupMembersRecursively() throws Exception {
AccountGroup.UUID gx = groupOperations.newGroup().create();
String ux = name("ux");
accountOperations.newAccount().username(ux).create();
gApi.groups().id(gx.get()).addMembers(ux);
AccountGroup.UUID gy = groupOperations.newGroup().create();
String uy = name("uy");
accountOperations.newAccount().username(uy).create();
gApi.groups().id(gy.get()).addMembers(uy);
AccountGroup.UUID gz = groupOperations.newGroup().create();
String uz = name("uz");
accountOperations.newAccount().username(uz).create();
gApi.groups().id(gz.get()).addMembers(uz);
gApi.groups().id(gx.get()).addGroups(gy.get());
gApi.groups().id(gy.get()).addGroups(gz.get());
assertMembers(gApi.groups().id(gx.get()).members(), ux);
assertMembers(gApi.groups().id(gx.get()).members(true), ux, uy, uz);
}
@Test
public void usersSeeTheirDirectMembershipWhenListingMembersRecursively() throws Exception {
AccountGroup.UUID group = groupOperations.newGroup().create();
gApi.groups().id(group.get()).addMembers(user.username());
requestScopeOperations.setApiUser(user.id());
assertMembers(gApi.groups().id(group.get()).members(true), user.fullName());
}
@Test
public void usersDoNotSeeTheirIndirectMembershipWhenListingMembersRecursively() throws Exception {
AccountGroup.UUID group1 = groupOperations.newGroup().ownerGroupUuid(adminGroupUuid()).create();
AccountGroup.UUID group2 = groupOperations.newGroup().ownerGroupUuid(adminGroupUuid()).create();
gApi.groups().id(group1.get()).addGroups(group2.get());
gApi.groups().id(group2.get()).addMembers(user.username());
requestScopeOperations.setApiUser(user.id());
List<AccountInfo> listedMembers = gApi.groups().id(group1.get()).members(true);
assertMembers(listedMembers);
}
@Test
public void adminsSeeTheirIndirectMembershipWhenListingMembersRecursively() throws Exception {
AccountGroup.UUID ownerGroup = groupOperations.newGroup().create();
AccountGroup.UUID group1 = groupOperations.newGroup().ownerGroupUuid(ownerGroup).create();
AccountGroup.UUID group2 = groupOperations.newGroup().ownerGroupUuid(ownerGroup).create();
gApi.groups().id(group1.get()).addGroups(group2.get());
gApi.groups().id(group2.get()).addMembers(admin.username());
List<AccountInfo> listedMembers = gApi.groups().id(group1.get()).members(true);
assertMembers(listedMembers, admin.fullName());
}
@Test
public void ownersSeeTheirIndirectMembershipWhenListingMembersRecursively() throws Exception {
AccountGroup.UUID ownerGroup = groupOperations.newGroup().create();
AccountGroup.UUID group1 = groupOperations.newGroup().ownerGroupUuid(ownerGroup).create();
AccountGroup.UUID group2 = groupOperations.newGroup().ownerGroupUuid(ownerGroup).create();
gApi.groups().id(group1.get()).addGroups(group2.get());
gApi.groups().id(ownerGroup.get()).addMembers(user.username());
gApi.groups().id(group2.get()).addMembers(user.username());
requestScopeOperations.setApiUser(user.id());
List<AccountInfo> listedMembers = gApi.groups().id(group1.get()).members(true);
assertMembers(listedMembers, user.fullName());
}
@Test
public void defaultGroupsCreated() throws Exception {
Iterable<String> names = gApi.groups().list().getAsMap().keySet();
assertThat(names)
.containsAtLeast("Administrators", ServiceUserClassifier.SERVICE_USERS)
.inOrder();
}
@Test
public void listAllGroups() throws Exception {
List<String> expectedGroups =
groups.getAllGroupReferences().map(GroupReference::getName).sorted().collect(toList());
assertThat(expectedGroups.size()).isAtLeast(2);
assertThatMap(gApi.groups().list().getAsMap())
.keys()
.containsExactlyElementsIn(expectedGroups)
.inOrder();
}
@Test
public void getGroupsByOwner() throws Exception {
AccountGroup.UUID parent = groupOperations.newGroup().ownerGroupUuid(adminGroupUuid()).create();
List<AccountGroup.UUID> children =
Arrays.asList(
groupOperations.newGroup().ownerGroupUuid(parent).create(),
groupOperations.newGroup().ownerGroupUuid(parent).create());
// By UUID
List<GroupInfo> owned = gApi.groups().list().withOwnedBy(parent.get()).get();
assertThat(owned.stream().map(g -> AccountGroup.uuid(g.id)).collect(toList()))
.containsExactlyElementsIn(children);
// By name
String parentName = groupOperations.group(parent).get().name();
owned = gApi.groups().list().withOwnedBy(parentName).get();
assertThat(owned.stream().map(g -> AccountGroup.uuid(g.id)).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
UnprocessableEntityException thrown =
assertThrows(
UnprocessableEntityException.class,
() -> gApi.groups().list().withOwnedBy("does-not-exist").get());
assertThat(thrown).hasMessageThat().contains("Group Not Found: does-not-exist");
}
@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 = adminGroupUuid().get();
gApi.groups().create(in);
requestScopeOperations.setApiUser(user.id());
assertThatMap(gApi.groups().list().getAsMap()).keys().doesNotContain(newGroupName);
requestScopeOperations.setApiUser(admin.id());
gApi.groups().id(newGroupName).addMembers(user.username());
requestScopeOperations.setApiUser(user.id());
assertThatMap(gApi.groups().list().getAsMap()).keys().contains(newGroupName);
}
@Test
public void suggestGroup() throws Exception {
Map<String, GroupInfo> groups = gApi.groups().list().withSuggest("adm").getAsMap();
assertThatMap(groups).keys().containsExactly("Administrators");
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();
assertThatMap(groups).keys().containsExactly(group);
groups = gApi.groups().list().withSubstring("abcdefghi").getAsMap();
assertThatMap(groups).keys().containsExactly(group);
String otherGroup = name("Abcdefghijklmnop2");
gApi.groups().create(otherGroup);
groups = gApi.groups().list().withSubstring(substring).getAsMap();
assertThatMap(groups).keys().containsExactly(group, otherGroup);
groups = gApi.groups().list().withSubstring("non-existing-substring").getAsMap();
assertThat(groups).isEmpty();
}
@Test
public void withRegex() throws Exception {
Map<String, GroupInfo> groups = gApi.groups().list().withRegex("Admin.*").getAsMap();
assertThatMap(groups).keys().containsExactly("Administrators");
groups = gApi.groups().list().withRegex("admin.*").getAsMap();
assertThat(groups).isEmpty();
groups = gApi.groups().list().withRegex(".*istrators").getAsMap();
assertThatMap(groups).keys().containsExactly("Administrators");
assertBadRequest(gApi.groups().list().withRegex(".*istrators").withSubstring("s"));
}
@Test
public void allGroupInfoFieldsSetCorrectly() throws Exception {
InternalGroup adminGroup = adminGroup();
Map<String, GroupInfo> groups = gApi.groups().list().addGroup(adminGroup.getName()).getAsMap();
assertThatMap(groups).keys().containsExactly("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);
assertMemberAuditEvent(
auditEvents.get(0), GroupAuditEventInfo.Type.ADD_USER, admin.id(), admin.id());
g.addMembers(user.username());
auditEvents = g.auditLog();
assertThat(auditEvents).hasSize(2);
assertMemberAuditEvent(
auditEvents.get(0), GroupAuditEventInfo.Type.ADD_USER, admin.id(), user.id());
g.removeMembers(user.username());
auditEvents = g.auditLog();
assertThat(auditEvents).hasSize(3);
assertMemberAuditEvent(
auditEvents.get(0), GroupAuditEventInfo.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);
assertSubgroupAuditEvent(
auditEvents.get(0), GroupAuditEventInfo.Type.ADD_GROUP, admin.id(), otherGroup);
g.removeGroups(otherGroup);
auditEvents = g.auditLog();
assertThat(auditEvents).hasSize(5);
assertSubgroupAuditEvent(
auditEvents.get(0), GroupAuditEventInfo.Type.REMOVE_GROUP, admin.id(), otherGroup);
// Add a removed member back again.
g.addMembers(user.username());
auditEvents = g.auditLog();
assertThat(auditEvents).hasSize(6);
assertMemberAuditEvent(
auditEvents.get(0), GroupAuditEventInfo.Type.ADD_USER, admin.id(), user.id());
// Add a removed group back again.
g.addGroups(otherGroup);
auditEvents = g.auditLog();
assertThat(auditEvents).hasSize(7);
assertSubgroupAuditEvent(
auditEvents.get(0), GroupAuditEventInfo.Type.ADD_GROUP, admin.id(), otherGroup);
Timestamp lastDate = null;
for (GroupAuditEventInfo auditEvent : auditEvents) {
if (lastDate != null) {
assertThat(lastDate).isAtLeast(auditEvent.date);
}
lastDate = auditEvent.date;
}
}
/**
* {@code @Sandboxed} is used by this test because it deletes a group reference which introduces
* an inconsistency for the group storage. Once group deletion is supported, this test should be
* updated to use the API instead.
*/
@Test
@Sandboxed
@IgnoreGroupInconsistencies
public void getAuditLogAfterDeletingASubgroup() throws Exception {
GroupInfo parentGroup = gApi.groups().create(name("parent-group")).get();
// Creates a subgroup and adds it to "parent-group" as a subgroup.
GroupInfo subgroup = gApi.groups().create(name("sub-group")).get();
gApi.groups().id(parentGroup.id).addGroups(subgroup.id);
// Deletes the subgroup.
deleteGroupRef(subgroup.id);
List<? extends GroupAuditEventInfo> auditEvents = gApi.groups().id(parentGroup.id).auditLog();
assertThat(auditEvents).hasSize(2);
// Verify the unavailable subgroup's name is null.
assertSubgroupAuditEvent(
auditEvents.get(0), GroupAuditEventInfo.Type.ADD_GROUP, admin.id(), null);
}
private void deleteGroupRef(String groupId) throws Exception {
AccountGroup.UUID uuid = AccountGroup.uuid(groupId);
try (Repository repo = repoManager.openRepository(allUsers)) {
RefUpdate ru = repo.updateRef(RefNames.refsGroups(uuid));
ru.setForceUpdate(true);
ru.setNewObjectId(ObjectId.zeroId());
assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
}
// Reindex the group.
gApi.groups().id(uuid.get()).index();
// Verify "sub-group" has been deleted.
assertThrows(ResourceNotFoundException.class, () -> gApi.groups().id(uuid.get()).get());
}
// 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 = Stream.of(groupOwner).map(u -> u.id().toString()).collect(toList());
in.visibleToAll = true;
GroupInfo group = gApi.groups().create(in).get();
// admin can reindex any group
requestScopeOperations.setApiUser(admin.id());
gApi.groups().id(group.id).index();
// group owner can reindex own group (group is owned by itself)
requestScopeOperations.setApiUser(groupOwner.id());
gApi.groups().id(group.id).index();
// user cannot reindex any group
requestScopeOperations.setApiUser(user.id());
AuthException thrown =
assertThrows(AuthException.class, () -> gApi.groups().id(group.id).index());
assertThat(thrown).hasMessageThat().contains("not allowed to index group");
}
@Test
public void pushToGroupBranchIsRejectedForAllUsersRepo() throws Exception {
assertPushToGroupBranch(
allUsers, RefNames.refsGroups(adminGroupUuid()), "group update not allowed");
}
@Test
public void pushToDeletedGroupBranchIsRejectedForAllUsersRepo() throws Exception {
// refs/deleted-groups is only visible with ACCESS_DATABASE
projectOperations
.allProjectsForUpdate()
.add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
.update();
String groupRef =
RefNames.refsDeletedGroups(AccountGroup.uuid(gApi.groups().create(name("foo")).get().id));
createBranch(allUsers, groupRef);
assertPushToGroupBranch(allUsers, groupRef, "group update not allowed");
}
@Test
public void pushToGroupNamesBranchIsRejectedForAllUsersRepo() throws Exception {
// refs/meta/group-names isn't usually available for fetch, so grant ACCESS_DATABASE
projectOperations
.allProjectsForUpdate()
.add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
.update();
assertPushToGroupBranch(allUsers, RefNames.REFS_GROUPNAMES, "group update not allowed");
}
@Test
public void pushToGroupsBranchForNonAllUsersRepo() throws Exception {
assertCreateGroupBranch(project);
String groupRef =
RefNames.refsGroups(AccountGroup.uuid(gApi.groups().create(name("foo")).get().id));
createBranch(project, groupRef);
assertPushToGroupBranch(project, groupRef, null);
}
@Test
public void pushToDeletedGroupsBranchForNonAllUsersRepo() throws Exception {
assertCreateGroupBranch(project);
String groupRef =
RefNames.refsDeletedGroups(AccountGroup.uuid(gApi.groups().create(name("foo")).get().id));
createBranch(project, groupRef);
assertPushToGroupBranch(project, groupRef, null);
}
@Test
public void pushToGroupNamesBranchForNonAllUsersRepo() throws Exception {
createBranch(project, RefNames.REFS_GROUPNAMES);
assertPushToGroupBranch(project, RefNames.REFS_GROUPNAMES, null);
}
private void assertPushToGroupBranch(
Project.NameKey project, String groupRefName, String expectedErrorOnUpdate) throws Exception {
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.CREATE).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
.add(allow(Permission.PUSH).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
.add(
allow(Permission.CREATE)
.ref(RefNames.REFS_DELETED_GROUPS + "*")
.group(REGISTERED_USERS))
.add(allow(Permission.PUSH).ref(RefNames.REFS_DELETED_GROUPS + "*").group(REGISTERED_USERS))
.add(allow(Permission.PUSH).ref(RefNames.REFS_GROUPNAMES).group(REGISTERED_USERS))
.update();
TestRepository<InMemoryRepository> repo = cloneProject(project);
// update existing branch
fetch(repo, groupRefName + ":groupRef");
repo.reset("groupRef");
PushOneCommit.Result r =
pushFactory
.create(admin.newIdent(), repo, "Update group", "arbitraryFile.txt", "some content")
.to(groupRefName);
if (expectedErrorOnUpdate != null) {
r.assertErrorStatus(expectedErrorOnUpdate);
} else {
r.assertOkStatus();
}
}
private void assertCreateGroupBranch(Project.NameKey project) throws Exception {
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.CREATE).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
.add(allow(Permission.PUSH).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
.update();
TestRepository<InMemoryRepository> repo = cloneProject(project);
PushOneCommit.Result r =
pushFactory
.create(admin.newIdent(), repo, "Update group", "arbitraryFile.txt", "some content")
.setParents(ImmutableList.of())
.to(RefNames.REFS_GROUPS + name("bar"));
r.assertOkStatus();
}
@Test
public void pushToGroupBranchForReviewForAllUsersRepoIsRejectedOnSubmit() throws Throwable {
pushToGroupBranchForReviewAndSubmit(
allUsers, RefNames.refsGroups(adminGroupUuid()), "group update not allowed");
}
@Test
public void pushToGroupBranchForReviewForNonAllUsersRepoAndSubmit() throws Throwable {
String groupRef = RefNames.refsGroups(adminGroupUuid());
createBranch(project, groupRef);
pushToGroupBranchForReviewAndSubmit(project, groupRef, null);
}
@Test
public void pushCustomInheritanceForAllUsersFails() throws Exception {
TestRepository<InMemoryRepository> repo = cloneProject(allUsers);
GitUtil.fetch(repo, RefNames.REFS_CONFIG + ":" + RefNames.REFS_CONFIG);
repo.reset(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(admin.newIdent(), repo, "Subject", "project.config", config)
.to(RefNames.REFS_CONFIG);
r.assertErrorStatus("invalid project configuration");
r.assertMessage("All-Users must inherit from All-Projects");
}
@Test
public void cannotCreateGroupBranch() throws Exception {
testCannotCreateGroupBranch(
RefNames.REFS_GROUPS + "*", RefNames.refsGroups(AccountGroup.uuid(name("foo"))));
}
@Test
public void cannotCreateDeletedGroupBranch() throws Exception {
testCannotCreateGroupBranch(
RefNames.REFS_DELETED_GROUPS + "*",
RefNames.refsDeletedGroups(AccountGroup.uuid(name("foo"))));
}
@Test
@IgnoreGroupInconsistencies
public void cannotCreateGroupNamesBranch() throws Exception {
// Use ProjectResetter to restore the group names ref
try (ProjectResetter resetter =
projectResetter
.builder()
.build(new ProjectResetter.Config().reset(allUsers, RefNames.REFS_GROUPNAMES))) {
// Manually delete group names ref
try (Repository repo = repoManager.openRepository(allUsers);
RevWalk rw = new RevWalk(repo)) {
RevCommit commit = rw.parseCommit(repo.exactRef(RefNames.REFS_GROUPNAMES).getObjectId());
RefUpdate updateRef = repo.updateRef(RefNames.REFS_GROUPNAMES);
updateRef.setExpectedOldObjectId(commit.toObjectId());
updateRef.setNewObjectId(ObjectId.zeroId());
updateRef.setForceUpdate(true);
assertThat(updateRef.delete()).isEqualTo(RefUpdate.Result.FORCED);
}
// refs/meta/group-names is only visible with ACCESS_DATABASE
projectOperations
.allProjectsForUpdate()
.add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
.update();
testCannotCreateGroupBranch(RefNames.REFS_GROUPNAMES, RefNames.REFS_GROUPNAMES);
}
}
private void testCannotCreateGroupBranch(String refPattern, String groupRef) throws Exception {
projectOperations
.project(allUsers)
.forUpdate()
.add(allow(Permission.CREATE).ref(refPattern).group(adminGroupUuid()))
.add(allow(Permission.PUSH).ref(refPattern).group(adminGroupUuid()))
.update();
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
PushOneCommit.Result r = pushFactory.create(admin.newIdent(), 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
public void cannotDeleteGroupBranch() throws Exception {
testCannotDeleteGroupBranch(RefNames.REFS_GROUPS + "*", RefNames.refsGroups(adminGroupUuid()));
}
@Test
public void cannotDeleteDeletedGroupBranch() throws Exception {
// refs/deleted-groups is only visible with ACCESS_DATABASE
projectOperations
.allProjectsForUpdate()
.add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
.update();
String groupRef = RefNames.refsDeletedGroups(AccountGroup.uuid(name("foo")));
createBranch(allUsers, groupRef);
testCannotDeleteGroupBranch(RefNames.REFS_DELETED_GROUPS + "*", groupRef);
}
@Test
public void cannotDeleteGroupNamesBranch() throws Exception {
// refs/meta/group-names is only visible with ACCESS_DATABASE
projectOperations
.allProjectsForUpdate()
.add(allowCapability(GlobalCapability.ACCESS_DATABASE).group(REGISTERED_USERS))
.update();
testCannotDeleteGroupBranch(RefNames.REFS_GROUPNAMES, RefNames.REFS_GROUPNAMES);
}
private void testCannotDeleteGroupBranch(String refPattern, String groupRef) throws Exception {
projectOperations
.project(allUsers)
.forUpdate()
.add(allow(Permission.DELETE).ref(refPattern).group(REGISTERED_USERS).force(true))
.update();
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 {
assertPermissions(
allUsers, groupRef(REGISTERED_USERS), RefNames.REFS_GROUPS + "*", true, Permission.READ);
}
@Test
@IgnoreGroupInconsistencies
public void stalenessChecker() throws Exception {
// Newly created group is not stale
GroupInfo groupInfo = gApi.groups().create(name("foo")).get();
AccountGroup.UUID groupUuid = AccountGroup.uuid(groupInfo.id);
assertThat(stalenessChecker.check(groupUuid).isStale()).isFalse();
// Manual update makes index document stale
String groupRef = RefNames.refsGroups(groupUuid);
try (Repository repo = repoManager.openRepository(allUsers);
RevWalk rw = new RevWalk(repo)) {
RevCommit commit = rw.parseCommit(repo.exactRef(groupRef).getObjectId());
ObjectId emptyCommit = createCommit(repo, commit.getFullMessage(), commit.getTree());
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);
}
@Test
@Sandboxed
public void groupsOfUserCanBeListedInSlaveMode() throws Exception {
GroupInput groupInput = new GroupInput();
groupInput.name = name("contributors");
groupInput.members = ImmutableList.of(user.username());
gApi.groups().create(groupInput).get();
restartAsSlave();
requestScopeOperations.setApiUser(user.id());
List<GroupInfo> groups = gApi.groups().list().withUser(user.username()).get();
ImmutableList<String> groupNames =
groups.stream().map(group -> group.name).collect(toImmutableList());
assertThat(groupNames).contains(groupInput.name);
}
@Test
@Sandboxed
@GerritConfig(name = "index.scheduledIndexer.enabled", value = "false")
@GerritConfig(name = "index.autoReindexIfStale", value = "false")
@IgnoreGroupInconsistencies
public void reindexGroupsInSlaveMode() throws Exception {
List<AccountGroup.UUID> expectedGroups =
groups.getAllGroupReferences().map(GroupReference::getUUID).collect(toList());
assertThat(expectedGroups.size()).isAtLeast(2);
// Restart the server as slave, on startup of the slave all groups are indexed.
restartAsSlave();
GroupIndexedCounter groupIndexedCounter = new GroupIndexedCounter();
try (Registration registration = extensionRegistry.newRegistration().add(groupIndexedCounter)) {
// Running the reindexer right after startup should not need to reindex any group since
// reindexing was already done on startup.
slaveGroupIndexer.run();
groupIndexedCounter.assertNoReindex();
// Create a group without updating the cache or index,
// then run the reindexer -> only the new group is reindexed.
String groupName = "foo";
AccountGroup.UUID groupUuid = AccountGroup.uuid(groupName + "-UUID");
groupsUpdate.createGroupInNoteDb(
InternalGroupCreation.builder()
.setGroupUUID(groupUuid)
.setNameKey(AccountGroup.nameKey(groupName))
.setId(AccountGroup.id(seq.nextGroupId()))
.build(),
GroupDelta.builder().build());
slaveGroupIndexer.run();
groupIndexedCounter.assertReindexOf(groupUuid);
// Update a group without updating the cache or index,
// then run the reindexer -> only the updated group is reindexed.
groupsUpdate.updateGroupInNoteDb(
groupUuid, GroupDelta.builder().setDescription("bar").build());
slaveGroupIndexer.run();
groupIndexedCounter.assertReindexOf(groupUuid);
// Delete a group without updating the cache or index,
// then run the reindexer -> only the deleted group is reindexed.
try (Repository repo = repoManager.openRepository(allUsers)) {
RefUpdate u = repo.updateRef(RefNames.refsGroups(groupUuid));
u.setForceUpdate(true);
assertThat(u.delete()).isEqualTo(RefUpdate.Result.FORCED);
}
slaveGroupIndexer.run();
groupIndexedCounter.assertReindexOf(groupUuid);
}
}
@Test
@Sandboxed
@GerritConfig(name = "index.scheduledIndexer.runOnStartup", value = "false")
@GerritConfig(name = "index.scheduledIndexer.enabled", value = "false")
@GerritConfig(name = "index.autoReindexIfStale", value = "false")
@IgnoreGroupInconsistencies
public void disabledReindexGroupsOnStartupSlaveMode() throws Exception {
List<AccountGroup.UUID> expectedGroups =
groups.getAllGroupReferences().map(GroupReference::getUUID).collect(toList());
assertThat(expectedGroups.size()).isAtLeast(2);
restartAsSlave();
GroupIndexedCounter groupIndexedCounter = new GroupIndexedCounter();
try (Registration registration = extensionRegistry.newRegistration().add(groupIndexedCounter)) {
// No group indexing happened on startup. All groups should be reindexed now.
slaveGroupIndexer.run();
groupIndexedCounter.assertReindexOf(expectedGroups);
}
}
private static Correspondence<AccountInfo, String> getAccountToUsernameCorrespondence() {
return NullAwareCorrespondence.transforming(
accountInfo -> accountInfo.username, "has username");
}
private void assertStaleGroupAndReindex(AccountGroup.UUID groupUuid) throws IOException {
// Evict group from cache to be sure that we use the index state for staleness checks.
groupCache.evict(groupUuid);
assertThat(stalenessChecker.check(groupUuid).isStale()).isTrue();
// Reindex fixes staleness
groupIndexer.index(groupUuid);
assertThat(stalenessChecker.check(groupUuid).isStale()).isFalse();
}
private void pushToGroupBranchForReviewAndSubmit(
Project.NameKey project, String groupRef, String expectedError) throws Throwable {
projectOperations
.project(project)
.forUpdate()
.add(
allowLabel(LabelId.CODE_REVIEW)
.ref(RefNames.REFS_GROUPS + "*")
.group(REGISTERED_USERS)
.range(-2, 2))
.add(allow(Permission.SUBMIT).ref(RefNames.REFS_GROUPS + "*").group(REGISTERED_USERS))
.update();
TestRepository<InMemoryRepository> repo = cloneProject(project);
fetch(repo, groupRef + ":groupRef");
repo.reset("groupRef");
PushOneCommit.Result r =
pushFactory
.create(admin.newIdent(), repo, "Update group config", "group.config", "some content")
.to(MagicBranch.NEW_CHANGE + groupRef);
r.assertOkStatus();
assertThat(r.getChange().change().getDest().branch()).isEqualTo(groupRef);
gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
ThrowingRunnable submit = () -> gApi.changes().id(r.getChangeId()).current().submit();
if (expectedError != null) {
Throwable thrown = assertThrows(ResourceConflictException.class, submit);
assertThat(thrown).hasMessageThat().contains("group update not allowed");
} else {
submit.run();
}
}
private void createBranch(Project.NameKey project, String ref) throws IOException {
try (Repository r = repoManager.openRepository(project);
ObjectInserter oi = r.newObjectInserter();
RevWalk rw = new RevWalk(r)) {
ObjectId emptyCommit = createCommit(r, "Test change");
RefUpdate updateRef = r.updateRef(ref);
updateRef.setExpectedOldObjectId(ObjectId.zeroId());
updateRef.setNewObjectId(emptyCommit);
assertThat(updateRef.update(rw)).isEqualTo(RefUpdate.Result.NEW);
}
}
private ObjectId createCommit(Repository repo, String commitMessage) throws IOException {
return createCommit(repo, commitMessage, null);
}
private ObjectId createCommit(Repository repo, String commitMessage, @Nullable ObjectId treeId)
throws IOException {
try (ObjectInserter oi = repo.newObjectInserter()) {
if (treeId == null) {
treeId = oi.insert(Constants.OBJ_TREE, new byte[] {});
}
PersonIdent ident = new PersonIdent(serverIdent.get(), TimeUtil.nowTs());
CommitBuilder cb = new CommitBuilder();
cb.setTreeId(treeId);
cb.setCommitter(ident);
cb.setAuthor(ident);
cb.setMessage(commitMessage);
ObjectId commit = oi.insert(cb);
oi.flush();
return commit;
}
}
private void assertMemberAuditEvent(
GroupAuditEventInfo info,
GroupAuditEventInfo.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 assertSubgroupAuditEvent(
GroupAuditEventInfo info,
GroupAuditEventInfo.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).toArray(new String[0]));
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 static void assertIncludes(List<GroupInfo> includes, String... expectedNames) {
List<String> names = includes.stream().map(i -> i.name).collect(toImmutableList());
assertThat(names).containsExactlyElementsIn(Arrays.asList(expectedNames));
assertThat(names).isInOrder();
}
private void assertBadRequest(ListRequest req) throws Exception {
assertThrows(BadRequestException.class, () -> req.get());
}
@Target({METHOD})
@Retention(RUNTIME)
private @interface IgnoreGroupInconsistencies {}
/** Checks if a group is indexed the correct number of times. */
private static class GroupIndexedCounter implements GroupIndexedListener {
private final AtomicLongMap<String> countsByGroup = AtomicLongMap.create();
@Override
public void onGroupIndexed(String uuid) {
countsByGroup.incrementAndGet(uuid);
}
void clear() {
countsByGroup.clear();
}
void assertReindexOf(AccountGroup.UUID groupUuid) {
assertReindexOf(ImmutableList.of(groupUuid));
}
void assertReindexOf(List<AccountGroup.UUID> groupUuids) {
Map<String, Long> expected = groupUuids.stream().collect(toMap(u -> u.get(), u -> 1L));
assertThat(countsByGroup.asMap()).containsExactlyEntriesIn(expected);
clear();
}
void assertNoReindex() {
assertThat(countsByGroup.asMap()).isEmpty();
}
}
}