Merge changes Ied06561c,Idff421e1,Iaf12bab5,I2b744f5a
* changes:
Add permission_sort cache to remember sort orderings
Reuse cached RefControl data in FunctionState
Refactor how permissions are matched by ProjectControl, RefControl
Cache effective capabilities to improve lookup performance
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 9bbc5e0..d17109d 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -432,6 +432,13 @@
cache automatically updates when a user first creates their account
within Gerrit, so the cache expire time is largely irrelevant.
+cache `"permission_sort"`::
++
+Caches the order access control sections must be applied to a
+reference. Sorting the sections can be expensive when regular
+expressions are used, so this cache remembers the ordering for
+each branch.
+
cache `"projects"`::
+
Caches the project description records, from the `projects` table
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
index c274702..dedc656 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
@@ -154,8 +154,7 @@
db.patchSetApprovals().byChange(changeId).toList();
if (detail.getChange().getStatus().isOpen()) {
- final FunctionState fs =
- functionState.create(detail.getChange(), psId, allApprovals);
+ final FunctionState fs = functionState.create(control, psId, allApprovals);
for (final ApprovalType at : approvalTypes.getApprovalTypes()) {
CategoryFunction.forCategory(at.getCategory()).run(at, fs);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
index 394b43f..59755e3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
@@ -182,7 +182,7 @@
final Map<ApprovalCategory.Id, PatchSetApproval> psas =
new HashMap<ApprovalCategory.Id, PatchSetApproval>();
final FunctionState fs =
- functionStateFactory.create(change, ps_id, psas.values());
+ functionStateFactory.create(cc, ps_id, psas.values());
for (final PatchSetApproval ca : db.patchSetApprovals()
.byPatchSetUser(ps_id, aid)) {
@@ -229,7 +229,7 @@
final Map<ApprovalCategory.Id, PatchSetApproval> psas =
new HashMap<ApprovalCategory.Id, PatchSetApproval>();
final FunctionState fs =
- functionStateFactory.create(change, ps_id, psas.values());
+ functionStateFactory.create(cc, ps_id, psas.values());
for (PatchSetApproval ca : db.patchSetApprovals().byPatchSet(ps_id)) {
final ApprovalCategory.Id category = ca.getCategoryId();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java
new file mode 100644
index 0000000..3d03ed8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java
@@ -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;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index 39719ad..57d01fe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -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.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 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 boolean canAdministrateServer() {
if (canAdministrateServer == null) {
canAdministrateServer = user instanceof PeerDaemonUser
- || canPerform(GlobalCapability.ADMINISTRATE_SERVER);
+ || matchAny(capabilities.administrateServer);
}
return canAdministrateServer;
}
@@ -131,19 +129,21 @@
// 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) {
- switch (r.getAction()) {
- case INTERACTIVE:
- if (!isGenericGroup(r.getGroup())) {
- return QueueProvider.QueueType.INTERACTIVE;
- }
- break;
+ for (PermissionRule r : capabilities.priority) {
+ if (match(groups, r)) {
+ switch (r.getAction()) {
+ case INTERACTIVE:
+ if (!isGenericGroup(r.getGroup())) {
+ return QueueProvider.QueueType.INTERACTIVE;
+ }
+ break;
- case BATCH:
- batch = true;
- break;
+ case BATCH:
+ batch = true;
+ break;
+ }
}
}
@@ -186,71 +186,53 @@
/** 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();
- }
-
- /** All rules that pertain to this user. */
- private Map<String, List<PermissionRule>> permissions() {
- if (permissions == null) {
- permissions = indexPermissions();
- }
- return permissions;
- }
-
- 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);
+ List<PermissionRule> rules = effective.get(permissionName);
+ if (rules != null) {
+ 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);
- }
- }
+ rules = capabilities.getPermission(permissionName);
+
+ if (rules.isEmpty()) {
+ effective.put(permissionName, rules);
+ return rules;
+ }
+
+ 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;
+ }
+
+ 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 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 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 boolean match(Set<AccountGroup.UUID> groups,
+ PermissionRule rule) {
+ return groups.contains(rule.getGroup().getUUID());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ConcurrentHashMapCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ConcurrentHashMapCache.java
new file mode 100644
index 0000000..268fe2b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ConcurrentHashMapCache.java
@@ -0,0 +1,54 @@
+// 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.git;
+
+package com.google.gerrit.server.cache;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An infinitely sized cache backed by java.util.ConcurrentHashMap.
+ * <p>
+ * This cache type is only suitable for unit tests, as it has no upper limit on
+ * number of items held in the cache. No upper limit can result in memory leaks
+ * in production servers.
+ */
+public class ConcurrentHashMapCache<K, V> implements Cache<K, V> {
+ private final ConcurrentHashMap<K, V> map = new ConcurrentHashMap<K, V>();
+
+ @Override
+ public V get(K key) {
+ return map.get(key);
+ }
+
+ @Override
+ public void put(K key, V value) {
+ map.put(key, value);
+ }
+
+ @Override
+ public void remove(K key) {
+ map.remove(key);
+ }
+
+ @Override
+ public void removeAll() {
+ map.clear();
+ }
+
+ @Override
+ public long getTimeToLive(TimeUnit unit) {
+ return 0;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index a564e56..7bca406 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -58,10 +58,11 @@
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.project.AccessControlModule;
import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.PermissionCollection;
import com.google.gerrit.server.project.ProjectCacheImpl;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.project.SectionSortCache;
import com.google.gerrit.server.tools.ToolsCatalog;
import com.google.gerrit.server.util.IdGenerator;
import com.google.gerrit.server.workflow.FunctionState;
@@ -147,6 +148,7 @@
install(GroupIncludeCacheImpl.module());
install(PatchListCacheImpl.module());
install(ProjectCacheImpl.module());
+ install(SectionSortCache.module());
install(TagCache.module());
install(new AccessControlModule());
install(new GitModule());
@@ -156,7 +158,7 @@
factory(CapabilityControl.Factory.class);
factory(GroupInfoCacheFactory.Factory.class);
factory(ProjectState.Factory.class);
- factory(RefControl.Factory.class);
+ bind(PermissionCollection.Factory.class);
bind(FileTypeRegistry.class).to(MimeUtilFileTypeRegistry.class);
bind(WorkQueue.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
index a050cfd..4c386e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
@@ -40,6 +40,7 @@
import com.google.gerrit.server.patch.PublishComments;
import com.google.gerrit.server.patch.RemoveReviewer;
import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.PerRequestProjectControlCache;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ChangeQueryRewriter;
@@ -58,6 +59,7 @@
bind(ChangeQueryRewriter.class);
bind(AnonymousUser.class).in(RequestScoped.class);
+ bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
bind(ChangeControl.Factory.class).in(SINGLETON);
bind(GroupControl.Factory.class).in(SINGLETON);
bind(ProjectControl.Factory.class).in(SINGLETON);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 88676bd..2c756d4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -40,6 +40,8 @@
import com.google.gerrit.server.mail.MergedSender;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.workflow.CategoryFunction;
@@ -137,6 +139,7 @@
private final ApprovalTypes approvalTypes;
private final PatchSetInfoFactory patchSetInfoFactory;
private final IdentifiedUser.GenericFactory identifiedUserFactory;
+ private final ChangeControl.GenericFactory changeControlFactory;
private final MergeQueue mergeQueue;
private final PersonIdent myIdent;
@@ -167,6 +170,7 @@
@CanonicalWebUrl @Nullable final Provider<String> cwu,
final ApprovalTypes approvalTypes, final PatchSetInfoFactory psif,
final IdentifiedUser.GenericFactory iuf,
+ final ChangeControl.GenericFactory changeControlFactory,
@GerritPersonIdent final PersonIdent myIdent,
final MergeQueue mergeQueue, @Assisted final Branch.NameKey branch,
final ChangeHookRunner hooks, final AccountCache accountCache,
@@ -183,6 +187,7 @@
this.approvalTypes = approvalTypes;
patchSetInfoFactory = psif;
identifiedUserFactory = iuf;
+ this.changeControlFactory = changeControlFactory;
this.mergeQueue = mergeQueue;
this.hooks = hooks;
this.accountCache = accountCache;
@@ -1240,7 +1245,11 @@
c.setStatus(Change.Status.MERGED);
final List<PatchSetApproval> approvals =
schema.patchSetApprovals().byChange(changeId).toList();
- final FunctionState fs = functionState.create(c, merged, approvals);
+ final FunctionState fs = functionState.create(
+ changeControlFactory.controlFor(
+ c,
+ identifiedUserFactory.create(c.getOwner())),
+ merged, approvals);
for (ApprovalType at : approvalTypes.getApprovalTypes()) {
CategoryFunction.forCategory(at.getCategory()).run(at, fs);
}
@@ -1256,6 +1265,8 @@
a.cache(c);
}
schema.patchSetApprovals().update(approvals);
+ } catch (NoSuchChangeException err) {
+ log.warn("Cannot normalize approvals for change " + changeId, err);
} catch (OrmException err) {
log.warn("Cannot normalize approvals for change " + changeId, err);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
index 244648a..8efb4ae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
@@ -120,7 +120,7 @@
final boolean isCurrent = patchSetId.equals(change.currentPatchSetId());
if (isCurrent && change.getStatus().isOpen()) {
- publishApprovals();
+ publishApprovals(ctl);
} else if (! approvals.isEmpty()) {
throw new InvalidChangeOperationException("Change is closed");
} else {
@@ -141,7 +141,7 @@
db.patchComments().update(drafts);
}
- private void publishApprovals() throws OrmException {
+ private void publishApprovals(ChangeControl ctl) throws OrmException {
ChangeUtil.updated(change);
final Set<ApprovalCategory.Id> dirty = new HashSet<ApprovalCategory.Id>();
@@ -169,7 +169,7 @@
// Normalize all of the items the user is changing.
//
final FunctionState functionState =
- functionStateFactory.create(change, patchSetId, all);
+ functionStateFactory.create(ctl, patchSetId, all);
for (final ApprovalCategoryValue.Id want : approvals) {
final PatchSetApproval a = mine.get(want.getParentKey());
final short o = a.getValue();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PerRequestProjectControlCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerRequestProjectControlCache.java
new file mode 100644
index 0000000..500ac06
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerRequestProjectControlCache.java
@@ -0,0 +1,52 @@
+// 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.project;
+
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.servlet.RequestScoped;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Caches {@link ProjectControl} objects for the current user of the request. */
+@RequestScoped
+public class PerRequestProjectControlCache {
+ private final ProjectCache projectCache;
+ private final CurrentUser user;
+ private final Map<Project.NameKey, ProjectControl> controls;
+
+ @Inject
+ PerRequestProjectControlCache(ProjectCache projectCache,
+ CurrentUser userProvider) {
+ this.projectCache = projectCache;
+ this.user = userProvider;
+ this.controls = new HashMap<Project.NameKey, ProjectControl>();
+ }
+
+ ProjectControl get(Project.NameKey nameKey) throws NoSuchProjectException {
+ ProjectControl ctl = controls.get(nameKey);
+ if (ctl == null) {
+ ProjectState p = projectCache.get(nameKey);
+ if (p == null) {
+ throw new NoSuchProjectException(nameKey);
+ }
+ ctl = p.controlFor(user);
+ controls.put(nameKey, ctl);
+ }
+ return ctl;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
new file mode 100644
index 0000000..ca3bb4d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
@@ -0,0 +1,218 @@
+// 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.project;
+
+import static com.google.gerrit.server.project.RefControl.isRE;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Effective permissions applied to a reference in a project.
+ * <p>
+ * A collection may be user specific if a matching {@link AccessSection} uses
+ * "${username}" in its name. The permissions granted in that section may only
+ * be granted to the username that appears in the reference name, and also only
+ * if the user is a member of the relevant group.
+ */
+public class PermissionCollection {
+ @Singleton
+ public static class Factory {
+ private final SectionSortCache sorter;
+
+ @Inject
+ Factory(SectionSortCache sorter) {
+ this.sorter = sorter;
+ }
+
+ /**
+ * Get all permissions that apply to a reference.
+ *
+ * @param matcherList collection of sections that should be considered, in
+ * priority order (project specific definitions must appear before
+ * inherited ones).
+ * @param ref reference being accessed.
+ * @param username if the reference is a per-user reference, access sections
+ * using the parameter variable "${username}" will first have {@code
+ * username} inserted into them before seeing if they apply to the
+ * reference named by {@code ref}. If null, per-user references are
+ * ignored.
+ * @return map of permissions that apply to this reference, keyed by
+ * permission name.
+ */
+ PermissionCollection filter(Iterable<SectionMatcher> matcherList,
+ String ref, String username) {
+ if (isRE(ref)) {
+ ref = RefControl.shortestExample(ref);
+ } else if (ref.endsWith("/*")) {
+ ref = ref.substring(0, ref.length() - 1);
+ }
+
+ boolean perUser = false;
+ List<AccessSection> sections = new ArrayList<AccessSection>();
+ for (SectionMatcher matcher : matcherList) {
+ // If the matcher has to expand parameters and its prefix matches the
+ // reference there is a very good chance the reference is actually user
+ // specific, even if the matcher does not match the reference. Since its
+ // difficult to prove this is true all of the time, use an approximation
+ // to prevent reuse of collections across users accessing the same
+ // reference at the same time.
+ //
+ // This check usually gets caching right, as most per-user references
+ // use a common prefix like "refs/sandbox/" or "refs/heads/users/"
+ // that will never be shared with non-user references, and the per-user
+ // references are usually less frequent than the non-user references.
+ //
+ if (username != null && !perUser
+ && matcher instanceof SectionMatcher.ExpandParameters) {
+ perUser = ((SectionMatcher.ExpandParameters) matcher).matchPrefix(ref);
+ }
+
+ if (matcher.match(ref, username)) {
+ sections.add(matcher.section);
+ }
+ }
+ sorter.sort(ref, sections);
+
+ Set<SeenRule> seen = new HashSet<SeenRule>();
+ Set<String> exclusiveGroupPermissions = new HashSet<String>();
+
+ HashMap<String, List<PermissionRule>> permissions =
+ new HashMap<String, List<PermissionRule>>();
+ for (AccessSection section : sections) {
+ for (Permission permission : section.getPermissions()) {
+ if (exclusiveGroupPermissions.contains(permission.getName())) {
+ continue;
+ }
+
+ for (PermissionRule rule : permission.getRules()) {
+ SeenRule s = new SeenRule(section, permission, rule);
+ if (seen.add(s) && !rule.getDeny()) {
+ List<PermissionRule> r = permissions.get(permission.getName());
+ if (r == null) {
+ r = new ArrayList<PermissionRule>(2);
+ permissions.put(permission.getName(), r);
+ }
+ r.add(rule);
+ }
+ }
+
+ if (permission.getExclusiveGroup()) {
+ exclusiveGroupPermissions.add(permission.getName());
+ }
+ }
+ }
+
+ return new PermissionCollection(ref, permissions, perUser ? username : null);
+ }
+ }
+
+ private final String ref;
+ private final Map<String, List<PermissionRule>> rules;
+ private final String username;
+
+ private PermissionCollection(String ref,
+ Map<String, List<PermissionRule>> rules, String username) {
+ this.ref = ref;
+ this.rules = rules;
+ this.username = username;
+ }
+
+ /**
+ * @return true if a "${username}" pattern might need to be expanded to build
+ * this collection, making the results user specific.
+ */
+ public boolean isUserSpecific() {
+ return username != null;
+ }
+
+ /**
+ * Obtain all permission rules for a given type of permission.
+ *
+ * @param permissionName type of permission.
+ * @return all rules that apply to this reference, for any group. Never null;
+ * the empty list is returned when there are no rules for the requested
+ * permission name.
+ */
+ public List<PermissionRule> getPermission(String permissionName) {
+ List<PermissionRule> r = rules.get(permissionName);
+ return r != null ? r : Collections.<PermissionRule> emptyList();
+ }
+
+ /**
+ * Obtain all declared permission rules that match the reference.
+ *
+ * @return all rules. The collection will iterate a permission if it was
+ * declared in the project configuration, either directly or
+ * inherited. If the project owner did not use a known permission (for
+ * example {@link Permission#FORGE_SERVER}, then it will not be
+ * represented in the result even if {@link #getPermission(String)}
+ * returns an empty list for the same permission.
+ */
+ public Iterable<Map.Entry<String, List<PermissionRule>>> getDeclaredPermissions() {
+ return rules.entrySet();
+ }
+
+ /** Tracks whether or not a permission has been overridden. */
+ private static class SeenRule {
+ final String refPattern;
+ final String permissionName;
+ final AccountGroup.UUID group;
+
+ SeenRule(AccessSection section, Permission permission, PermissionRule rule) {
+ refPattern = section.getName();
+ permissionName = permission.getName();
+ group = rule.getGroup().getUUID();
+ }
+
+ @Override
+ public int hashCode() {
+ int hc = refPattern.hashCode();
+ hc = hc * 31 + permissionName.hashCode();
+ if (group != null) {
+ hc = hc * 31 + group.hashCode();
+ }
+ return hc;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof SeenRule) {
+ SeenRule a = this;
+ SeenRule b = (SeenRule) other;
+ return a.refPattern.equals(b.refPattern) //
+ && a.permissionName.equals(b.permissionName) //
+ && eq(a.group, b.group);
+ }
+ return false;
+ }
+
+ private boolean eq(AccountGroup.UUID a, AccountGroup.UUID b) {
+ return a != null && b != null && a.equals(b);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index e9247af..8a2966a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -14,8 +14,6 @@
package com.google.gerrit.server.project;
-import static com.google.gerrit.common.CollectionsUtil.isAnyIncludedIn;
-
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.Capable;
@@ -46,10 +44,11 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
@@ -81,22 +80,16 @@
}
public static class Factory {
- private final ProjectCache projectCache;
- private final Provider<CurrentUser> user;
+ private final Provider<PerRequestProjectControlCache> userCache;
@Inject
- Factory(final ProjectCache pc, final Provider<CurrentUser> cu) {
- projectCache = pc;
- user = cu;
+ Factory(Provider<PerRequestProjectControlCache> uc) {
+ userCache = uc;
}
public ProjectControl controlFor(final Project.NameKey nameKey)
throws NoSuchProjectException {
- final ProjectState p = projectCache.get(nameKey);
- if (p == null) {
- throw new NoSuchProjectException(nameKey);
- }
- return p.controlFor(user.get());
+ return userCache.get().get(nameKey);
}
public ProjectControl validateFor(final Project.NameKey nameKey)
@@ -130,34 +123,38 @@
private final Set<AccountGroup.UUID> receiveGroups;
private final String canonicalWebUrl;
- private final RefControl.Factory refControlFactory;
private final SchemaFactory<ReviewDb> schema;
private final CurrentUser user;
private final ProjectState state;
private final GroupCache groupCache;
+ private final PermissionCollection.Factory permissionFilter;
-
- private Collection<AccessSection> access;
+ private List<SectionMatcher> allSections;
+ private Map<String, RefControl> refControls;
+ private Boolean declaredOwner;
@Inject
ProjectControl(@GitUploadPackGroups Set<AccountGroup.UUID> uploadGroups,
@GitReceivePackGroups Set<AccountGroup.UUID> receiveGroups,
final SchemaFactory<ReviewDb> schema, final GroupCache groupCache,
+ final PermissionCollection.Factory permissionFilter,
@CanonicalWebUrl @Nullable final String canonicalWebUrl,
- final RefControl.Factory refControlFactory,
@Assisted CurrentUser who, @Assisted ProjectState ps) {
this.uploadGroups = uploadGroups;
this.receiveGroups = receiveGroups;
this.schema = schema;
this.groupCache = groupCache;
+ this.permissionFilter = permissionFilter;
this.canonicalWebUrl = canonicalWebUrl;
- this.refControlFactory = refControlFactory;
user = who;
state = ps;
}
- public ProjectControl forUser(final CurrentUser who) {
- return state.controlFor(who);
+ public ProjectControl forUser(CurrentUser who) {
+ ProjectControl r = state.controlFor(who);
+ // Not per-user, and reusing saves lookup time.
+ r.allSections = allSections;
+ return r;
}
public ChangeControl controlFor(final Change change) {
@@ -169,7 +166,17 @@
}
public RefControl controlForRef(String refName) {
- return refControlFactory.create(this, refName);
+ if (refControls == null) {
+ refControls = new HashMap<String, RefControl>();
+ }
+ RefControl ctl = refControls.get(refName);
+ if (ctl == null) {
+ PermissionCollection relevant =
+ permissionFilter.filter(access(), refName, user.getUserName());
+ ctl = new RefControl(this, refName, relevant);
+ refControls.put(refName, ctl);
+ }
+ return ctl;
}
public CurrentUser getCurrentUser() {
@@ -181,7 +188,7 @@
}
public Project getProject() {
- return getProjectState().getProject();
+ return state.getProject();
}
/** Can this user see this project exists? */
@@ -203,20 +210,27 @@
/** Is this project completely visible for replication? */
boolean visibleForReplication() {
- return getCurrentUser() instanceof ReplicationUser
- && ((ReplicationUser) getCurrentUser()).isEverythingVisible();
+ return user instanceof ReplicationUser
+ && ((ReplicationUser) user).isEverythingVisible();
}
/** Is this user a project owner? Ownership does not imply {@link #isVisible()} */
public boolean isOwner() {
- return controlForRef(AccessSection.ALL).isOwner()
- || getCurrentUser().getCapabilities().canAdministrateServer();
+ return isDeclaredOwner()
+ || user.getCapabilities().canAdministrateServer();
+ }
+
+ private boolean isDeclaredOwner() {
+ if (declaredOwner == null) {
+ declaredOwner = state.isOwner(user.getEffectiveGroups());
+ }
+ return declaredOwner;
}
/** Does this user have ownership on at least one reference name? */
public boolean isOwnerAnyRef() {
return canPerformOnAnyRef(Permission.OWNER)
- || getCurrentUser().getCapabilities().canAdministrateServer();
+ || user.getCapabilities().canAdministrateServer();
}
/** @return true if the user can upload to at least one reference */
@@ -370,33 +384,16 @@
return value == null || value.trim().equals("");
}
- /**
- * @return the effective groups of the current user for this project
- */
- private Set<AccountGroup.UUID> getEffectiveUserGroups() {
- final Set<AccountGroup.UUID> userGroups = user.getEffectiveGroups();
- if (isOwner()) {
- final Set<AccountGroup.UUID> userGroupsOnProject =
- new HashSet<AccountGroup.UUID>(userGroups.size() + 1);
- userGroupsOnProject.addAll(userGroups);
- userGroupsOnProject.add(AccountGroup.PROJECT_OWNERS);
- return Collections.unmodifiableSet(userGroupsOnProject);
- } else {
- return userGroups;
- }
- }
-
private boolean canPerformOnAnyRef(String permissionName) {
- final Set<AccountGroup.UUID> groups = getEffectiveUserGroups();
-
- for (AccessSection section : access()) {
+ for (SectionMatcher matcher : access()) {
+ AccessSection section = matcher.section;
Permission permission = section.getPermission(permissionName);
if (permission == null) {
continue;
}
for (PermissionRule rule : permission.getRules()) {
- if (rule.getDeny()) {
+ if (rule.getDeny() || !match(rule)) {
continue;
}
@@ -404,9 +401,10 @@
// approximation. There might be overrides and doNotInherit
// that would render this to be false.
//
- if (groups.contains(rule.getGroup().getUUID())
- && controlForRef(section.getName()).canPerform(permissionName)) {
+ if (controlForRef(section.getName()).canPerform(permissionName)) {
return true;
+ } else {
+ break;
}
}
}
@@ -435,7 +433,8 @@
private Set<String> allRefPatterns(String permissionName) {
Set<String> all = new HashSet<String>();
- for (AccessSection section : access()) {
+ for (SectionMatcher matcher : access()) {
+ AccessSection section = matcher.section;
Permission permission = section.getPermission(permissionName);
if (permission != null) {
all.add(section.getName());
@@ -444,18 +443,40 @@
return all;
}
- Collection<AccessSection> access() {
- if (access == null) {
- access = state.getAllAccessSections();
+ private List<SectionMatcher> access() {
+ if (allSections == null) {
+ allSections = state.getAllSections();
}
- return access;
+ return allSections;
+ }
+
+ boolean match(PermissionRule rule) {
+ return match(rule.getGroup().getUUID());
+ }
+
+ boolean match(AccountGroup.UUID uuid) {
+ if (AccountGroup.PROJECT_OWNERS.equals(uuid)) {
+ return isDeclaredOwner();
+ } else {
+ return user.getEffectiveGroups().contains(uuid);
+ }
}
public boolean canRunUploadPack() {
- return isAnyIncludedIn(uploadGroups, getEffectiveUserGroups());
+ for (AccountGroup.UUID group : uploadGroups) {
+ if (match(group)) {
+ return true;
+ }
+ }
+ return false;
}
public boolean canRunReceivePack() {
- return isAnyIncludedIn(receiveGroups, getEffectiveUserGroups());
+ for (AccountGroup.UUID group : receiveGroups) {
+ if (match(group)) {
+ return true;
+ }
+ }
+ return false;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index 376d1a7..c587829 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.project;
+import com.google.gerrit.common.CollectionsUtil;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.Permission;
@@ -23,6 +24,7 @@
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 +67,11 @@
/** Last system time the configuration's revision was examined. */
private volatile long lastCheckTime;
+ /** Local access sections, wrapped in SectionMatchers for faster evaluation. */
+ private volatile List<SectionMatcher> localAccessSections;
+
+ /** If this is all projects, the capabilities used by the server. */
+ private final CapabilityCollection capabilities;
@Inject
protected ProjectState(
@@ -82,6 +89,9 @@
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,64 +137,77 @@
}
}
+ /**
+ * @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;
if (pmc == null) {
pmc = rulesCache.loadMachine(
getProject().getNameKey(),
- getConfig().getRulesId());
+ config.getRulesId());
rulesMachine = pmc;
}
return envFactory.create(pmc);
}
public Project getProject() {
- return getConfig().getProject();
+ return config.getProject();
}
public ProjectConfig getConfig() {
return config;
}
- /** Get the rights that pertain only to this project. */
- public Collection<AccessSection> getLocalAccessSections() {
- return getConfig().getAccessSections();
+ /** Get the sections that pertain only to this project. */
+ private List<SectionMatcher> getLocalAccessSections() {
+ List<SectionMatcher> sm = localAccessSections;
+ if (sm == null) {
+ Collection<AccessSection> fromConfig = config.getAccessSections();
+ sm = new ArrayList<SectionMatcher>(fromConfig.size());
+ for (AccessSection section : fromConfig) {
+ SectionMatcher matcher = SectionMatcher.wrap(section);
+ if (matcher != null) {
+ sm.add(matcher);
+ }
+ }
+ localAccessSections = sm;
+ }
+ return sm;
}
- /** Get the rights this project inherits. */
- public Collection<AccessSection> getInheritedAccessSections() {
+ /**
+ * Obtain all local and inherited sections. This collection is looked up
+ * dynamically and is not cached. Callers should try to cache this result
+ * per-request as much as possible.
+ */
+ List<SectionMatcher> getAllSections() {
if (isAllProjects) {
- return Collections.emptyList();
+ return getLocalAccessSections();
}
- List<AccessSection> inherited = new ArrayList<AccessSection>();
+ List<SectionMatcher> all = new ArrayList<SectionMatcher>();
Set<Project.NameKey> seen = new HashSet<Project.NameKey>();
- Project.NameKey parent = getProject().getParent();
+ seen.add(getProject().getNameKey());
- while (parent != null && seen.add(parent)) {
- ProjectState s = projectCache.get(parent);
- if (s != null) {
- inherited.addAll(s.getLocalAccessSections());
- parent = s.getProject().getParent();
- } else {
+ ProjectState s = this;
+ do {
+ all.addAll(s.getLocalAccessSections());
+
+ Project.NameKey parent = s.getProject().getParent();
+ if (parent == null || !seen.add(parent)) {
break;
}
- }
-
- // The root of the tree is the special "All-Projects" case.
- if (parent == null) {
- inherited.addAll(projectCache.getAllProjects().getLocalAccessSections());
- }
-
- return inherited;
- }
-
- /** Get both local and inherited access sections. */
- public Collection<AccessSection> getAllAccessSections() {
- List<AccessSection> all = new ArrayList<AccessSection>();
- all.addAll(getLocalAccessSections());
- all.addAll(getInheritedAccessSections());
+ s = projectCache.get(parent);
+ } while (s != null);
+ all.addAll(projectCache.getAllProjects().getLocalAccessSections());
return all;
}
@@ -209,30 +232,26 @@
}
/**
- * @return all {@link AccountGroup}'s that are allowed to administrate the
- * complete project. This includes all groups to which the owner
- * privilege for 'refs/*' is assigned for this project (the local
- * owners) and all groups to which the owner privilege for 'refs/*' is
- * assigned for one of the parent projects (the inherited owners).
+ * @return true if any of the groups listed in {@code groups} was declared to
+ * be an owner of this project, or one of its parent projects..
*/
- public Set<AccountGroup.UUID> getAllOwners() {
- HashSet<AccountGroup.UUID> owners = new HashSet<AccountGroup.UUID>();
- owners.addAll(localOwners);
-
+ boolean isOwner(Set<AccountGroup.UUID> groups) {
Set<Project.NameKey> seen = new HashSet<Project.NameKey>();
- Project.NameKey parent = getProject().getParent();
+ seen.add(getProject().getNameKey());
- while (parent != null && seen.add(parent)) {
- ProjectState s = projectCache.get(parent);
- if (s != null) {
- owners.addAll(s.localOwners);
- parent = s.getProject().getParent();
- } else {
+ ProjectState s = this;
+ do {
+ if (CollectionsUtil.isAnyIncludedIn(s.localOwners, groups)) {
+ return true;
+ }
+
+ Project.NameKey parent = s.getProject().getParent();
+ if (parent == null || !seen.add(parent)) {
break;
}
- }
-
- return Collections.unmodifiableSet(owners);
+ s = projectCache.get(parent);
+ } while (s != null);
+ return false;
}
public ProjectControl controlFor(final CurrentUser user) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index e6b39c9..984623d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -14,22 +14,16 @@
package com.google.gerrit.server.project;
-import com.google.gerrit.common.CollectionsUtil;
import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.ParamertizedString;
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 com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
import dk.brics.automaton.RegExp;
-import org.apache.commons.lang.StringUtils;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
@@ -39,43 +33,32 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Comparator;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.Set;
-import java.util.regex.Pattern;
/** Manages access control for Git references (aka branches, tags). */
public class RefControl {
- public interface Factory {
- RefControl create(ProjectControl projectControl, String ref);
- }
-
private final ProjectControl projectControl;
private final String refName;
- private Map<String, List<PermissionRule>> permissions;
+ /** All permissions that apply to this reference. */
+ private final PermissionCollection relevant;
+
+ /** Cached set of permissions matching this user. */
+ private final Map<String, List<PermissionRule>> effective;
private Boolean owner;
private Boolean canForgeAuthor;
private Boolean canForgeCommitter;
- @Inject
- protected RefControl(@Assisted final ProjectControl projectControl,
- @Assisted String ref) {
- if (isRE(ref)) {
- ref = shortestExample(ref);
-
- } else if (ref.endsWith("/*")) {
- ref = ref.substring(0, ref.length() - 1);
-
- }
-
+ RefControl(ProjectControl projectControl, String ref,
+ PermissionCollection relevant) {
this.projectControl = projectControl;
this.refName = ref;
+ this.relevant = relevant;
+ this.effective = new HashMap<String, List<PermissionRule>>();
}
public String getRefName() {
@@ -87,11 +70,16 @@
}
public CurrentUser getCurrentUser() {
- return getProjectControl().getCurrentUser();
+ return projectControl.getCurrentUser();
}
- public RefControl forUser(final CurrentUser who) {
- return getProjectControl().forUser(who).controlForRef(getRefName());
+ public RefControl forUser(CurrentUser who) {
+ ProjectControl newCtl = projectControl.forUser(who);
+ if (relevant.isUserSpecific()) {
+ return newCtl.controlForRef(getRefName());
+ } else {
+ return new RefControl(newCtl, getRefName(), relevant);
+ }
}
/** Is this user a ref owner? */
@@ -100,16 +88,8 @@
if (canPerform(Permission.OWNER)) {
owner = true;
- } else if (getRefName().equals(
- AccessSection.ALL.substring(0, AccessSection.ALL.length() - 1))) {
- // We have to prevent infinite recursion here, the project control
- // calls us to find out if there is ownership of all references in
- // order to determine project level ownership.
- //
- owner = getCurrentUser().getCapabilities().canAdministrateServer();
-
} else {
- owner = getProjectControl().isOwner();
+ owner = projectControl.isOwner();
}
}
return owner;
@@ -117,7 +97,7 @@
/** Can this user see this reference exists? */
public boolean isVisible() {
- return getProjectControl().visibleForReplication()
+ return projectControl.visibleForReplication()
|| canPerform(Permission.READ);
}
@@ -129,14 +109,14 @@
* ref
*/
public boolean canUpload() {
- return getProjectControl()
- .controlForRef("refs/for/" + getRefName())
- .canPerform(Permission.PUSH);
+ return projectControl
+ .controlForRef("refs/for/" + getRefName())
+ .canPerform(Permission.PUSH);
}
/** @return true if this user can submit merge patch sets to this ref */
public boolean canUploadMerges() {
- return getProjectControl()
+ return projectControl
.controlForRef("refs/for/" + getRefName())
.canPerform(Permission.PUSH_MERGE);
}
@@ -149,7 +129,7 @@
// rules. Allowing this to be done by a non-project-owner opens
// a security hole enabling editing of access rules, and thus
// granting of powers beyond submitting to the configuration.
- return getProjectControl().isOwner();
+ return projectControl.isOwner();
}
return canPerform(Permission.SUBMIT);
}
@@ -157,7 +137,7 @@
/** @return true if the user can update the reference as a fast-forward. */
public boolean canUpdate() {
if (GitRepositoryManager.REF_CONFIG.equals(refName)
- && !getProjectControl().isOwner()) {
+ && !projectControl.isOwner()) {
// Pushing requires being at least project owner, in addition to push.
// Pushing configuration changes modifies the access control
// rules. Allowing this to be done by a non-project-owner opens
@@ -175,7 +155,7 @@
private boolean canPushWithForce() {
if (GitRepositoryManager.REF_CONFIG.equals(refName)
- && !getProjectControl().isOwner()) {
+ && !projectControl.isOwner()) {
// Pushing requires being at least project owner, in addition to push.
// Pushing configuration changes modifies the access control
// rules. Allowing this to be done by a non-project-owner opens
@@ -303,9 +283,19 @@
/** All value ranges of any allowed label permission. */
public List<PermissionRange> getLabelRanges() {
List<PermissionRange> r = new ArrayList<PermissionRange>();
- for (Map.Entry<String, List<PermissionRule>> e : permissions().entrySet()) {
+ for (Map.Entry<String, List<PermissionRule>> e : relevant.getDeclaredPermissions()) {
if (Permission.isLabel(e.getKey())) {
- r.add(toRange(e.getKey(), e.getValue()));
+ int min = 0;
+ int max = 0;
+ for (PermissionRule rule : e.getValue()) {
+ if (projectControl.match(rule)) {
+ min = Math.min(min, rule.getMin());
+ max = Math.max(max, rule.getMax());
+ }
+ }
+ if (min != 0 || max != 0) {
+ r.add(new PermissionRange(e.getKey(), min, max));
+ }
}
}
return r;
@@ -319,7 +309,8 @@
return null;
}
- private static PermissionRange toRange(String permissionName, List<PermissionRule> ruleList) {
+ private static PermissionRange toRange(String permissionName,
+ List<PermissionRule> ruleList) {
int min = 0;
int max = 0;
for (PermissionRule rule : ruleList) {
@@ -336,115 +327,41 @@
/** 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();
- }
-
- /** All rules that pertain to this user, on this reference. */
- private Map<String, List<PermissionRule>> permissions() {
- if (permissions == null) {
- List<AccessSection> sections = new ArrayList<AccessSection>();
- for (AccessSection section : projectControl.access()) {
- if (appliesToRef(section)) {
- sections.add(section);
- }
- }
- Collections.sort(sections, new MostSpecificComparator(getRefName()));
-
- Set<SeenRule> seen = new HashSet<SeenRule>();
- Set<String> exclusiveGroupPermissions = new HashSet<String>();
-
- permissions = new HashMap<String, List<PermissionRule>>();
- for (AccessSection section : sections) {
- for (Permission permission : section.getPermissions()) {
- if (exclusiveGroupPermissions.contains(permission.getName())) {
- continue;
- }
-
- for (PermissionRule rule : permission.getRules()) {
- if (matchGroup(rule.getGroup().getUUID())) {
- SeenRule s = new SeenRule(section, permission, rule);
- if (seen.add(s) && !rule.getDeny()) {
- List<PermissionRule> r = permissions.get(permission.getName());
- if (r == null) {
- r = new ArrayList<PermissionRule>(2);
- permissions.put(permission.getName(), r);
- }
- r.add(rule);
- }
- }
- }
-
- if (permission.getExclusiveGroup()) {
- exclusiveGroupPermissions.add(permission.getName());
- }
- }
- }
- }
- return permissions;
- }
-
- private boolean appliesToRef(AccessSection section) {
- String refPattern = section.getName();
-
- if (isTemplate(refPattern)) {
- ParamertizedString template = new ParamertizedString(refPattern);
- HashMap<String, String> p = new HashMap<String, String>();
-
- if (getCurrentUser() instanceof IdentifiedUser) {
- p.put("username", ((IdentifiedUser) getCurrentUser()).getUserName());
- } else {
- // Right now we only template the username. If not available
- // this rule cannot be matched at all.
- //
- return false;
- }
-
- if (isRE(refPattern)) {
- for (Map.Entry<String, String> ent : p.entrySet()) {
- ent.setValue(escape(ent.getValue()));
- }
- }
-
- refPattern = template.replace(p);
+ List<PermissionRule> rules = effective.get(permissionName);
+ if (rules != null) {
+ return rules;
}
- if (isRE(refPattern)) {
- return Pattern.matches(refPattern, getRefName());
+ rules = relevant.getPermission(permissionName);
- } else if (refPattern.endsWith("/*")) {
- String prefix = refPattern.substring(0, refPattern.length() - 1);
- return getRefName().startsWith(prefix);
-
- } else {
- return getRefName().equals(refPattern);
+ if (rules.isEmpty()) {
+ effective.put(permissionName, rules);
+ return rules;
}
- }
- private boolean matchGroup(AccountGroup.UUID uuid) {
- Set<AccountGroup.UUID> userGroups = getCurrentUser().getEffectiveGroups();
-
- if (AccountGroup.PROJECT_OWNERS.equals(uuid)) {
- ProjectState state = projectControl.getProjectState();
- return CollectionsUtil.isAnyIncludedIn(state.getAllOwners(), userGroups);
-
- } else {
- return userGroups.contains(uuid);
+ if (rules.size() == 1) {
+ if (!projectControl.match(rules.get(0))) {
+ rules = Collections.emptyList();
+ }
+ effective.put(permissionName, rules);
+ return rules;
}
+
+ List<PermissionRule> mine = new ArrayList<PermissionRule>(rules.size());
+ for (PermissionRule rule : rules) {
+ if (projectControl.match(rule)) {
+ mine.add(rule);
+ }
+ }
+
+ if (mine.isEmpty()) {
+ mine = Collections.emptyList();
+ }
+ effective.put(permissionName, mine);
+ return mine;
}
- private static boolean isTemplate(String refPattern) {
- return 0 <= refPattern.indexOf("${");
- }
-
- private static String escape(String value) {
- // Right now the only special character allowed in a
- // variable value is a . in the username.
- //
- return value.replace(".", "\\.");
- }
-
- private static boolean isRE(String refPattern) {
+ static boolean isRE(String refPattern) {
return refPattern.startsWith(AccessSection.REGEX_PREFIX);
}
@@ -458,149 +375,10 @@
}
}
- private static RegExp toRegExp(String refPattern) {
+ static RegExp toRegExp(String refPattern) {
if (isRE(refPattern)) {
refPattern = refPattern.substring(1);
}
return new RegExp(refPattern, RegExp.NONE);
}
-
- /** Tracks whether or not a permission has been overridden. */
- private static class SeenRule {
- final String refPattern;
- final String permissionName;
- final AccountGroup.UUID group;
-
- SeenRule(AccessSection section, Permission permission, PermissionRule rule) {
- refPattern = section.getName();
- permissionName = permission.getName();
- group = rule.getGroup().getUUID();
- }
-
- @Override
- public int hashCode() {
- int hc = refPattern.hashCode();
- hc = hc * 31 + permissionName.hashCode();
- if (group != null) {
- hc = hc * 31 + group.hashCode();
- }
- return hc;
- }
-
- @Override
- public boolean equals(Object other) {
- if (other instanceof SeenRule) {
- SeenRule a = this;
- SeenRule b = (SeenRule) other;
- return a.refPattern.equals(b.refPattern) //
- && a.permissionName.equals(b.permissionName) //
- && eq(a.group, b.group);
- }
- return false;
- }
-
- private boolean eq(AccountGroup.UUID a, AccountGroup.UUID b) {
- return a != null && b != null && a.equals(b);
- }
- }
-
- /**
- * Order the Ref Pattern by the most specific. This sort is done by:
- * <ul>
- * <li>1 - The minor value of Levenshtein string distance between the branch
- * name and the regex string shortest example. A shorter distance is a more
- * specific match.
- * <li>2 - Finites first, infinities after.
- * <li>3 - Number of transitions.
- * <li>4 - Length of the expression text.
- * </ul>
- *
- * Levenshtein distance is a measure of the similarity between two strings.
- * The distance is the number of deletions, insertions, or substitutions
- * required to transform one string into another.
- *
- * For example, if given refs/heads/m* and refs/heads/*, the distances are 5
- * and 6. It means that refs/heads/m* is more specific because it's closer to
- * refs/heads/master than refs/heads/*.
- *
- * Another example could be refs/heads/* and refs/heads/[a-zA-Z]*, the
- * distances are both 6. Both are infinite, but refs/heads/[a-zA-Z]* has more
- * transitions, which after all turns it more specific.
- */
- private static final class MostSpecificComparator implements
- Comparator<AccessSection> {
- private final String refName;
-
- MostSpecificComparator(String refName) {
- this.refName = refName;
- }
-
- public int compare(AccessSection a, AccessSection b) {
- return compare(a.getName(), b.getName());
- }
-
- private int compare(final String pattern1, final String pattern2) {
- int cmp = distance(pattern1) - distance(pattern2);
- if (cmp == 0) {
- boolean p1_finite = finite(pattern1);
- boolean p2_finite = finite(pattern2);
-
- if (p1_finite && !p2_finite) {
- cmp = -1;
- } else if (!p1_finite && p2_finite) {
- cmp = 1;
- } else /* if (f1 == f2) */{
- cmp = 0;
- }
- }
- if (cmp == 0) {
- cmp = transitions(pattern1) - transitions(pattern2);
- }
- if (cmp == 0) {
- cmp = pattern2.length() - pattern1.length();
- }
- return cmp;
- }
-
- private int distance(String pattern) {
- String example;
- if (isRE(pattern)) {
- example = shortestExample(pattern);
-
- } else if (pattern.endsWith("/*")) {
- example = pattern.substring(0, pattern.length() - 1) + '1';
-
- } else if (pattern.equals(refName)) {
- return 0;
-
- } else {
- return Math.max(pattern.length(), refName.length());
- }
- return StringUtils.getLevenshteinDistance(example, refName);
- }
-
- private boolean finite(String pattern) {
- if (isRE(pattern)) {
- return toRegExp(pattern).toAutomaton().isFinite();
-
- } else if (pattern.endsWith("/*")) {
- return false;
-
- } else {
- return true;
- }
- }
-
- private int transitions(String pattern) {
- if (isRE(pattern)) {
- return toRegExp(pattern).toAutomaton().getNumberOfTransitions();
-
- } else if (pattern.endsWith("/*")) {
- return pattern.length();
-
- } else {
- return pattern.length();
- }
- }
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java
new file mode 100644
index 0000000..9a93725
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java
@@ -0,0 +1,154 @@
+// 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.project;
+
+import static com.google.gerrit.server.project.RefControl.isRE;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.ParamertizedString;
+
+import dk.brics.automaton.Automaton;
+
+import java.util.Collections;
+import java.util.regex.Pattern;
+
+/**
+ * Matches an AccessSection against a reference name.
+ * <p>
+ * These matchers are "compiled" versions of the AccessSection name, supporting
+ * faster selection of which sections are relevant to any given input reference.
+ */
+abstract class SectionMatcher {
+ static SectionMatcher wrap(AccessSection section) {
+ String ref = section.getName();
+ if (AccessSection.isAccessSection(ref)) {
+ return wrap(ref, section);
+ } else {
+ return null;
+ }
+ }
+
+ static SectionMatcher wrap(String pattern, AccessSection section) {
+ if (pattern.contains("${")) {
+ return new ExpandParameters(pattern, section);
+
+ } else if (isRE(pattern)) {
+ return new Regexp(pattern, section);
+
+ } else if (pattern.endsWith("/*")) {
+ return new Prefix(pattern.substring(0, pattern.length() - 1), section);
+
+ } else {
+ return new Exact(pattern, section);
+ }
+ }
+
+ final AccessSection section;
+
+ SectionMatcher(AccessSection section) {
+ this.section = section;
+ }
+
+ abstract boolean match(String ref, String username);
+
+ private static class Exact extends SectionMatcher {
+ private final String expect;
+
+ Exact(String name, AccessSection section) {
+ super(section);
+ expect = name;
+ }
+
+ @Override
+ boolean match(String ref, String username) {
+ return expect.equals(ref);
+ }
+ }
+
+ private static class Prefix extends SectionMatcher {
+ private final String prefix;
+
+ Prefix(String pfx, AccessSection section) {
+ super(section);
+ prefix = pfx;
+ }
+
+ @Override
+ boolean match(String ref, String username) {
+ return ref.startsWith(prefix);
+ }
+ }
+
+ private static class Regexp extends SectionMatcher {
+ private final Pattern pattern;
+
+ Regexp(String re, AccessSection section) {
+ super(section);
+ pattern = Pattern.compile(re);
+ }
+
+ @Override
+ boolean match(String ref, String username) {
+ return pattern.matcher(ref).matches();
+ }
+ }
+
+ static class ExpandParameters extends SectionMatcher {
+ private final ParamertizedString template;
+ private final String prefix;
+
+ ExpandParameters(String pattern, AccessSection section) {
+ super(section);
+ template = new ParamertizedString(pattern);
+
+ if (isRE(pattern)) {
+ // Replace ${username} with ":USERNAME:" as : is not legal
+ // in a reference and the string :USERNAME: is not likely to
+ // be a valid part of the regex. This later allows the pattern
+ // prefix to be clipped, saving time on evaluation.
+ Automaton am = RefControl.toRegExp(
+ template.replace(Collections.singletonMap("username", ":USERNAME:")))
+ .toAutomaton();
+ String rePrefix = am.getCommonPrefix();
+ prefix = rePrefix.substring(0, rePrefix.indexOf(":USERNAME:"));
+ } else {
+ prefix = pattern.substring(0, pattern.indexOf("${"));
+ }
+ }
+
+ @Override
+ boolean match(String ref, String username) {
+ if (!ref.startsWith(prefix) || username == null) {
+ return false;
+ }
+
+ String u;
+ if (isRE(template.getPattern())) {
+ u = username.replace(".", "\\.");
+ } else {
+ u = username;
+ }
+
+ SectionMatcher next = wrap(
+ template.replace(Collections.singletonMap("username", u)),
+ section);
+ return next != null ? next.match(ref, username) : false;
+ }
+
+ boolean matchPrefix(String ref) {
+ return ref.startsWith(prefix);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
new file mode 100644
index 0000000..c0d2034
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
@@ -0,0 +1,263 @@
+// 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.git;
+
+package com.google.gerrit.server.project;
+
+import static com.google.gerrit.server.project.RefControl.isRE;
+import static com.google.gerrit.server.project.RefControl.shortestExample;
+import static com.google.gerrit.server.project.RefControl.toRegExp;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.server.cache.Cache;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+
+import org.apache.commons.lang.StringUtils;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.IdentityHashMap;
+import java.util.List;
+
+/** Caches the order AccessSections should be sorted for evaluation. */
+@Singleton
+public class SectionSortCache {
+ private static final String CACHE_NAME = "permission_sort";
+
+ public static Module module() {
+ return new CacheModule() {
+ @Override
+ protected void configure() {
+ final TypeLiteral<Cache<EntryKey, EntryVal>> type =
+ new TypeLiteral<Cache<EntryKey, EntryVal>>() {};
+ core(type, CACHE_NAME);
+ bind(SectionSortCache.class);
+ }
+ };
+ }
+
+ private final Cache<EntryKey, EntryVal> cache;
+
+ @Inject
+ SectionSortCache(@Named(CACHE_NAME) Cache<EntryKey, EntryVal> cache) {
+ this.cache = cache;
+ }
+
+ void sort(String ref, List<AccessSection> sections) {
+ final int cnt = sections.size();
+ if (cnt <= 1) {
+ return;
+ }
+
+ EntryKey key = new EntryKey(ref, sections);
+ EntryVal val = cache.get(key);
+ if (val != null) {
+ int[] srcIdx = val.order;
+ if (srcIdx != null) {
+ AccessSection[] srcList = copy(sections);
+ for (int i = 0; i < cnt; i++) {
+ sections.set(i, srcList[srcIdx[i]]);
+ }
+ } else {
+ // Identity transform. No sorting is required.
+ }
+
+ } else {
+ IdentityHashMap<AccessSection, Integer> srcMap =
+ new IdentityHashMap<AccessSection, Integer>();
+ for (int i = 0; i < cnt; i++) {
+ srcMap.put(sections.get(i), i);
+ }
+
+ Collections.sort(sections, new MostSpecificComparator(ref));
+
+ int srcIdx[];
+ if (isIdentityTransform(sections, srcMap)) {
+ srcIdx = null;
+ } else {
+ srcIdx = new int[cnt];
+ for (int i = 0; i < cnt; i++) {
+ srcIdx[i] = srcMap.get(sections.get(i));
+ }
+ }
+
+ cache.put(key, new EntryVal(srcIdx));
+ }
+ }
+
+ private static AccessSection[] copy(List<AccessSection> sections) {
+ return sections.toArray(new AccessSection[sections.size()]);
+ }
+
+ private static boolean isIdentityTransform(List<AccessSection> sections,
+ IdentityHashMap<AccessSection, Integer> srcMap) {
+ for (int i = 0; i < sections.size(); i++) {
+ if (i != srcMap.get(sections.get(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ static final class EntryKey {
+ private final String ref;
+ private final String[] patterns;
+ private final int hashCode;
+
+ EntryKey(String refName, List<AccessSection> sections) {
+ int hc = refName.hashCode();
+ ref = refName;
+ patterns = new String[sections.size()];
+ for (int i = 0; i < patterns.length; i++) {
+ String n = sections.get(i).getName();
+ patterns[i] = n;
+ hc = hc * 31 + n.hashCode();
+ }
+ hashCode = hc;
+ }
+
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof EntryKey) {
+ EntryKey b = (EntryKey) other;
+ return ref.equals(b.ref) && Arrays.equals(patterns, b.patterns);
+ }
+ return false;
+ }
+ }
+
+ static final class EntryVal {
+ /**
+ * Maps the input index to the output index.
+ * <p>
+ * For {@code x == order[y]} the expression means move the item at
+ * source position {@code x} to the output position {@code y}.
+ */
+ final int[] order;
+
+ EntryVal(int[] order) {
+ this.order = order;
+ }
+ }
+
+ /**
+ * Order the Ref Pattern by the most specific. This sort is done by:
+ * <ul>
+ * <li>1 - The minor value of Levenshtein string distance between the branch
+ * name and the regex string shortest example. A shorter distance is a more
+ * specific match.
+ * <li>2 - Finites first, infinities after.
+ * <li>3 - Number of transitions.
+ * <li>4 - Length of the expression text.
+ * </ul>
+ *
+ * Levenshtein distance is a measure of the similarity between two strings.
+ * The distance is the number of deletions, insertions, or substitutions
+ * required to transform one string into another.
+ *
+ * For example, if given refs/heads/m* and refs/heads/*, the distances are 5
+ * and 6. It means that refs/heads/m* is more specific because it's closer to
+ * refs/heads/master than refs/heads/*.
+ *
+ * Another example could be refs/heads/* and refs/heads/[a-zA-Z]*, the
+ * distances are both 6. Both are infinite, but refs/heads/[a-zA-Z]* has more
+ * transitions, which after all turns it more specific.
+ */
+ private static final class MostSpecificComparator implements
+ Comparator<AccessSection> {
+ private final String refName;
+
+ MostSpecificComparator(String refName) {
+ this.refName = refName;
+ }
+
+ public int compare(AccessSection a, AccessSection b) {
+ return compare(a.getName(), b.getName());
+ }
+
+ private int compare(final String pattern1, final String pattern2) {
+ int cmp = distance(pattern1) - distance(pattern2);
+ if (cmp == 0) {
+ boolean p1_finite = finite(pattern1);
+ boolean p2_finite = finite(pattern2);
+
+ if (p1_finite && !p2_finite) {
+ cmp = -1;
+ } else if (!p1_finite && p2_finite) {
+ cmp = 1;
+ } else /* if (f1 == f2) */{
+ cmp = 0;
+ }
+ }
+ if (cmp == 0) {
+ cmp = transitions(pattern1) - transitions(pattern2);
+ }
+ if (cmp == 0) {
+ cmp = pattern2.length() - pattern1.length();
+ }
+ return cmp;
+ }
+
+ private int distance(String pattern) {
+ String example;
+ if (isRE(pattern)) {
+ example = shortestExample(pattern);
+
+ } else if (pattern.endsWith("/*")) {
+ example = pattern.substring(0, pattern.length() - 1) + '1';
+
+ } else if (pattern.equals(refName)) {
+ return 0;
+
+ } else {
+ return Math.max(pattern.length(), refName.length());
+ }
+ return StringUtils.getLevenshteinDistance(example, refName);
+ }
+
+ private boolean finite(String pattern) {
+ if (isRE(pattern)) {
+ return toRegExp(pattern).toAutomaton().isFinite();
+
+ } else if (pattern.endsWith("/*")) {
+ return false;
+
+ } else {
+ return true;
+ }
+ }
+
+ private int transitions(String pattern) {
+ if (isRE(pattern)) {
+ return toRegExp(pattern).toAutomaton().getNumberOfTransitions();
+
+ } else if (pattern.endsWith("/*")) {
+ return pattern.length();
+
+ } else {
+ return pattern.length();
+ }
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java
index ffed95a..7b8b4b9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java
@@ -76,11 +76,4 @@
* the valid status into.
*/
public abstract void run(ApprovalType at, FunctionState state);
-
- public boolean isValid(final CurrentUser user, final ApprovalType at,
- final FunctionState state) {
- return !state.controlFor(user) //
- .getRange(Permission.forLabel(at.getCategory().getLabelName())) //
- .isEmpty();
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
index 2cb3e81..2a871f7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
@@ -27,10 +27,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.project.ChangeControl;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -44,7 +41,7 @@
/** State passed through to a {@link CategoryFunction}. */
public class FunctionState {
public interface Factory {
- FunctionState create(Change c, PatchSet.Id psId,
+ FunctionState create(ChangeControl c, PatchSet.Id psId,
Collection<PatchSetApproval> all);
}
@@ -55,20 +52,19 @@
new HashMap<ApprovalCategory.Id, Collection<PatchSetApproval>>();
private final Map<ApprovalCategory.Id, Boolean> valid =
new HashMap<ApprovalCategory.Id, Boolean>();
+ private final ChangeControl callerChangeControl;
private final Change change;
- private final ProjectState project;
@Inject
FunctionState(final ApprovalTypes approvalTypes,
- final ProjectCache projectCache,
final IdentifiedUser.GenericFactory userFactory, final GroupCache egc,
- @Assisted final Change c, @Assisted final PatchSet.Id psId,
+ @Assisted final ChangeControl c, @Assisted final PatchSet.Id psId,
@Assisted final Collection<PatchSetApproval> all) {
this.approvalTypes = approvalTypes;
this.userFactory = userFactory;
- change = c;
- project = projectCache.get(change.getProject());
+ callerChangeControl = c;
+ change = c.getChange();
for (final PatchSetApproval ca : all) {
if (psId.equals(ca.getPatchSetId())) {
@@ -147,10 +143,8 @@
a.setValue((short) range.squash(a.getValue()));
}
- RefControl controlFor(final CurrentUser user) {
- ProjectControl pc = project.controlFor(user);
- RefControl rc = pc.controlForRef(change.getDest().get());
- return rc;
+ private ChangeControl controlFor(CurrentUser user) {
+ return callerChangeControl.forUser(user);
}
/** Run <code>applyTypeFloor</code>, <code>applyRightFloor</code>. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoBlock.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoBlock.java
index a089817..08d0705 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoBlock.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoBlock.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.workflow;
import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.server.CurrentUser;
/** A function that does nothing. */
public class NoBlock extends CategoryFunction {
@@ -25,10 +24,4 @@
public void run(final ApprovalType at, final FunctionState state) {
state.valid(at, true);
}
-
- @Override
- public boolean isValid(final CurrentUser user, final ApprovalType at,
- final FunctionState state) {
- return true;
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoOpFunction.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoOpFunction.java
index 6d2c26c..8c76cc5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoOpFunction.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoOpFunction.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.workflow;
import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.server.CurrentUser;
/** A function that does nothing. */
public class NoOpFunction extends CategoryFunction {
@@ -24,10 +23,4 @@
@Override
public void run(final ApprovalType at, final FunctionState state) {
}
-
- @Override
- public boolean isValid(final CurrentUser user, final ApprovalType at,
- final FunctionState state) {
- return false;
- }
}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index 90cc342..0981da1 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -33,6 +33,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.CapabilityControl;
import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.cache.ConcurrentHashMapCache;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -195,6 +196,30 @@
u.controlForRef("refs/heads/master").canUpload());
}
+ public void testUsernamePatternNonRegex() {
+ grant(local, READ, devs, "refs/sb/${username}/heads/*");
+
+ ProjectControl u = user("u", devs), d = user("d", devs);
+ assertFalse("u can't read", u.controlForRef("refs/sb/d/heads/foobar").isVisible());
+ assertTrue("d can read", d.controlForRef("refs/sb/d/heads/foobar").isVisible());
+ }
+
+ public void testUsernamePatternWithRegex() {
+ grant(local, READ, devs, "^refs/sb/${username}/heads/.*");
+
+ ProjectControl u = user("d.v", devs), d = user("dev", devs);
+ assertFalse("u can't read", u.controlForRef("refs/sb/dev/heads/foobar").isVisible());
+ assertTrue("d can read", d.controlForRef("refs/sb/dev/heads/foobar").isVisible());
+ }
+
+ public void testSortWithRegex() {
+ grant(local, READ, devs, "^refs/heads/.*");
+ grant(parent, READ, anonymous, "^refs/heads/.*-QA-.*");
+
+ ProjectControl u = user(devs), d = user(devs);
+ assertTrue("u can read", u.controlForRef("refs/heads/foo-QA-bar").isVisible());
+ assertTrue("d can read", d.controlForRef("refs/heads/foo-QA-bar").isVisible());
+ }
// -----------------------------------------------------------------------
@@ -204,6 +229,8 @@
private ProjectConfig local;
private ProjectConfig parent;
+ private PermissionCollection.Factory sectionSorter;
+
private final AccountGroup.UUID admin = new AccountGroup.UUID("test.admin");
private final AccountGroup.UUID anonymous = AccountGroup.ANONYMOUS_USERS;
private final AccountGroup.UUID registered = AccountGroup.REGISTERED_USERS;
@@ -273,6 +300,11 @@
local = new ProjectConfig(new Project.NameKey("local"));
local.createInMemory();
local.getProject().setParentName(parent.getProject().getName());
+
+ sectionSorter =
+ new PermissionCollection.Factory(
+ new SectionSortCache(
+ new ConcurrentHashMapCache<SectionSortCache.EntryKey, SectionSortCache.EntryVal>()));
}
private static void assertOwner(String ref, ProjectControl u) {
@@ -307,19 +339,18 @@
}
private ProjectControl user(AccountGroup.UUID... memberOf) {
+ return user(null, memberOf);
+ }
+
+ private ProjectControl user(String name, AccountGroup.UUID... memberOf) {
SchemaFactory<ReviewDb> schema = null;
GroupCache groupCache = null;
String canonicalWebUrl = "http://localhost";
- RefControl.Factory refControlFactory = new RefControl.Factory() {
- @Override
- public RefControl create(final ProjectControl projectControl, final String ref) {
- return new RefControl(projectControl, ref);
- }
- };
return new ProjectControl(Collections.<AccountGroup.UUID> emptySet(),
Collections.<AccountGroup.UUID> emptySet(), schema, groupCache,
- canonicalWebUrl, refControlFactory, new MockUser(memberOf),
+ sectionSorter,
+ canonicalWebUrl, new MockUser(name, memberOf),
newProjectState());
}
@@ -338,10 +369,12 @@
}
private class MockUser extends CurrentUser {
+ private final String username;
private final Set<AccountGroup.UUID> groups;
- MockUser(AccountGroup.UUID[] groupId) {
+ MockUser(String name, AccountGroup.UUID[] groupId) {
super(RefControlTest.this.capabilityControlFactory, AccessPath.UNKNOWN);
+ username = name;
groups = new HashSet<AccountGroup.UUID>(Arrays.asList(groupId));
groups.add(registered);
groups.add(anonymous);
@@ -353,6 +386,11 @@
}
@Override
+ public String getUserName() {
+ return username;
+ }
+
+ @Override
public Set<Change.Id> getStarredChanges() {
return Collections.emptySet();
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 0f63577..467488a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -380,7 +380,7 @@
new PatchSetApproval(new PatchSetApproval.Key(patchSetId, currentUser
.getAccountId(), ao.getCategoryId()), v);
final FunctionState fs =
- functionStateFactory.create(changeControl.getChange(), patchSetId,
+ functionStateFactory.create(changeControl, patchSetId,
Collections.<PatchSetApproval> emptyList());
psa.setValue(v);
fs.normalize(approvalTypes.byId(psa.getCategoryId()), psa);