Merge changes If917e10d,Ic5fae13c

* changes:
  Rewrite permission evaluation
  RefControlTest: BLOCK aggregates all the forbidden votes across projects
This commit is contained in:
Edwin Kempin
2018-02-21 16:03:30 +00:00
committed by Gerrit Code Review
4 changed files with 441 additions and 250 deletions

View File

@@ -14,18 +14,18 @@
package com.google.gerrit.server.permissions;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.gerrit.common.data.PermissionRule.Action.BLOCK;
import static com.google.gerrit.server.project.RefPattern.isRE;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.MultimapBuilder;
import com.google.gerrit.common.Nullable;
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.common.data.PermissionRule.Action;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
@@ -35,13 +35,13 @@ import com.google.gerrit.server.project.SectionMatcher;
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.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Effective permissions applied to a reference in a project.
@@ -126,78 +126,134 @@ public class PermissionCollection {
// LinkedHashMap to maintain input ordering.
Map<AccessSection, Project.NameKey> sectionToProject = new LinkedHashMap<>();
boolean perUser = filterRefMatchingSections(matcherList, ref, user, sectionToProject);
List<AccessSection> sections = Lists.newArrayList(sectionToProject.keySet());
// Sort by ref pattern specificity. For equally specific patterns, the sections from the
// project closer to the current one come first.
sorter.sort(ref, sections);
Set<SeenRule> seen = new HashSet<>();
Set<String> exclusiveGroupPermissions = new HashSet<>();
// For block permissions, we want a different order: first, we want to go from parent to child.
List<Map.Entry<AccessSection, Project.NameKey>> accessDescending =
Lists.reverse(Lists.newArrayList(sectionToProject.entrySet()));
HashMap<String, List<PermissionRule>> permissions = new HashMap<>();
HashMap<String, List<PermissionRule>> overridden = new HashMap<>();
Map<PermissionRule, ProjectRef> ruleProps = Maps.newIdentityHashMap();
ListMultimap<Project.NameKey, String> exclusivePermissionsByProject =
MultimapBuilder.hashKeys().arrayListValues().build();
for (AccessSection section : sections) {
Project.NameKey project = sectionToProject.get(section);
for (Permission permission : section.getPermissions()) {
boolean exclusivePermissionExists =
exclusiveGroupPermissions.contains(permission.getName());
for (PermissionRule rule : permission.getRules()) {
SeenRule s = SeenRule.create(section, permission, rule);
boolean addRule;
if (rule.isBlock()) {
addRule = !exclusivePermissionsByProject.containsEntry(project, permission.getName());
} else {
addRule = seen.add(s) && !rule.isDeny() && !exclusivePermissionExists;
}
HashMap<String, List<PermissionRule>> p = null;
if (addRule) {
p = permissions;
} else if (!rule.isDeny() && !exclusivePermissionExists) {
p = overridden;
}
if (p != null) {
List<PermissionRule> r = p.get(permission.getName());
if (r == null) {
r = new ArrayList<>(2);
p.put(permission.getName(), r);
}
r.add(rule);
ruleProps.put(rule, ProjectRef.create(project, section.getName()));
}
}
if (permission.getExclusiveGroup()) {
exclusivePermissionsByProject.put(project, permission.getName());
exclusiveGroupPermissions.add(permission.getName());
}
}
Map<Project.NameKey, List<AccessSection>> accessByProject =
accessDescending
.stream()
.collect(
Collectors.groupingBy(
e -> e.getValue(), LinkedHashMap::new, mapping(e -> e.getKey(), toList())));
// Within each project, sort by ref specificity.
for (List<AccessSection> secs : accessByProject.values()) {
sorter.sort(ref, secs);
}
return new PermissionCollection(permissions, overridden, ruleProps, perUser);
return new PermissionCollection(
Lists.newArrayList(accessByProject.values()), sections, perUser);
}
}
private final Map<String, List<PermissionRule>> rules;
private final Map<String, List<PermissionRule>> overridden;
private final Map<PermissionRule, ProjectRef> ruleProps;
/** Returns permissions in the right order for evaluating BLOCK status. */
List<List<Permission>> getBlockRules(String perm) {
List<List<Permission>> ps = blockPerProjectByPermission.get(perm);
if (ps == null) {
ps = calculateBlockRules(perm);
blockPerProjectByPermission.put(perm, ps);
}
return ps;
}
/** Returns permissions in the right order for evaluating ALLOW/DENY status. */
List<PermissionRule> getAllowRules(String perm) {
List<PermissionRule> ps = rulesByPermission.get(perm);
if (ps == null) {
ps = calculateAllowRules(perm);
rulesByPermission.put(perm, ps);
}
return ps;
}
/** calculates permissions for ALLOW processing. */
private List<PermissionRule> calculateAllowRules(String permName) {
Set<SeenRule> seen = new HashSet<>();
List<PermissionRule> r = new ArrayList<>();
for (AccessSection s : accessSectionsUpward) {
Permission p = s.getPermission(permName);
if (p == null) {
continue;
}
for (PermissionRule pr : p.getRules()) {
SeenRule sr = SeenRule.create(s, pr);
if (seen.contains(sr)) {
// We allow only one rule per (ref-pattern, group) tuple. This is used to implement DENY:
// If we see a DENY before an ALLOW rule, that causes the ALLOW rule to be skipped here,
// negating access.
continue;
}
seen.add(sr);
if (pr.getAction() == BLOCK) {
// Block rules are handled elsewhere.
continue;
}
if (pr.getAction() == PermissionRule.Action.DENY) {
// DENY rules work by not adding ALLOW rules. Nothing else to do.
continue;
}
r.add(pr);
}
if (p.getExclusiveGroup()) {
// We found an exclusive permission, so no need to further go up the hierarchy.
break;
}
}
return r;
}
// Calculates the inputs for determining BLOCK status, grouped by project.
private List<List<Permission>> calculateBlockRules(String permName) {
List<List<Permission>> result = new ArrayList<>();
for (List<AccessSection> secs : this.accessSectionsPerProjectDownward) {
List<Permission> perms = new ArrayList<>();
boolean blockFound = false;
for (AccessSection sec : secs) {
Permission p = sec.getPermission(permName);
if (p == null) {
continue;
}
for (PermissionRule pr : p.getRules()) {
if (blockFound || pr.getAction() == Action.BLOCK) {
blockFound = true;
break;
}
}
perms.add(p);
}
if (blockFound) {
result.add(perms);
}
}
return result;
}
private List<List<AccessSection>> accessSectionsPerProjectDownward;
private List<AccessSection> accessSectionsUpward;
private final Map<String, List<PermissionRule>> rulesByPermission;
private final Map<String, List<List<Permission>>> blockPerProjectByPermission;
private final boolean perUser;
private PermissionCollection(
Map<String, List<PermissionRule>> rules,
Map<String, List<PermissionRule>> overridden,
Map<PermissionRule, ProjectRef> ruleProps,
List<List<AccessSection>> accessSectionsDownward,
List<AccessSection> accessSectionsUpward,
boolean perUser) {
this.rules = rules;
this.overridden = overridden;
this.ruleProps = ruleProps;
this.accessSectionsPerProjectDownward = accessSectionsDownward;
this.accessSectionsUpward = accessSectionsUpward;
this.rulesByPermission = new HashMap<>();
this.blockPerProjectByPermission = new HashMap<>();
this.perUser = perUser;
}
@@ -209,55 +265,18 @@ public class PermissionCollection {
return perUser;
}
/**
* 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();
}
List<PermissionRule> getOverridden(String permissionName) {
return firstNonNull(overridden.get(permissionName), Collections.<PermissionRule>emptyList());
}
ProjectRef getRuleProps(PermissionRule rule) {
return ruleProps.get(rule);
}
/**
* 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();
}
/** (ref, permission, group) tuple. */
@AutoValue
abstract static class SeenRule {
public abstract String refPattern();
public abstract String permissionName();
@Nullable
public abstract AccountGroup.UUID group();
static SeenRule create(
AccessSection section, Permission permission, @Nullable PermissionRule rule) {
static SeenRule create(AccessSection section, @Nullable PermissionRule rule) {
AccountGroup.UUID group =
rule != null && rule.getGroup() != null ? rule.getGroup().getUUID() : null;
return new AutoValue_PermissionCollection_SeenRule(
section.getName(), permission.getName(), group);
return new AutoValue_PermissionCollection_SeenRule(section.getName(), group);
}
}
}

View File

@@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkArgument;
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.common.data.PermissionRule.Action;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
import com.google.gerrit.reviewdb.client.Change;
@@ -32,14 +33,9 @@ import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.util.MagicBranch;
import com.google.gwtorm.server.OrmException;
import com.google.inject.util.Providers;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Manages access control for Git references (aka branches, tags). */
@@ -50,8 +46,7 @@ class RefControl {
/** 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;
// The next 4 members are cached canPerform() permissions.
private Boolean owner;
private Boolean canForgeAuthor;
@@ -62,7 +57,6 @@ class RefControl {
this.projectControl = projectControl;
this.refName = ref;
this.relevant = relevant;
this.effective = new HashMap<>();
}
ProjectControl getProjectControl() {
@@ -124,12 +118,12 @@ class RefControl {
// granting of powers beyond submitting to the configuration.
return projectControl.isOwner();
}
return canPerform(Permission.SUBMIT, isChangeOwner);
return canPerform(Permission.SUBMIT, isChangeOwner, false);
}
/** @return true if this user can force edit topic names. */
boolean canForceEditTopicName() {
return canForcePerform(Permission.EDIT_TOPIC_NAME);
return canPerform(Permission.EDIT_TOPIC_NAME, false, true);
}
/** The range of permitted values associated with a label permission. */
@@ -140,14 +134,14 @@ class RefControl {
/** The range of permitted values associated with a label permission. */
PermissionRange getRange(String permission, boolean isChangeOwner) {
if (Permission.hasRange(permission)) {
return toRange(permission, access(permission, isChangeOwner));
return toRange(permission, isChangeOwner);
}
return null;
}
/** True if the user has this permission. Works only for non labels. */
boolean canPerform(String permissionName) {
return canPerform(permissionName, false);
return canPerform(permissionName, false, false);
}
ForRef asForRef() {
@@ -198,7 +192,7 @@ class RefControl {
case UNKNOWN:
case WEB_BROWSER:
default:
return (isOwner() && !isForceBlocked(Permission.PUSH)) || projectControl.isAdmin();
return (isOwner() && !isBlocked(Permission.PUSH, false, true)) || projectControl.isAdmin();
}
}
@@ -211,7 +205,7 @@ class RefControl {
// granting of powers beyond pushing to the configuration.
return false;
}
return canForcePerform(Permission.PUSH);
return canPerform(Permission.PUSH, false, true);
}
/**
@@ -239,7 +233,10 @@ class RefControl {
case UNKNOWN:
case WEB_BROWSER:
default:
return (isOwner() && !isForceBlocked(Permission.PUSH))
return
// We allow owner to delete refs even if they have no force-push rights. We forbid
// it if force push is blocked, though. See commit 40bd5741026863c99bea13eb5384bd27855c5e1b
(isOwner() && !isBlocked(Permission.PUSH, false, true))
|| canPushWithForce()
|| canPerform(Permission.DELETE)
|| projectControl.isAdmin();
@@ -267,158 +264,140 @@ class RefControl {
return canPerform(Permission.FORGE_SERVER);
}
private static class AllowedRange {
private int allowMin;
private int allowMax;
private int blockMin = Integer.MIN_VALUE;
private int blockMax = Integer.MAX_VALUE;
void update(PermissionRule rule) {
if (rule.isBlock()) {
blockMin = Math.max(blockMin, rule.getMin());
blockMax = Math.min(blockMax, rule.getMax());
} else {
allowMin = Math.min(allowMin, rule.getMin());
allowMax = Math.max(allowMax, rule.getMax());
}
}
int getAllowMin() {
return allowMin;
}
int getAllowMax() {
return allowMax;
}
int getBlockMin() {
// ALLOW wins over BLOCK on the same project
return Math.min(blockMin, allowMin - 1);
}
int getBlockMax() {
// ALLOW wins over BLOCK on the same project
return Math.max(blockMax, allowMax + 1);
}
private static boolean isAllow(PermissionRule pr, boolean withForce) {
return pr.getAction() == Action.ALLOW && (pr.getForce() || !withForce);
}
private PermissionRange toRange(String permissionName, List<PermissionRule> ruleList) {
Map<ProjectRef, AllowedRange> ranges = new HashMap<>();
for (PermissionRule rule : ruleList) {
ProjectRef p = relevant.getRuleProps(rule);
AllowedRange r = ranges.get(p);
if (r == null) {
r = new AllowedRange();
ranges.put(p, r);
}
r.update(rule);
}
int allowMin = 0;
int allowMax = 0;
int blockMin = Integer.MIN_VALUE;
int blockMax = Integer.MAX_VALUE;
for (AllowedRange r : ranges.values()) {
allowMin = Math.min(allowMin, r.getAllowMin());
allowMax = Math.max(allowMax, r.getAllowMax());
blockMin = Math.max(blockMin, r.getBlockMin());
blockMax = Math.min(blockMax, r.getBlockMax());
}
// BLOCK wins over ALLOW across projects
int min = Math.max(allowMin, blockMin + 1);
int max = Math.min(allowMax, blockMax - 1);
return new PermissionRange(permissionName, min, max);
private static boolean isBlock(PermissionRule pr, boolean withForce) {
// BLOCK with force specified is a weaker rule than without.
return pr.getAction() == Action.BLOCK && (!pr.getForce() || withForce);
}
private boolean canPerform(String permissionName, boolean isChangeOwner) {
List<PermissionRule> access = access(permissionName, isChangeOwner);
List<PermissionRule> overridden = relevant.getOverridden(permissionName);
Set<ProjectRef> allows = new HashSet<>();
Set<ProjectRef> blocks = new HashSet<>();
for (PermissionRule rule : access) {
if (rule.isBlock() && !rule.getForce()) {
blocks.add(relevant.getRuleProps(rule));
} else {
allows.add(relevant.getRuleProps(rule));
private PermissionRange toRange(String permissionName, boolean isChangeOwner) {
int blockAllowMin = Integer.MIN_VALUE, blockAllowMax = Integer.MAX_VALUE;
projectLoop:
for (List<Permission> ps : relevant.getBlockRules(permissionName)) {
boolean blockFound = false;
int projectBlockAllowMin = Integer.MIN_VALUE, projectBlockAllowMax = Integer.MAX_VALUE;
for (Permission p : ps) {
if (p.getExclusiveGroup()) {
for (PermissionRule pr : p.getRules()) {
if (pr.getAction() == Action.ALLOW && projectControl.match(pr, isChangeOwner)) {
// exclusive override, usually for a more specific ref.
continue projectLoop;
}
}
}
for (PermissionRule pr : p.getRules()) {
if (pr.getAction() == Action.BLOCK && projectControl.match(pr, isChangeOwner)) {
projectBlockAllowMin = pr.getMin() + 1;
projectBlockAllowMax = pr.getMax() - 1;
blockFound = true;
}
}
if (blockFound) {
for (PermissionRule pr : p.getRules()) {
if (pr.getAction() == Action.ALLOW && projectControl.match(pr, isChangeOwner)) {
projectBlockAllowMin = pr.getMin();
projectBlockAllowMax = pr.getMax();
break;
}
}
break;
}
}
blockAllowMin = Math.max(projectBlockAllowMin, blockAllowMin);
blockAllowMax = Math.min(projectBlockAllowMax, blockAllowMax);
}
int voteMin = 0, voteMax = 0;
for (PermissionRule pr : relevant.getAllowRules(permissionName)) {
if (pr.getAction() == PermissionRule.Action.ALLOW
&& projectControl.match(pr, isChangeOwner)) {
// For votes, contrary to normal permissions, we aggregate all applicable rules.
voteMin = Math.min(voteMin, pr.getMin());
voteMax = Math.max(voteMax, pr.getMax());
}
}
for (PermissionRule rule : overridden) {
blocks.remove(relevant.getRuleProps(rule));
}
blocks.removeAll(allows);
return blocks.isEmpty() && !allows.isEmpty();
return new PermissionRange(
permissionName, Math.max(voteMin, blockAllowMin), Math.min(voteMax, blockAllowMax));
}
/** True if the user has force this permission. Works only for non labels. */
private boolean canForcePerform(String permissionName) {
List<PermissionRule> access = access(permissionName);
List<PermissionRule> overridden = relevant.getOverridden(permissionName);
Set<ProjectRef> allows = new HashSet<>();
Set<ProjectRef> blocks = new HashSet<>();
for (PermissionRule rule : access) {
if (rule.isBlock()) {
blocks.add(relevant.getRuleProps(rule));
} else if (rule.getForce()) {
allows.add(relevant.getRuleProps(rule));
private boolean isBlocked(String permissionName, boolean isChangeOwner, boolean withForce) {
// Permissions are ordered by (more general project, more specific ref). Because Permission
// does not have back pointers, we can't tell what ref-pattern or project each permission comes
// from.
List<List<Permission>> downwardPerProject = relevant.getBlockRules(permissionName);
projectLoop:
for (List<Permission> projectRules : downwardPerProject) {
boolean overrideFound = false;
for (Permission p : projectRules) {
// If this is an exclusive ALLOW, then block rules from the same project are ignored.
if (p.getExclusiveGroup()) {
for (PermissionRule pr : p.getRules()) {
if (isAllow(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
overrideFound = true;
break;
}
}
}
if (overrideFound) {
// Found an exclusive override, nothing further to do in this project.
continue projectLoop;
}
boolean blocked = false;
for (PermissionRule pr : p.getRules()) {
if (!withForce && pr.getForce()) {
// force on block rule only applies to withForce permission.
continue;
}
if (isBlock(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
blocked = true;
break;
}
}
if (blocked) {
// ALLOW in the same AccessSection (ie. in the same Permission) overrides the BLOCK.
for (PermissionRule pr : p.getRules()) {
if (isAllow(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
blocked = false;
break;
}
}
}
if (blocked) {
return true;
}
}
}
for (PermissionRule rule : overridden) {
if (rule.getForce()) {
blocks.remove(relevant.getRuleProps(rule));
}
}
blocks.removeAll(allows);
return blocks.isEmpty() && !allows.isEmpty();
return false;
}
/** True if for this permission force is blocked for the user. Works only for non labels. */
private boolean isForceBlocked(String permissionName) {
List<PermissionRule> access = access(permissionName);
List<PermissionRule> overridden = relevant.getOverridden(permissionName);
Set<ProjectRef> allows = new HashSet<>();
Set<ProjectRef> blocks = new HashSet<>();
for (PermissionRule rule : access) {
if (rule.isBlock()) {
blocks.add(relevant.getRuleProps(rule));
} else if (rule.getForce()) {
allows.add(relevant.getRuleProps(rule));
}
}
for (PermissionRule rule : overridden) {
if (rule.getForce()) {
blocks.remove(relevant.getRuleProps(rule));
}
}
blocks.removeAll(allows);
return !blocks.isEmpty();
}
/** Rules for the given permission, or the empty list. */
private List<PermissionRule> access(String permissionName) {
return access(permissionName, false);
}
/** Rules for the given permission, or the empty list. */
private List<PermissionRule> access(String permissionName, boolean isChangeOwner) {
List<PermissionRule> rules = effective.get(permissionName);
if (rules != null) {
return rules;
/** True if the user has this permission. */
private boolean canPerform(String permissionName, boolean isChangeOwner, boolean withForce) {
if (isBlocked(permissionName, isChangeOwner, withForce)) {
return false;
}
rules = relevant.getPermission(permissionName);
List<PermissionRule> mine = new ArrayList<>(rules.size());
for (PermissionRule rule : rules) {
if (projectControl.match(rule, isChangeOwner)) {
mine.add(rule);
for (PermissionRule pr : relevant.getAllowRules(permissionName)) {
if (isAllow(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
return true;
}
}
if (mine.isEmpty()) {
mine = Collections.emptyList();
}
effective.put(permissionName, mine);
return mine;
return false;
}
private class ForRefImpl extends ForRef {