Cache effective capabilities to improve lookup performance

Instead of scanning through the AccessSection from All-Projects on
every request, cache most of the lookup work inside of a (mostly)
singleton CapabilityCollection available through the All-Projects
ProjectState.

This allows us to reuse the indexing of which groups have the
Administrate Server capability, making it faster to conclude if
the current user is an administrator.  Since nearly all access
control decisions wind up with an "OR is administrator" this is
an important check to optimize since many of those checks return
false without user-level errors.

Critically, the lookup for queryLimit is improved by only doing
a lookup for the capability requested, rather than all of them.
Web UI navigations need to locate the user's query limit to find the
page size that can be returned to the web UI, but typically these
only need to perform that one capability test and do not need the
other available capabilities.  Deferring lookup of an effective
capability to always be on-demand improves this one very common case.

Change-Id: I2b744f5a6b4adfc017b33072c7f644692383d855
This commit is contained in:
Shawn O. Pearce
2011-06-22 12:25:21 -07:00
parent bc0916318c
commit 106796c589
3 changed files with 180 additions and 75 deletions

View File

@@ -0,0 +1,108 @@
// Copyright (C) 2011 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.account;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.reviewdb.AccountGroup;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Caches active {@link GlobalCapability} set for a site. */
public class CapabilityCollection {
private final Map<String, List<PermissionRule>> permissions;
public final List<PermissionRule> administrateServer;
public final List<PermissionRule> priority;
public final List<PermissionRule> queryLimit;
public CapabilityCollection(AccessSection section) {
if (section == null) {
section = new AccessSection(AccessSection.GLOBAL_CAPABILITIES);
}
Map<String, List<PermissionRule>> tmp =
new HashMap<String, List<PermissionRule>>();
for (Permission permission : section.getPermissions()) {
for (PermissionRule rule : permission.getRules()) {
if (rule.getAction() != PermissionRule.Action.DENY) {
List<PermissionRule> r = tmp.get(permission.getName());
if (r == null) {
r = new ArrayList<PermissionRule>(2);
tmp.put(permission.getName(), r);
}
r.add(rule);
}
}
}
configureDefaults(tmp, section);
Map<String, List<PermissionRule>> res =
new HashMap<String, List<PermissionRule>>();
for (Map.Entry<String, List<PermissionRule>> e : tmp.entrySet()) {
List<PermissionRule> rules = e.getValue();
if (rules.size() == 1) {
res.put(e.getKey(), Collections.singletonList(rules.get(0)));
} else {
res.put(e.getKey(), Collections.unmodifiableList(
Arrays.asList(rules.toArray(new PermissionRule[rules.size()]))));
}
}
permissions = Collections.unmodifiableMap(res);
administrateServer = getPermission(GlobalCapability.ADMINISTRATE_SERVER);
priority = getPermission(GlobalCapability.PRIORITY);
queryLimit = getPermission(GlobalCapability.QUERY_LIMIT);
}
public List<PermissionRule> getPermission(String permissionName) {
List<PermissionRule> r = permissions.get(permissionName);
return r != null ? r : Collections.<PermissionRule> emptyList();
}
private static final GroupReference anonymous = new GroupReference(
AccountGroup.ANONYMOUS_USERS,
"Anonymous Users");
private static void configureDefaults(Map<String, List<PermissionRule>> out,
AccessSection section) {
configureDefault(out, section, GlobalCapability.QUERY_LIMIT, anonymous);
}
private static void configureDefault(Map<String, List<PermissionRule>> out,
AccessSection section, String capName, GroupReference group) {
if (doesNotDeclare(section, capName)) {
PermissionRange.WithDefaults range = GlobalCapability.getRange(capName);
if (range != null) {
PermissionRule rule = new PermissionRule(group);
rule.setRange(range.getDefaultMin(), range.getDefaultMax());
out.put(capName, Collections.singletonList(rule));
}
}
}
private static boolean doesNotDeclare(AccessSection section, String capName) {
return section.getPermission(capName) == null;
}
}

View File

@@ -14,10 +14,8 @@
package com.google.gerrit.server.account;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.reviewdb.AccountGroup;
@@ -25,7 +23,6 @@ import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.PeerDaemonUser;
import com.google.gerrit.server.git.QueueProvider;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -42,16 +39,17 @@ public class CapabilityControl {
public CapabilityControl create(CurrentUser user);
}
private final ProjectState state;
private final CapabilityCollection capabilities;
private final CurrentUser user;
private Map<String, List<PermissionRule>> permissions;
private final Map<String, List<PermissionRule>> effective;
private Boolean canAdministrateServer;
@Inject
CapabilityControl(ProjectCache projectCache, @Assisted CurrentUser currentUser) {
state = projectCache.getAllProjects();
capabilities = projectCache.getAllProjects().getCapabilityCollection();
user = currentUser;
effective = new HashMap<String, List<PermissionRule>>();
}
/** Identity of the user the control will compute for. */
@@ -63,7 +61,7 @@ public class CapabilityControl {
public boolean canAdministrateServer() {
if (canAdministrateServer == null) {
canAdministrateServer = user instanceof PeerDaemonUser
|| canPerform(GlobalCapability.ADMINISTRATE_SERVER);
|| matchAny(capabilities.administrateServer);
}
return canAdministrateServer;
}
@@ -131,9 +129,10 @@ public class CapabilityControl {
// the 'CI Servers' actually use the BATCH queue while everyone else gets
// to use the INTERACTIVE queue without additional grants.
//
List<PermissionRule> rules = access(GlobalCapability.PRIORITY);
Set<AccountGroup.UUID> groups = user.getEffectiveGroups();
boolean batch = false;
for (PermissionRule r : rules) {
for (PermissionRule r : capabilities.priority) {
if (match(groups, r)) {
switch (r.getAction()) {
case INTERACTIVE:
if (!isGenericGroup(r.getGroup())) {
@@ -146,6 +145,7 @@ public class CapabilityControl {
break;
}
}
}
if (batch) {
// If any of our groups matched to the BATCH queue, use it.
@@ -186,71 +186,53 @@ public class CapabilityControl {
/** Rules for the given permission, or the empty list. */
private List<PermissionRule> access(String permissionName) {
List<PermissionRule> r = permissions().get(permissionName);
return r != null ? r : Collections.<PermissionRule> emptyList();
List<PermissionRule> rules = effective.get(permissionName);
if (rules != null) {
return rules;
}
/** All rules that pertain to this user. */
private Map<String, List<PermissionRule>> permissions() {
if (permissions == null) {
permissions = indexPermissions();
}
return permissions;
rules = capabilities.getPermission(permissionName);
if (rules.isEmpty()) {
effective.put(permissionName, rules);
return rules;
}
private Map<String, List<PermissionRule>> indexPermissions() {
Map<String, List<PermissionRule>> res =
new HashMap<String, List<PermissionRule>>();
AccessSection section = state.getConfig()
.getAccessSection(AccessSection.GLOBAL_CAPABILITIES);
if (section == null) {
section = new AccessSection(AccessSection.GLOBAL_CAPABILITIES);
Set<AccountGroup.UUID> groups = user.getEffectiveGroups();
if (rules.size() == 1) {
if (!match(groups, rules.get(0))) {
rules = Collections.emptyList();
}
effective.put(permissionName, rules);
return rules;
}
for (Permission permission : section.getPermissions()) {
for (PermissionRule rule : permission.getRules()) {
if (matchGroup(rule.getGroup().getUUID())) {
if (rule.getAction() != PermissionRule.Action.DENY) {
List<PermissionRule> r = res.get(permission.getName());
if (r == null) {
r = new ArrayList<PermissionRule>(2);
res.put(permission.getName(), r);
}
r.add(rule);
}
}
List<PermissionRule> mine = new ArrayList<PermissionRule>(rules.size());
for (PermissionRule rule : rules) {
if (match(groups, rule)) {
mine.add(rule);
}
}
configureDefaults(res, section);
return res;
if (mine.isEmpty()) {
mine = Collections.emptyList();
}
effective.put(permissionName, mine);
return mine;
}
private boolean matchGroup(AccountGroup.UUID uuid) {
Set<AccountGroup.UUID> userGroups = getCurrentUser().getEffectiveGroups();
return userGroups.contains(uuid);
private boolean matchAny(List<PermissionRule> rules) {
Set<AccountGroup.UUID> groups = user.getEffectiveGroups();
for (PermissionRule rule : rules) {
if (match(groups, rule)) {
return true;
}
}
return false;
}
private static final GroupReference anonymous = new GroupReference(
AccountGroup.ANONYMOUS_USERS,
"Anonymous Users");
private static void configureDefaults(
Map<String, List<PermissionRule>> res,
AccessSection section) {
configureDefault(res, section, GlobalCapability.QUERY_LIMIT, anonymous);
}
private static void configureDefault(Map<String, List<PermissionRule>> res,
AccessSection section, String capName, GroupReference group) {
if (section.getPermission(capName) == null) {
PermissionRange.WithDefaults range = GlobalCapability.getRange(capName);
if (range != null) {
PermissionRule rule = new PermissionRule(group);
rule.setRange(range.getDefaultMin(), range.getDefaultMax());
res.put(capName, Collections.singletonList(rule));
}
}
private static boolean match(Set<AccountGroup.UUID> groups,
PermissionRule rule) {
return groups.contains(rule.getGroup().getUUID());
}
}

View File

@@ -23,6 +23,7 @@ import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.rules.PrologEnvironment;
import com.google.gerrit.rules.RulesCache;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.CapabilityCollection;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.ProjectConfig;
@@ -65,6 +66,8 @@ public class ProjectState {
/** Last system time the configuration's revision was examined. */
private volatile long lastCheckTime;
/** If this is all projects, the capabilities used by the server. */
private final CapabilityCollection capabilities;
@Inject
protected ProjectState(
@@ -82,6 +85,9 @@ public class ProjectState {
this.gitMgr = gitMgr;
this.rulesCache = rulesCache;
this.config = config;
this.capabilities = isAllProjects
? new CapabilityCollection(config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES))
: null;
HashSet<AccountGroup.UUID> groups = new HashSet<AccountGroup.UUID>();
AccessSection all = config.getAccessSection(AccessSection.ALL);
@@ -127,6 +133,15 @@ public class ProjectState {
}
}
/**
* @return cached computation of all global capabilities. This should only be
* invoked on the state from {@link ProjectCache#getAllProjects()}.
* Null on any other project.
*/
public CapabilityCollection getCapabilityCollection() {
return capabilities;
}
/** @return Construct a new PrologEnvironment for the calling thread. */
public PrologEnvironment newPrologEnvironment() throws CompileException {
PrologMachineCopy pmc = rulesMachine;