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
This commit is contained in:
@@ -432,6 +432,13 @@ Caches a mapping of LDAP username to Gerrit account identity. The
|
||||
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
|
||||
|
||||
@@ -154,8 +154,7 @@ public class ChangeDetailFactory extends Handler<ChangeDetail> {
|
||||
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);
|
||||
|
||||
@@ -182,7 +182,7 @@ class PatchDetailServiceImpl extends BaseServiceImplementation implements
|
||||
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 @@ class PatchDetailServiceImpl extends BaseServiceImplementation implements
|
||||
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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,19 +129,21 @@ 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) {
|
||||
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 @@ 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();
|
||||
}
|
||||
|
||||
/** 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());
|
||||
}
|
||||
}
|
||||
|
||||
54
gerrit-server/src/main/java/com/google/gerrit/server/cache/ConcurrentHashMapCache.java
vendored
Normal file
54
gerrit-server/src/main/java/com/google/gerrit/server/cache/ConcurrentHashMapCache.java
vendored
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -58,10 +58,11 @@ import com.google.gerrit.server.patch.PatchListCacheImpl;
|
||||
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 @@ public class GerritGlobalModule extends FactoryModule {
|
||||
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 @@ public class GerritGlobalModule extends FactoryModule {
|
||||
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);
|
||||
|
||||
@@ -40,6 +40,7 @@ import com.google.gerrit.server.patch.AddReviewer;
|
||||
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 @@ public class GerritRequestModule extends FactoryModule {
|
||||
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);
|
||||
|
||||
@@ -40,6 +40,8 @@ import com.google.gerrit.server.mail.MergeFailSender;
|
||||
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 @@ public class MergeOp {
|
||||
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 @@ public class MergeOp {
|
||||
@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 @@ public class MergeOp {
|
||||
this.approvalTypes = approvalTypes;
|
||||
patchSetInfoFactory = psif;
|
||||
identifiedUserFactory = iuf;
|
||||
this.changeControlFactory = changeControlFactory;
|
||||
this.mergeQueue = mergeQueue;
|
||||
this.hooks = hooks;
|
||||
this.accountCache = accountCache;
|
||||
@@ -1240,7 +1245,11 @@ public class MergeOp {
|
||||
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 @@ public class MergeOp {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ public class PublishComments implements Callable<VoidResult> {
|
||||
|
||||
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 @@ public class PublishComments implements Callable<VoidResult> {
|
||||
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 @@ public class PublishComments implements Callable<VoidResult> {
|
||||
// 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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 com.google.inject.assistedinject.Assisted;
|
||||
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 class ProjectControl {
|
||||
}
|
||||
|
||||
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 @@ public class ProjectControl {
|
||||
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 class ProjectControl {
|
||||
}
|
||||
|
||||
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 class ProjectControl {
|
||||
}
|
||||
|
||||
public Project getProject() {
|
||||
return getProjectState().getProject();
|
||||
return state.getProject();
|
||||
}
|
||||
|
||||
/** Can this user see this project exists? */
|
||||
@@ -203,20 +210,27 @@ public class ProjectControl {
|
||||
|
||||
/** 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 @@ public class ProjectControl {
|
||||
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 @@ public class ProjectControl {
|
||||
// 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 @@ public class ProjectControl {
|
||||
|
||||
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 @@ public class ProjectControl {
|
||||
return all;
|
||||
}
|
||||
|
||||
Collection<AccessSection> access() {
|
||||
if (access == null) {
|
||||
access = state.getAllAccessSections();
|
||||
private List<SectionMatcher> access() {
|
||||
if (allSections == null) {
|
||||
allSections = state.getAllSections();
|
||||
}
|
||||
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);
|
||||
}
|
||||
return access;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.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 +67,11 @@ public class ProjectState {
|
||||
/** 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 @@ 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,64 +137,77 @@ 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;
|
||||
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 @@ public class ProjectState {
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
|
||||
@@ -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 org.eclipse.jgit.revwalk.RevWalk;
|
||||
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 class RefControl {
|
||||
}
|
||||
|
||||
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 @@ public class RefControl {
|
||||
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 @@ public class RefControl {
|
||||
|
||||
/** Can this user see this reference exists? */
|
||||
public boolean isVisible() {
|
||||
return getProjectControl().visibleForReplication()
|
||||
return projectControl.visibleForReplication()
|
||||
|| canPerform(Permission.READ);
|
||||
}
|
||||
|
||||
@@ -129,14 +109,14 @@ public class RefControl {
|
||||
* 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 @@ public class RefControl {
|
||||
// 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 @@ public class RefControl {
|
||||
/** @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 @@ public class RefControl {
|
||||
|
||||
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 @@ public class RefControl {
|
||||
/** 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 @@ public class RefControl {
|
||||
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 @@ public class RefControl {
|
||||
|
||||
/** 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 @@ public class RefControl {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,11 +76,4 @@ public abstract class CategoryFunction {
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,7 @@ import com.google.gerrit.reviewdb.ApprovalCategory.Id;
|
||||
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 @@ import java.util.Map;
|
||||
/** 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 @@ public class FunctionState {
|
||||
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 @@ public class FunctionState {
|
||||
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>. */
|
||||
|
||||
@@ -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 class NoBlock extends CategoryFunction {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 @@ public class NoOpFunction extends CategoryFunction {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.google.gerrit.server.AccessPath;
|
||||
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 @@ public class RefControlTest extends TestCase {
|
||||
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 @@ public class RefControlTest extends TestCase {
|
||||
|
||||
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 @@ public class RefControlTest extends TestCase {
|
||||
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 @@ public class RefControlTest extends TestCase {
|
||||
}
|
||||
|
||||
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 @@ public class RefControlTest extends TestCase {
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -352,6 +385,11 @@ public class RefControlTest extends TestCase {
|
||||
return groups;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUserName() {
|
||||
return username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Change.Id> getStarredChanges() {
|
||||
return Collections.emptySet();
|
||||
|
||||
@@ -380,7 +380,7 @@ public class ReviewCommand extends BaseCommand {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user