Simplify reference level access control.

The initial implementation of reference level access control only
supports a corner case, that of "locking down" access for a specific
branch.

Upon further discussion, we've decided that this is not the more
general need. Most Gerrit configurations prefer to have a more "open"
access model, where access rights on a reference specified with a
wildcard, such as "refs/heads/*" aren't overridden by a more specific
access right. So this change makes the default behavior to evaluate
all rights, including the wild card ones.

However, in order to accomodate the corner case we were supporting
before, this change also introduces a new way to specify exclusive
reference level access rights. All access rights that start with the
'-' prefix are considered exclusive, and will prevent all wild card
rights from being considered.

Change-Id: I629f5439967b2141e46098614fadb25ff28e5f45
This commit is contained in:
Nico Sallembien
2010-05-04 11:49:12 -07:00
parent fc8e99f041
commit a78a37cf45
14 changed files with 375 additions and 61 deletions

View File

@@ -44,8 +44,11 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
/** Manages access control for Git references (aka branches, tags). */
@@ -225,6 +228,52 @@ public class RefControl {
return canPerform(FORGE_IDENTITY, FORGE_SERVER);
}
/**
* Convenience holder class used to map a ref pattern to the list of
* {@code RefRight}s that use it in the database.
*/
public final static class RefRightsForPattern {
private final List<RefRight> rights;
private boolean containsExclusive;
public RefRightsForPattern() {
rights = new ArrayList<RefRight>();
containsExclusive = false;
}
public void addRight(RefRight right) {
rights.add(right);
if (right.isExclusive()) {
containsExclusive = true;
}
}
public List<RefRight> getRights() {
return Collections.unmodifiableList(rights);
}
public boolean containsExclusive() {
return containsExclusive;
}
/**
* Returns The max allowed value for this ref pattern for all specified
* groups.
*
* @param groups The groups of the user
* @return The allowed value for this ref for all the specified groups
*/
public int allowedValueForRef(Set<AccountGroup.Id> groups) {
int val = Integer.MIN_VALUE;
for (RefRight right : rights) {
if (groups.contains(right.getAccountGroupId())) {
val = Math.max(right.getMaxValue(), val);
}
}
return val;
}
}
boolean canPerform(ApprovalCategory.Id actionId, short level) {
final Set<AccountGroup.Id> groups = getCurrentUser().getEffectiveGroups();
int val = Integer.MIN_VALUE;
@@ -236,42 +285,68 @@ public class RefControl {
allRights.addAll(getInheritedRights(actionId));
}
// Sort in descending refPattern length
Collections.sort(allRights, RefRight.REF_PATTERN_ORDER);
SortedMap<String, RefRightsForPattern> perPatternRights =
sortedRightsByPattern(allRights);
for (RefRight right : filterMostSpecific(allRights)) {
if (groups.contains(right.getAccountGroupId())) {
val = Math.max(right.getMaxValue(), val);
for (RefRightsForPattern right : perPatternRights.values()) {
val = Math.max(val, right.allowedValueForRef(groups));
if (val >= level || right.containsExclusive()) {
return val >= level;
}
}
return val >= level;
}
public static List<RefRight> filterMostSpecific(List<RefRight> actionRights) {
// Grab the first set of RefRight which have the same refPattern
// those are the most specific RefRights we have, and are the
// we will consider to verify if this action can be performed.
// We do this so that one can override the ref rights for a specific
// project on a specific branch
boolean sameRefPattern = true;
List<RefRight> mostSpecific = new ArrayList<RefRight>();
String currentRefPattern = null;
int i = 0;
while (sameRefPattern && i < actionRights.size()) {
if (currentRefPattern == null) {
currentRefPattern = actionRights.get(i).getRefPattern();
mostSpecific.add(actionRights.get(i));
i++;
} else {
if (currentRefPattern.equals(actionRights.get(i).getRefPattern())) {
mostSpecific.add(actionRights.get(i));
i++;
} else {
sameRefPattern = false;
}
public static final Comparator<String> DESCENDING_SORT =
new Comparator<String>() {
@Override
public int compare(String a, String b) {
int aLength = a.length();
int bLength = b.length();
if (bLength == aLength) {
return a.compareTo(b);
}
return bLength - aLength;
}
return mostSpecific;
};
/**
* Sorts all given rights into a map, ordered by descending length of
* ref pattern.
*
* For example, if given the following rights in argument:
*
* ["refs/heads/master", group1, -1, +1],
* ["refs/heads/master", group2, -2, +2],
* ["refs/heads/*", group3, -1, +1]
* ["refs/heads/stable", group2, -1, +1]
*
* Then the following map is returned:
* "refs/heads/master" => {
* ["refs/heads/master", group1, -1, +1],
* ["refs/heads/master", group2, -2, +2]
* }
* "refs/heads/stable" => {["refs/heads/stable", group2, -1, +1]}
* "refs/heads/*" => {["refs/heads/*", group3, -1, +1]}
*
* @param actionRights
* @return A sorted map keyed off the ref pattern of all rights.
*/
private static SortedMap<String, RefRightsForPattern> sortedRightsByPattern(
List<RefRight> actionRights) {
SortedMap<String, RefRightsForPattern> rights =
new TreeMap<String, RefRightsForPattern>(DESCENDING_SORT);
for (RefRight actionRight : actionRights) {
RefRightsForPattern patternRights =
rights.get(actionRight.getRefPattern());
if (patternRights == null) {
patternRights = new RefRightsForPattern();
rights.put(actionRight.getRefPattern(), patternRights);
}
patternRights.addRight(actionRight);
}
return rights;
}
private List<RefRight> getLocalRights(ApprovalCategory.Id actionId) {
@@ -282,12 +357,30 @@ public class RefControl {
return filter(getProjectState().getInheritedRights(actionId));
}
public List<RefRight> getAllRights(final ApprovalCategory.Id id) {
/**
* Returns all applicable rights for a given approval category.
*
* Applicable rights are defined as the list of {@code RefRight}s which match
* the ref for which this object was created, stopping the ref wildcard
* matching when an exclusive ref right was encountered, for the given
* approval category.
* @param id The {@link ApprovalCategory.Id}.
* @return All applicalbe rights.
*/
public List<RefRight> getApplicableRights(final ApprovalCategory.Id id) {
List<RefRight> l = new ArrayList<RefRight>();
l.addAll(getLocalRights(id));
l.addAll(getInheritedRights(id));
Collections.sort(l, RefRight.REF_PATTERN_ORDER);
return Collections.unmodifiableList(RefControl.filterMostSpecific(l));
SortedMap<String, RefRightsForPattern> perPatternRights =
sortedRightsByPattern(l);
List<RefRight> applicable = new ArrayList<RefRight>();
for (RefRightsForPattern patternRights : perPatternRights.values()) {
applicable.addAll(patternRights.getRights());
if (patternRights.containsExclusive()) {
break;
}
}
return Collections.unmodifiableList(applicable);
}
private List<RefRight> filter(Collection<RefRight> all) {

View File

@@ -32,7 +32,7 @@ import java.util.List;
/** A version of the database schema. */
public abstract class SchemaVersion {
/** The current schema version. */
private static final Class<? extends SchemaVersion> C = Schema_33.class;
private static final Class<? extends SchemaVersion> C = Schema_34.class;
public static class Module extends AbstractModule {
@Override

View File

@@ -0,0 +1,110 @@
// Copyright (C) 2010 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.schema;
import com.google.gerrit.reviewdb.ApprovalCategory;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.RefRight;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.reviewdb.RefRight.RefPattern;
import com.google.gerrit.server.project.RefControl;
import com.google.gerrit.server.project.RefControl.RefRightsForPattern;
import com.google.gwtorm.client.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class Schema_34 extends SchemaVersion {
@Inject
Schema_34(Provider<Schema_33> prior) {
super(prior);
}
@Override
protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
Iterable<Project> projects = db.projects().all();
boolean showedBanner = false;
List<RefRight> toUpdate = new ArrayList<RefRight>();
List<RefRight> toDelete = new ArrayList<RefRight>();
for (Project p : projects) {
boolean showedProject = false;
List<RefRight> pr = db.refRights().byProject(p.getNameKey()).toList();
Map<ApprovalCategory.Id, Map<String, RefRightsForPattern>> r =
new HashMap<ApprovalCategory.Id, Map<String, RefRightsForPattern>>();
for (RefRight right : pr) {
ui.message(right.toString());
ApprovalCategory.Id cat = right.getApprovalCategoryId();
if (r.get(cat) == null) {
Map<String, RefRightsForPattern> m =
new TreeMap<String, RefRightsForPattern>(RefControl.DESCENDING_SORT);
r.put(cat, m);
}
if (r.get(cat).get(right.getRefPattern()) == null) {
RefRightsForPattern s = new RefRightsForPattern();
r.get(cat).put(right.getRefPattern(), s);
}
r.get(cat).get(right.getRefPattern()).addRight(right);
}
for (Map<String, RefRightsForPattern> categoryRights : r.values()) {
for (RefRightsForPattern rrp : categoryRights.values()) {
RefRight oldRight = rrp.getRights().get(0);
if (shouldPrompt(oldRight)) {
if (!showedBanner) {
ui.message("Entering interactive reference rights migration tool...");
showedBanner = true;
}
if (!showedProject) {
ui.message("In project " + p.getName());
showedProject = true;
}
ui.message("For category " + oldRight.getApprovalCategoryId());
boolean isWildcard = oldRight.getRefPattern().endsWith("/*");
boolean shouldUpdate = ui.yesno(!isWildcard,
"Should rights for pattern "
+ oldRight.getRefPattern()
+ " be considered exclusive?");
if (shouldUpdate) {
RefRight.Key newKey = new RefRight.Key(oldRight.getProjectNameKey(),
new RefPattern("-" + oldRight.getRefPattern()),
oldRight.getApprovalCategoryId(),
oldRight.getAccountGroupId());
RefRight newRight = new RefRight(newKey);
newRight.setMaxValue(oldRight.getMaxValue());
newRight.setMinValue(oldRight.getMinValue());
toUpdate.add(newRight);
toDelete.add(oldRight);
}
}
}
}
}
db.refRights().insert(toUpdate);
db.refRights().delete(toDelete);
}
private boolean shouldPrompt(RefRight right) {
return !right.getRefPattern().equals("refs/*")
&& !right.getRefPattern().equals("refs/heads/*")
&& !right.getRefPattern().equals("refs/tags/*");
}
}

View File

@@ -22,6 +22,8 @@ import java.util.List;
public interface UpdateUI {
void message(String msg);
boolean yesno(boolean def, String msg);
void pruneSchema(StatementExecutor e, List<String> pruneList)
throws OrmException;
}

View File

@@ -92,7 +92,7 @@ public abstract class CategoryFunction {
public boolean isValid(final CurrentUser user, final ApprovalType at,
final FunctionState state) {
RefControl rc = state.controlFor(user);
for (final RefRight pr : rc.getAllRights(at.getCategory().getId())) {
for (final RefRight pr : rc.getApplicableRights(at.getCategory().getId())) {
if (user.getEffectiveGroups().contains(pr.getAccountGroupId())
&& (pr.getMinValue() < 0 || pr.getMaxValue() > 0)) {
return true;

View File

@@ -146,7 +146,7 @@ public class FunctionState {
// Find the maximal range actually granted to the user.
//
short minAllowed = 0, maxAllowed = 0;
for (final RefRight r : rc.getAllRights(a.getCategoryId())) {
for (final RefRight r : rc.getApplicableRights(a.getCategoryId())) {
final AccountGroup.Id grp = r.getAccountGroupId();
if (user.getEffectiveGroups().contains(grp)) {
minAllowed = (short) Math.min(minAllowed, r.getMinValue());
@@ -154,8 +154,7 @@ public class FunctionState {
}
}
// Normalize the value into that range, returning true if we changed
// the value.
// Normalize the value into that range.
//
if (a.getValue() < minAllowed) {
a.setValue(minAllowed);

View File

@@ -44,7 +44,7 @@ public class SubmitFunction extends CategoryFunction {
final FunctionState state) {
if (valid(at, state)) {
RefControl rc = state.controlFor(user);
for (final RefRight pr : rc.getAllRights(at.getCategory().getId())) {
for (final RefRight pr : rc.getApplicableRights(at.getCategory().getId())) {
if (user.getEffectiveGroups().contains(pr.getAccountGroupId())
&& pr.getMaxValue() > 0) {
return true;

View File

@@ -66,6 +66,11 @@ public class SchemaUpdaterTest extends TestCase {
public void message(String msg) {
}
@Override
public boolean yesno(boolean def, String msg) {
return def;
}
@Override
public void pruneSchema(StatementExecutor e, List<String> pruneList)
throws OrmException {