Merge "Fix nested group expansion"
This commit is contained in:
@@ -165,7 +165,8 @@ class GroupAdminServiceImpl extends BaseServiceImplementation implements
|
||||
public GroupDetail run(ReviewDb db) throws OrmException, Failure,
|
||||
NoSuchGroupException {
|
||||
final GroupControl control = groupControlFactory.validateFor(groupId);
|
||||
if (groupCache.get(groupId).getType() != AccountGroup.Type.INTERNAL) {
|
||||
AccountGroup group = groupCache.get(groupId);
|
||||
if (group.getType() != AccountGroup.Type.INTERNAL) {
|
||||
throw new Failure(new NameAlreadyUsedException());
|
||||
}
|
||||
|
||||
@@ -186,7 +187,8 @@ class GroupAdminServiceImpl extends BaseServiceImplementation implements
|
||||
Collections.singleton(new AccountGroupIncludeByUuidAudit(m,
|
||||
getAccountId())));
|
||||
db.accountGroupIncludesByUuid().insert(Collections.singleton(m));
|
||||
groupIncludeCache.evictInclude(incGroupUUID);
|
||||
groupIncludeCache.evictMemberIn(incGroupUUID);
|
||||
groupIncludeCache.evictMembersOf(group.getGroupUUID());
|
||||
}
|
||||
|
||||
return groupDetailFactory.create(groupId).call();
|
||||
@@ -201,7 +203,8 @@ class GroupAdminServiceImpl extends BaseServiceImplementation implements
|
||||
public VoidResult run(final ReviewDb db) throws OrmException,
|
||||
NoSuchGroupException, Failure {
|
||||
final GroupControl control = groupControlFactory.validateFor(groupId);
|
||||
if (groupCache.get(groupId).getType() != AccountGroup.Type.INTERNAL) {
|
||||
AccountGroup group = groupCache.get(groupId);
|
||||
if (group.getType() != AccountGroup.Type.INTERNAL) {
|
||||
throw new Failure(new NameAlreadyUsedException());
|
||||
}
|
||||
|
||||
@@ -241,7 +244,10 @@ class GroupAdminServiceImpl extends BaseServiceImplementation implements
|
||||
}
|
||||
}
|
||||
for (AccountGroup.UUID uuid : groupsToEvict) {
|
||||
groupIncludeCache.evictInclude(uuid);
|
||||
groupIncludeCache.evictMemberIn(uuid);
|
||||
}
|
||||
if (!groupsToEvict.isEmpty()) {
|
||||
groupIncludeCache.evictMembersOf(group.getGroupUUID());
|
||||
}
|
||||
return VoidResult.INSTANCE;
|
||||
}
|
||||
|
@@ -16,12 +16,17 @@ package com.google.gerrit.server.account;
|
||||
|
||||
import com.google.gerrit.reviewdb.client.AccountGroup;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
|
||||
/** Tracks group inclusions in memory for efficient access. */
|
||||
public interface GroupIncludeCache {
|
||||
public Collection<AccountGroup.UUID> getByInclude(AccountGroup.UUID groupId);
|
||||
/** @return groups directly a member of the passed group. */
|
||||
public Set<AccountGroup.UUID> membersOf(AccountGroup.UUID group);
|
||||
|
||||
public void evictInclude(AccountGroup.UUID groupId);
|
||||
/** @return any groups the passed group belongs to. */
|
||||
public Set<AccountGroup.UUID> memberIn(AccountGroup.UUID groupId);
|
||||
|
||||
public void evictMembersOf(AccountGroup.UUID groupId);
|
||||
public void evictMemberIn(AccountGroup.UUID groupId);
|
||||
}
|
||||
|
||||
|
@@ -32,7 +32,6 @@ import com.google.inject.name.Named;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -44,6 +43,7 @@ public class GroupIncludeCacheImpl implements GroupIncludeCache {
|
||||
private static final Logger log = LoggerFactory
|
||||
.getLogger(GroupIncludeCacheImpl.class);
|
||||
private static final String BYINCLUDE_NAME = "groups_byinclude";
|
||||
private static final String MEMBERS_NAME = "groups_members";
|
||||
|
||||
public static Module module() {
|
||||
return new CacheModule() {
|
||||
@@ -52,7 +52,12 @@ public class GroupIncludeCacheImpl implements GroupIncludeCache {
|
||||
cache(BYINCLUDE_NAME,
|
||||
AccountGroup.UUID.class,
|
||||
new TypeLiteral<Set<AccountGroup.UUID>>() {})
|
||||
.loader(ByIncludeLoader.class);
|
||||
.loader(MemberInLoader.class);
|
||||
|
||||
cache(MEMBERS_NAME,
|
||||
AccountGroup.UUID.class,
|
||||
new TypeLiteral<Set<AccountGroup.UUID>>() {})
|
||||
.loader(MembersOfLoader.class);
|
||||
|
||||
bind(GroupIncludeCacheImpl.class);
|
||||
bind(GroupIncludeCache.class).to(GroupIncludeCacheImpl.class);
|
||||
@@ -60,35 +65,83 @@ public class GroupIncludeCacheImpl implements GroupIncludeCache {
|
||||
};
|
||||
}
|
||||
|
||||
private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> byInclude;
|
||||
private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> membersOf;
|
||||
private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> memberIn;
|
||||
|
||||
@Inject
|
||||
GroupIncludeCacheImpl(
|
||||
@Named(BYINCLUDE_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> byInclude) {
|
||||
this.byInclude = byInclude;
|
||||
@Named(MEMBERS_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> membersOf,
|
||||
@Named(BYINCLUDE_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> memberIn) {
|
||||
this.membersOf = membersOf;
|
||||
this.memberIn = memberIn;
|
||||
}
|
||||
|
||||
public Collection<AccountGroup.UUID> getByInclude(AccountGroup.UUID groupId) {
|
||||
public Set<AccountGroup.UUID> membersOf(AccountGroup.UUID groupId) {
|
||||
try {
|
||||
return byInclude.get(groupId);
|
||||
return membersOf.get(groupId);
|
||||
} catch (ExecutionException e) {
|
||||
log.warn("Cannot load members of group", e);
|
||||
return Collections.emptySet();
|
||||
}
|
||||
}
|
||||
|
||||
public Set<AccountGroup.UUID> memberIn(AccountGroup.UUID groupId) {
|
||||
try {
|
||||
return memberIn.get(groupId);
|
||||
} catch (ExecutionException e) {
|
||||
log.warn("Cannot load included groups", e);
|
||||
return Collections.emptySet();
|
||||
}
|
||||
}
|
||||
|
||||
public void evictInclude(AccountGroup.UUID groupId) {
|
||||
public void evictMembersOf(AccountGroup.UUID groupId) {
|
||||
if (groupId != null) {
|
||||
byInclude.invalidate(groupId);
|
||||
membersOf.invalidate(groupId);
|
||||
}
|
||||
}
|
||||
|
||||
static class ByIncludeLoader extends
|
||||
public void evictMemberIn(AccountGroup.UUID groupId) {
|
||||
if (groupId != null) {
|
||||
memberIn.invalidate(groupId);
|
||||
}
|
||||
}
|
||||
|
||||
static class MembersOfLoader extends
|
||||
CacheLoader<AccountGroup.UUID, Set<AccountGroup.UUID>> {
|
||||
private final SchemaFactory<ReviewDb> schema;
|
||||
|
||||
@Inject
|
||||
ByIncludeLoader(final SchemaFactory<ReviewDb> sf) {
|
||||
MembersOfLoader(final SchemaFactory<ReviewDb> sf) {
|
||||
schema = sf;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<AccountGroup.UUID> load(AccountGroup.UUID key) throws Exception {
|
||||
final ReviewDb db = schema.open();
|
||||
try {
|
||||
List<AccountGroup> group = db.accountGroups().byUUID(key).toList();
|
||||
if (group.size() != 1) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
Set<AccountGroup.UUID> ids = Sets.newHashSet();
|
||||
for (AccountGroupIncludeByUuid agi : db.accountGroupIncludesByUuid()
|
||||
.byGroup(group.get(0).getId())) {
|
||||
ids.add(agi.getIncludeUUID());
|
||||
}
|
||||
return ImmutableSet.copyOf(ids);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class MemberInLoader extends
|
||||
CacheLoader<AccountGroup.UUID, Set<AccountGroup.UUID>> {
|
||||
private final SchemaFactory<ReviewDb> schema;
|
||||
|
||||
@Inject
|
||||
MemberInLoader(final SchemaFactory<ReviewDb> sf) {
|
||||
schema = sf;
|
||||
}
|
||||
|
||||
|
@@ -14,36 +14,49 @@
|
||||
|
||||
package com.google.gerrit.server.account;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.gerrit.reviewdb.client.AccountGroup;
|
||||
import com.google.gerrit.server.IdentifiedUser;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.assistedinject.Assisted;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Queue;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Creates a GroupMembership checker for the internal group system, which
|
||||
* starts with the seed groups and includes all child groups.
|
||||
* Group membership checker for the internal group system.
|
||||
* <p>
|
||||
* Groups the user is directly a member of are pulled from the in-memory
|
||||
* AccountCache by way of the IdentifiedUser. Transitive group memberhips are
|
||||
* resolved on demand starting from the requested group and looking for a path
|
||||
* to a group the user is a member of. Other group backends are supported by
|
||||
* recursively invoking the universal GroupMembership.
|
||||
*/
|
||||
public class IncludingGroupMembership implements GroupMembership {
|
||||
public interface Factory {
|
||||
IncludingGroupMembership create(Iterable<AccountGroup.UUID> groupIds);
|
||||
IncludingGroupMembership create(IdentifiedUser user);
|
||||
}
|
||||
|
||||
private final GroupIncludeCache groupIncludeCache;
|
||||
private final Set<AccountGroup.UUID> includes;
|
||||
private final Queue<AccountGroup.UUID> groupQueue;
|
||||
private final GroupIncludeCache includeCache;
|
||||
private final IdentifiedUser user;
|
||||
private final Map<AccountGroup.UUID, Boolean> memberOf;
|
||||
private Set<AccountGroup.UUID> knownGroups;
|
||||
|
||||
@Inject
|
||||
IncludingGroupMembership(
|
||||
GroupIncludeCache groupIncludeCache,
|
||||
@Assisted Iterable<AccountGroup.UUID> seedGroups) {
|
||||
this.groupIncludeCache = groupIncludeCache;
|
||||
this.includes = Sets.newHashSet(seedGroups);
|
||||
this.groupQueue = Lists.newLinkedList(seedGroups);
|
||||
IncludingGroupMembership(GroupIncludeCache includeCache,
|
||||
@Assisted IdentifiedUser user) {
|
||||
this.includeCache = includeCache;
|
||||
this.user = user;
|
||||
|
||||
Set<AccountGroup.UUID> groups = user.state().getInternalGroups();
|
||||
memberOf = Maps.newHashMapWithExpectedSize(groups.size());
|
||||
for (AccountGroup.UUID g : groups) {
|
||||
memberOf.put(g, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -51,23 +64,40 @@ public class IncludingGroupMembership implements GroupMembership {
|
||||
if (id == null) {
|
||||
return false;
|
||||
}
|
||||
if (includes.contains(id)) {
|
||||
return true;
|
||||
}
|
||||
return findIncludedGroup(Collections.singleton(id));
|
||||
|
||||
Boolean b = memberOf.get(id);
|
||||
return b != null ? b : containsAnyOf(ImmutableSet.of(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsAnyOf(Iterable<AccountGroup.UUID> ids) {
|
||||
Set<AccountGroup.UUID> query = Sets.newHashSet();
|
||||
for (AccountGroup.UUID groupId : ids) {
|
||||
if (includes.contains(groupId)) {
|
||||
public boolean containsAnyOf(Iterable<AccountGroup.UUID> queryIds) {
|
||||
// Prefer lookup of a cached result over expanding includes.
|
||||
boolean tryExpanding = false;
|
||||
for (AccountGroup.UUID id : queryIds) {
|
||||
Boolean b = memberOf.get(id);
|
||||
if (b == null) {
|
||||
tryExpanding = true;
|
||||
} else if (b) {
|
||||
return true;
|
||||
}
|
||||
query.add(groupId);
|
||||
}
|
||||
|
||||
return findIncludedGroup(query);
|
||||
if (tryExpanding) {
|
||||
for (AccountGroup.UUID id : queryIds) {
|
||||
if (memberOf.containsKey(id)) {
|
||||
// Membership was earlier proven to be false.
|
||||
continue;
|
||||
}
|
||||
|
||||
memberOf.put(id, false);
|
||||
if (search(includeCache.membersOf(id))) {
|
||||
memberOf.put(id, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -81,25 +111,31 @@ public class IncludingGroupMembership implements GroupMembership {
|
||||
return r;
|
||||
}
|
||||
|
||||
private boolean findIncludedGroup(Set<AccountGroup.UUID> query) {
|
||||
boolean found = false;
|
||||
while (!found && !groupQueue.isEmpty()) {
|
||||
AccountGroup.UUID id = groupQueue.remove();
|
||||
private boolean search(Set<AccountGroup.UUID> ids) {
|
||||
return user.getEffectiveGroups().containsAnyOf(ids);
|
||||
}
|
||||
|
||||
for (final AccountGroup.UUID groupId : groupIncludeCache.getByInclude(id)) {
|
||||
if (includes.add(groupId)) {
|
||||
groupQueue.add(groupId);
|
||||
found |= query.contains(groupId);
|
||||
private ImmutableSet<AccountGroup.UUID> computeKnownGroups() {
|
||||
Set<AccountGroup.UUID> direct = user.state().getInternalGroups();
|
||||
Set<AccountGroup.UUID> r = Sets.newHashSet(direct);
|
||||
List<AccountGroup.UUID> q = Lists.newArrayList(r);
|
||||
while (!q.isEmpty()) {
|
||||
AccountGroup.UUID id = q.remove(q.size() - 1);
|
||||
for (AccountGroup.UUID g : includeCache.memberIn(id)) {
|
||||
if (r.add(g)) {
|
||||
q.add(g);
|
||||
memberOf.put(g, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
return ImmutableSet.copyOf(r);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<AccountGroup.UUID> getKnownGroups() {
|
||||
findIncludedGroup(Collections.<AccountGroup.UUID>emptySet()); // find all
|
||||
return Sets.newHashSet(includes);
|
||||
if (knownGroups == null) {
|
||||
knownGroups = computeKnownGroups();
|
||||
}
|
||||
return knownGroups;
|
||||
}
|
||||
}
|
||||
|
@@ -28,9 +28,7 @@ import com.google.inject.Singleton;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Implementation of GroupBackend for the internal group system.
|
||||
*/
|
||||
/** Implementation of GroupBackend for the internal group system. */
|
||||
@Singleton
|
||||
public class InternalGroupBackend implements GroupBackend {
|
||||
private static final Function<AccountGroup, GroupReference> ACT_GROUP_TO_GROUP_REF =
|
||||
@@ -45,7 +43,6 @@ public class InternalGroupBackend implements GroupBackend {
|
||||
private final GroupCache groupCache;
|
||||
private final IncludingGroupMembership.Factory groupMembershipFactory;
|
||||
|
||||
|
||||
@Inject
|
||||
InternalGroupBackend(GroupControl.Factory groupControlFactory,
|
||||
GroupCache groupCache,
|
||||
@@ -89,6 +86,6 @@ public class InternalGroupBackend implements GroupBackend {
|
||||
|
||||
@Override
|
||||
public GroupMembership membershipsOf(IdentifiedUser user) {
|
||||
return groupMembershipFactory.create(user.state().getInternalGroups());
|
||||
return groupMembershipFactory.create(user);
|
||||
}
|
||||
}
|
||||
|
@@ -128,6 +128,7 @@ public class PerformCreateGroup {
|
||||
|
||||
if (initialGroups != null) {
|
||||
addGroups(groupId, initialGroups);
|
||||
groupIncludeCache.evictMembersOf(uuid);
|
||||
}
|
||||
|
||||
groupCache.onCreateGroup(nameKey);
|
||||
@@ -177,7 +178,7 @@ public class PerformCreateGroup {
|
||||
db.accountGroupIncludesByUuidAudit().insert(includesAudit);
|
||||
|
||||
for (AccountGroup.UUID uuid : groups) {
|
||||
groupIncludeCache.evictInclude(uuid);
|
||||
groupIncludeCache.evictMemberIn(uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user