Overrule BLOCK with ALLOW on the same project

It was impossible to block a permission for a group and allow the same
permission for a sub-group of that group as the block always won over an
allow. For example, it was impossible to block the "Forge Committer"
permission for all users and then allow it only for a couple of
privileged users.

This change gives an ALLOW permission priority over a BLOCK permission
when they are defined in the same access section of a project. To
achieve the above mentioned policy we define:

  [access "refs/heads/*"]
    forgeCommitter = block group Anonymous Users
    forgeCommitter = group Privileged Users

Across projects the BLOCK permission still wins over an ALLOW
permission. This way one cannot override an inherited BLOCK in a
subproject.

Overruling of BLOCK with ALLOW also works for labels i.e. permission
ranges. If a dedicated Verifiers group need to be the only group who can
vote in the Verified label and we must ensure that even project
owners cannot change this policy then we define the following in a
common parent project:

  [access "refs/heads/*"]
    label-Verified = block -1..+1 group Anonymous Users
    label-Verified = -1..+1 group Verifiers

Change-Id: I8e27b6b060d60bb8a846ce62d0338f613a7d7a3e
Signed-off-by: Sasa Zivkov <sasa.zivkov@sap.com>
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
This commit is contained in:
Sasa Zivkov
2013-01-15 18:52:32 +01:00
parent 272275343c
commit cff084b0ff
3 changed files with 259 additions and 44 deletions

View File

@@ -1109,8 +1109,9 @@ non-blocked rules as they wish. This gives best of both worlds:
~~~~~~~~~~~~~~~~~~~
The 'BLOCK' rule blocks a permission globally. An inherited 'BLOCK' rule cannot
be overridden in the inheriting project. Any 'ALLOW' rule which conflicts with
an inherited 'BLOCK' rule will not be honored. Searching for 'BLOCK' rules, in
be overridden in the inheriting project. Any 'ALLOW' rule, from a different
access section or from an inheriting project, which conflicts with an
inherited 'BLOCK' rule will not be honored. Searching for 'BLOCK' rules, in
the chain of parent projects, ignores the Exclusive flag that is normally
applied to access sections.
@@ -1131,6 +1132,26 @@ The interpretation of the 'min..max' range in case of a blocking rule is: block
every vote from '-INFINITE..min' and 'max..INFINITE'. For the example above it
means that the range '-1..+1' is not affected by this block.
'BLOCK' and 'ALLOW' rules in the same access section
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When an access section of a project contains a 'BLOCK' and an 'ALLOW' rule for
the same permission then this 'ALLOW' rule overrides the 'BLOCK' rule:
====
[access "refs/heads/*"]
push = block group X
push = group Y
====
In this case a user which is a member of the group 'Y' will still be allowed to
push to 'refs/heads/*' even if it is a member of the group 'X'.
NOTE: An 'ALLOW' rule overrides a 'BLOCK' rule only when both of them are
inside the same access section of the same project. An 'ALLOW' rule in a
different access section of the same project or in any access section in an
inheriting project cannot override a 'BLOCK' rule.
Examples
~~~~~~~~
@@ -1160,6 +1181,23 @@ owners>> are allowed to create tags, we would extend the example above:
pushTag = group Project Owners
====
Let only a dedicated group vote in a special category
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Assume there is a more restrictive process for submitting changes in stable
release branches which is manifested as a new voting category
'Release-Process'. Assume we want to make sure that only a 'Release Engineers'
group can vote in this category and that even project owners cannot approve
this category. We have to block everyone except the 'Release Engineers' to vote
in this category and, of course, allow 'Release Engineers' to vote in that
category. In the "`All-Projects`" we define the following rules:
====
[access "refs/heads/stable*"]
label-Release-Proces = block -1..+1 group Anonymous Users
label-Release-Proces = -1..+1 group Release Engineers
====
[[conversion_table]]
Conversion table from 2.1.x series to 2.2.x series
--------------------------------------------------

View File

@@ -14,6 +14,8 @@
package com.google.gerrit.server.project;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRange;
@@ -42,6 +44,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Manages access control for Git references (aka branches, tags). */
@@ -117,18 +120,18 @@ public class RefControl {
*/
public boolean isVisibleByRegisteredUsers() {
List<PermissionRule> access = relevant.getPermission(Permission.READ);
Set<ProjectRef> allows = Sets.newHashSet();
Set<ProjectRef> blocks = Sets.newHashSet();
for (PermissionRule rule : access) {
if (rule.isBlock()) {
return false;
}
}
for (PermissionRule rule : access) {
if (rule.getGroup().getUUID().equals(AccountGroup.ANONYMOUS_USERS)
blocks.add(relevant.getRuleProps(rule));
} else if (rule.getGroup().getUUID().equals(AccountGroup.ANONYMOUS_USERS)
|| rule.getGroup().getUUID().equals(AccountGroup.REGISTERED_USERS)) {
return true;
allows.add(relevant.getRuleProps(rule));
}
}
return false;
blocks.removeAll(allows);
return blocks.isEmpty() && !allows.isEmpty();
}
/**
@@ -218,16 +221,7 @@ public class RefControl {
// granting of powers beyond pushing to the configuration.
return false;
}
boolean result = false;
for (PermissionRule rule : access(Permission.PUSH)) {
if (rule.isBlock()) {
return false;
}
if (rule.getForce()) {
result = true;
}
}
return result;
return canForcePerform(Permission.PUSH);
}
/**
@@ -375,16 +369,7 @@ public class RefControl {
/** @return true if this user can force edit topic names. */
public boolean canForceEditTopicName() {
boolean result = false;
for (PermissionRule rule : access(Permission.EDIT_TOPIC_NAME)) {
if (rule.isBlock()) {
return false;
}
if (rule.getForce()) {
result = true;
}
}
return result;
return canForcePerform(Permission.EDIT_TOPIC_NAME);
}
/** All value ranges of any allowed label permission. */
@@ -416,39 +401,97 @@ public class RefControl {
return null;
}
private static PermissionRange toRange(String permissionName,
List<PermissionRule> ruleList) {
int min = 0;
int max = 0;
int blockMin = Integer.MIN_VALUE;
int blockMax = Integer.MAX_VALUE;
for (PermissionRule rule : ruleList) {
private static class AllowedRange {
private int allowMin = 0;
private int allowMax = 0;
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 {
min = Math.min(min, rule.getMin());
max = Math.max(max, rule.getMax());
allowMin = Math.min(allowMin, rule.getMin());
allowMax = Math.max(allowMax, rule.getMax());
}
}
if (blockMin > Integer.MIN_VALUE) {
min = Math.max(min, blockMin + 1);
int getAllowMin() {
return allowMin;
}
if (blockMax < Integer.MAX_VALUE) {
max = Math.min(max, blockMax - 1);
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 PermissionRange toRange(String permissionName,
List<PermissionRule> ruleList) {
Map<ProjectRef, AllowedRange> ranges = Maps.newHashMap();
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);
}
/** True if the user has this permission. Works only for non labels. */
boolean canPerform(String permissionName) {
List<PermissionRule> access = access(permissionName);
Set<ProjectRef> allows = Sets.newHashSet();
Set<ProjectRef> blocks = Sets.newHashSet();
for (PermissionRule rule : access) {
if (rule.isBlock() && !rule.getForce()) {
return false;
blocks.add(relevant.getRuleProps(rule));
} else {
allows.add(relevant.getRuleProps(rule));
}
}
return !access.isEmpty();
blocks.removeAll(allows);
return blocks.isEmpty() && !allows.isEmpty();
}
/** True if the user has force this permission. Works only for non labels. */
private boolean canForcePerform(String permissionName) {
List<PermissionRule> access = access(permissionName);
Set<ProjectRef> allows = Sets.newHashSet();
Set<ProjectRef> blocks = Sets.newHashSet();
for (PermissionRule rule : access) {
if (rule.isBlock()) {
blocks.add(relevant.getRuleProps(rule));
} else if (rule.getForce()) {
allows.add(relevant.getRuleProps(rule));
}
}
blocks.removeAll(allows);
return blocks.isEmpty() && !allows.isEmpty();
}
/** Rules for the given permission, or the empty list. */

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.server.project;
import static com.google.gerrit.common.data.Permission.EDIT_TOPIC_NAME;
import static com.google.gerrit.common.data.Permission.LABEL;
import static com.google.gerrit.common.data.Permission.OWNER;
import static com.google.gerrit.common.data.Permission.PUSH;
@@ -253,6 +254,139 @@ public class RefControlTest extends TestCase {
assertFalse("u can't vote -2", range.contains(-2));
assertFalse("u can't vote 2", range.contains(2));
}
public void testUnblockNoForce() {
grant(local, PUSH, anonymous, "refs/heads/*").setBlock();
grant(local, PUSH, devs, "refs/heads/*");
ProjectControl u = user(devs);
assertTrue("u can push", u.controlForRef("refs/heads/master").canUpdate());
}
public void testUnblockForce() {
PermissionRule r = grant(local, PUSH, anonymous, "refs/heads/*");
r.setBlock();
r.setForce(true);
grant(local, PUSH, devs, "refs/heads/*").setForce(true);
ProjectControl u = user(devs);
assertTrue("u can force push", u.controlForRef("refs/heads/master").canForceUpdate());
}
public void testUnblockForceWithAllowNoForce_NotPossible() {
PermissionRule r = grant(local, PUSH, anonymous, "refs/heads/*");
r.setBlock();
r.setForce(true);
grant(local, PUSH, devs, "refs/heads/*");
ProjectControl u = user(devs);
assertFalse("u can't force push", u.controlForRef("refs/heads/master").canForceUpdate());
}
public void testUnblockMoreSpecificRef_Fails() {
grant(local, PUSH, anonymous, "refs/heads/*").setBlock();
grant(local, PUSH, devs, "refs/heads/master");
ProjectControl u = user(devs);
assertFalse("u can't push", u.controlForRef("refs/heads/master").canUpdate());
}
public void testUnblockLargerScope_Fails() {
grant(local, PUSH, anonymous, "refs/heads/master").setBlock();
grant(local, PUSH, devs, "refs/heads/*");
ProjectControl u = user(devs);
assertFalse("u can't push", u.controlForRef("refs/heads/master").canUpdate());
}
public void testUnblockInLocal_Fails() {
grant(parent, PUSH, anonymous, "refs/heads/*").setBlock();
grant(local, PUSH, fixers, "refs/heads/*");
ProjectControl f = user(fixers);
assertFalse("u can't push", f.controlForRef("refs/heads/master").canUpdate());
}
public void testUnblockInParentBlockInLocal() {
grant(parent, PUSH, anonymous, "refs/heads/*").setBlock();
grant(parent, PUSH, devs, "refs/heads/*");
grant(local, PUSH, devs, "refs/heads/*").setBlock();
ProjectControl d = user(devs);
assertFalse("u can't push", d.controlForRef("refs/heads/master").canUpdate());
}
public void testUnblockVisibilityByRegisteredUsers() {
grant(local, READ, anonymous, "refs/heads/*").setBlock();
grant(local, READ, registered, "refs/heads/*");
ProjectControl u = user(registered);
assertTrue("u can read", u.controlForRef("refs/heads/master").isVisibleByRegisteredUsers());
}
public void testUnblockInLocalVisibilityByRegisteredUsers_Fails() {
grant(parent, READ, anonymous, "refs/heads/*").setBlock();
grant(local, READ, registered, "refs/heads/*");
ProjectControl u = user(registered);
assertFalse("u can't read", u.controlForRef("refs/heads/master").isVisibleByRegisteredUsers());
}
public void testUnblockForceEditTopicName() {
grant(local, EDIT_TOPIC_NAME, anonymous, "refs/heads/*").setBlock();
grant(local, EDIT_TOPIC_NAME, devs, "refs/heads/*").setForce(true);
ProjectControl u = user(devs);
assertTrue("u can edit topic name", u.controlForRef("refs/heads/master").canForceEditTopicName());
}
public void testUnblockInLocalForceEditTopicName_Fails() {
grant(parent, EDIT_TOPIC_NAME, anonymous, "refs/heads/*").setBlock();
grant(local, EDIT_TOPIC_NAME, devs, "refs/heads/*").setForce(true);
ProjectControl u = user(registered);
assertFalse("u can't edit topic name", u.controlForRef("refs/heads/master").canForceEditTopicName());
}
public void testUnblockRange() {
grant(local, LABEL + "Code-Review", -1, +1, anonymous, "refs/heads/*").setBlock();
grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/*");
ProjectControl u = user(devs);
PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
assertTrue("u can vote -2", range.contains(-2));
assertTrue("u can vote +2", range.contains(2));
}
public void testUnblockRangeOnMoreSpecificRef_Fails() {
grant(local, LABEL + "Code-Review", -1, +1, anonymous, "refs/heads/*").setBlock();
grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/master");
ProjectControl u = user(devs);
PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
assertFalse("u can't vote -2", range.contains(-2));
assertFalse("u can't vote +2", range.contains(-2));
}
public void testUnblockRangeOnLargerScope_Fails() {
grant(local, LABEL + "Code-Review", -1, +1, anonymous, "refs/heads/master").setBlock();
grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/*");
ProjectControl u = user(devs);
PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
assertFalse("u can't vote -2", range.contains(-2));
assertFalse("u can't vote +2", range.contains(-2));
}
public void testUnblockInLocalRange_Fails() {
grant(parent, LABEL + "Code-Review", -1, 1, anonymous, "refs/heads/*").setBlock();
grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/*");
ProjectControl u = user(devs);
PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
assertFalse("u can't vote -2", range.contains(-2));
assertFalse("u can't vote 2", range.contains(2));
}
// -----------------------------------------------------------------------
private final Map<Project.NameKey, ProjectState> all;