Support regular expressions for ref access rules

This change considers the rights of the most specific ref pattern.
If the ref pattern starts with ^ it is considered to be a regular
expression, otherwise the older glob style or exact match rules
are used.

Change-Id: Ie060d3758e5184a7cedd38883253f60817a04e1b
Portions-by: carloseduardo.baldacin <carloseduardo.baldacin@sonyericsson.com>
Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Shawn O. Pearce
2010-06-23 14:39:19 -03:00
parent cf60707301
commit 993ddfb847
9 changed files with 241 additions and 52 deletions

View File

@@ -22,6 +22,7 @@ Guice <<apache2,Apache License 2.0>>
Apache Commons Codec <<apache2,Apache License 2.0>>
Apache Commons DBCP <<apache2,Apache License 2.0>>
Apache Commons Http Client <<apache2,Apache License 2.0>>
Apache Commons Lang <<apache2,Apache License 2.0>>
Apache Commons Logging <<apache2,Apache License 2.0>>
Apache Commons Net <<apache2,Apache License 2.0>>
Apache Commons Pool <<apache2,Apache License 2.0>>
@@ -48,6 +49,7 @@ Clippy <<clippy,MIT License>>
juniversalchardet <<mpl1_1,MPL 1.1>>
AOP Alliance Public Domain
JSR 305 <<jsr305,New-Style BSD>>
Automaton <<automaton,New-Style BSD>>
-----------------------------------------------------------
Cryptography Notice
@@ -560,6 +562,43 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
----
[[automaton]]
dk.brics.automaton - The BSD License
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* link:http://www.brics.dk/automaton/index.html
----
Copyright (c) 2007-2009, dk.brics.automaton
All rights reserved.
http://www.opensource.org/licenses/bsd-license.php
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the JSR305 expert group nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
----
[[args4j]]
args4j - MIT License
~~~~~~~~~~~~~~~~~~~~

View File

@@ -144,17 +144,34 @@ class AddRefRight extends Handler<ProjectDetail> {
while (refPattern.startsWith("/")) {
refPattern = refPattern.substring(1);
}
if (!refPattern.startsWith(Constants.R_REFS)) {
refPattern = Constants.R_HEADS + refPattern;
}
if (refPattern.endsWith("/*")) {
final String prefix = refPattern.substring(0, refPattern.length() - 2);
if (!"refs".equals(prefix) && !Repository.isValidRefName(prefix)) {
if (refPattern.startsWith(RefRight.REGEX_PREFIX)) {
String example = RefControl.shortestExample(refPattern);
if (!example.startsWith(Constants.R_REFS)) {
refPattern = RefRight.REGEX_PREFIX + Constants.R_HEADS
+ refPattern.substring(RefRight.REGEX_PREFIX.length());
example = RefControl.shortestExample(refPattern);
}
if (!Repository.isValidRefName(example)) {
throw new InvalidNameException();
}
} else {
if (!Repository.isValidRefName(refPattern)) {
throw new InvalidNameException();
if (!refPattern.startsWith(Constants.R_REFS)) {
refPattern = Constants.R_HEADS + refPattern;
}
if (refPattern.endsWith("/*")) {
final String prefix = refPattern.substring(0, refPattern.length() - 2);
if (!"refs".equals(prefix) && !Repository.isValidRefName(prefix)) {
throw new InvalidNameException();
}
} else {
if (!Repository.isValidRefName(refPattern)) {
throw new InvalidNameException();
}
}
}
@@ -162,7 +179,7 @@ class AddRefRight extends Handler<ProjectDetail> {
refPattern = "-" + refPattern;
}
if (!controlForRef(projectControl, refPattern).isOwner()) {
if (!projectControl.controlForRef(refPattern).isOwner()) {
throw new NoSuchRefException(refPattern);
}
@@ -187,11 +204,4 @@ class AddRefRight extends Handler<ProjectDetail> {
projectCache.evictAll();
return projectDetailFactory.create(projectName).call();
}
private RefControl controlForRef(ProjectControl p, String ref) {
if (ref.endsWith("/*")) {
ref = ref.substring(0, ref.length() - 1);
}
return p.controlForRef(ref);
}
}

View File

@@ -71,7 +71,7 @@ class DeleteRefRights extends Handler<ProjectDetail> {
if (!projectName.equals(k.getProjectNameKey())) {
throw new IllegalArgumentException("All keys must be from same project");
}
if (!controlForRef(projectControl, k.getRefPattern()).isOwner()) {
if (!projectControl.controlForRef(k.getRefPattern()).isOwner()) {
throw new NoSuchRefException(k.getRefPattern());
}
}
@@ -85,11 +85,4 @@ class DeleteRefRights extends Handler<ProjectDetail> {
projectCache.evictAll();
return projectDetailFactory.create(projectName).call();
}
private RefControl controlForRef(ProjectControl p, String ref) {
if (ref.endsWith("/*")) {
ref = ref.substring(0, ref.length() - 1);
}
return p.controlForRef(ref);
}
}

View File

@@ -26,7 +26,6 @@ import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.RefControl;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -77,7 +76,7 @@ class ProjectDetailFactory extends Handler<ProjectDetail> {
for (final RefRight r : projectState.getInheritedRights()) {
InheritedRefRight refRight = new InheritedRefRight(
r, true, controlForRef(pc, r.getRefPattern()).isOwner());
r, true, pc.controlForRef(r.getRefPattern()).isOwner());
if (!refRights.contains(refRight)) {
refRights.add(refRight);
wantGroup(r.getAccountGroupId());
@@ -86,7 +85,7 @@ class ProjectDetailFactory extends Handler<ProjectDetail> {
for (final RefRight r : projectState.getLocalRights()) {
refRights.add(new InheritedRefRight(
r, false, controlForRef(pc, r.getRefPattern()).isOwner()));
r, false, pc.controlForRef(r.getRefPattern()).isOwner()));
wantGroup(r.getAccountGroupId());
}
@@ -144,11 +143,4 @@ class ProjectDetailFactory extends Handler<ProjectDetail> {
groups.put(groupId, groupCache.get(groupId));
}
}
private RefControl controlForRef(ProjectControl p, String ref) {
if (ref.endsWith("/*")) {
ref = ref.substring(0, ref.length() - 1);
}
return p.controlForRef(ref);
}
}

View File

@@ -25,6 +25,9 @@ public final class RefRight {
/** Pattern that matches all references in a project. */
public static final String ALL = "refs/*";
/** Prefix that triggers a regular expression pattern. */
public static final String REGEX_PREFIX = "^";
public static class RefPattern extends
StringKey<com.google.gwtorm.client.Key<?>> {
private static final long serialVersionUID = 1L;

View File

@@ -53,6 +53,11 @@ limitations under the License.
<artifactId>commons-dbcp</artifactId>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
@@ -133,6 +138,11 @@ limitations under the License.
<groupId>com.google.gerrit</groupId>
<artifactId>juniversalchardet</artifactId>
</dependency>
<dependency>
<groupId>dk.brics.automaton</groupId>
<artifactId>automaton</artifactId>
</dependency>
</dependencies>
<build>

View File

@@ -34,6 +34,9 @@ import com.google.gerrit.reviewdb.RefRight;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
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;
@@ -49,6 +52,7 @@ import java.util.List;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Pattern;
/** Manages access control for Git references (aka branches, tags). */
@@ -59,9 +63,17 @@ public class RefControl {
private Boolean canForgeAuthor;
private Boolean canForgeCommitter;
RefControl(final ProjectControl projectControl, final String refName) {
RefControl(final ProjectControl projectControl, String ref) {
if (ref.startsWith(RefRight.REGEX_PREFIX)) {
ref = shortestExample(ref);
} else if (ref.endsWith("/*")) {
ref = ref.substring(0, ref.length() - 1);
}
this.projectControl = projectControl;
this.refName = refName;
this.refName = ref;
}
public String getRefName() {
@@ -94,7 +106,9 @@ public class RefControl {
// calls us to find out if there is ownership of all references in
// order to determine project level ownership.
//
if (!RefRight.ALL.equals(getRefName()) && getProjectControl().isOwner()) {
if (getRefName().equals(
RefRight.ALL.substring(0, RefRight.ALL.length() - 1))
&& getProjectControl().isOwner()) {
return true;
}
@@ -302,19 +316,95 @@ public class RefControl {
return val >= level;
}
public static final Comparator<String> DESCENDING_SORT =
/**
* 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 final Comparator<String> BY_MOST_SPECIFIC_SORT =
new Comparator<String>() {
public int compare(final String pattern1, final String pattern2) {
int cmp = distance(pattern2) - distance(pattern1);
if (cmp == 0) {
boolean p1_finite = finite(pattern1);
boolean p2_finite = finite(pattern2);
@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;
}
};
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 (pattern.startsWith(RefRight.REGEX_PREFIX)) {
example = shortestExample(pattern);
} else if (pattern.endsWith("/*")) {
example = pattern.substring(0, pattern.length() - 1) + '1';
} else if (pattern.equals(getRefName())) {
return 0;
} else {
return Math.max(pattern.length(), getRefName().length());
}
return StringUtils.getLevenshteinDistance(example, getRefName());
}
private boolean finite(String pattern) {
if (pattern.startsWith(RefRight.REGEX_PREFIX)) {
return toRegExp(pattern).toAutomaton().isFinite();
} else if (pattern.endsWith("/*")) {
return false;
} else {
return true;
}
}
private int transitions(String pattern) {
if (pattern.startsWith(RefRight.REGEX_PREFIX)) {
return toRegExp(pattern).toAutomaton().getNumberOfTransitions();
} else if (pattern.endsWith("/*")) {
return pattern.length();
} else {
return pattern.length();
}
}
};
/**
* Sorts all given rights into a map, ordered by descending length of
@@ -338,10 +428,10 @@ public class RefControl {
* @param actionRights
* @return A sorted map keyed off the ref pattern of all rights.
*/
private static SortedMap<String, RefRightsForPattern> sortedRightsByPattern(
private SortedMap<String, RefRightsForPattern> sortedRightsByPattern(
List<RefRight> actionRights) {
SortedMap<String, RefRightsForPattern> rights =
new TreeMap<String, RefRightsForPattern>(DESCENDING_SORT);
new TreeMap<String, RefRightsForPattern>(BY_MOST_SPECIFIC_SORT);
for (RefRight actionRight : actionRights) {
RefRightsForPattern patternRights =
rights.get(actionRight.getRefPattern());
@@ -403,6 +493,10 @@ public class RefControl {
}
public static boolean matches(String refName, String refPattern) {
if (refPattern.startsWith(RefRight.REGEX_PREFIX)) {
return Pattern.matches(refPattern, refName);
}
if (refPattern.endsWith("/*")) {
String prefix = refPattern.substring(0, refPattern.length() - 1);
return refName.startsWith(prefix);
@@ -411,4 +505,21 @@ public class RefControl {
return refName.equals(refPattern);
}
}
public static String shortestExample(String pattern) {
if (pattern.startsWith(RefRight.REGEX_PREFIX)) {
return toRegExp(pattern).toAutomaton().getShortestExample(true);
} else if (pattern.endsWith("/*")) {
return pattern.substring(0, pattern.length() - 1) + '1';
} else {
return pattern;
}
}
private static RegExp toRegExp(String refPattern) {
if (refPattern.startsWith(RefRight.REGEX_PREFIX)) {
refPattern = refPattern.substring(1);
}
return new RegExp(refPattern);
}
}

View File

@@ -19,19 +19,33 @@ 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.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class Schema_34 extends SchemaVersion {
private 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;
}
};
@Inject
Schema_34(Provider<Schema_33> prior) {
super(prior);
@@ -54,7 +68,7 @@ public class Schema_34 extends SchemaVersion {
ApprovalCategory.Id cat = right.getApprovalCategoryId();
if (r.get(cat) == null) {
Map<String, RefRightsForPattern> m =
new TreeMap<String, RefRightsForPattern>(RefControl.DESCENDING_SORT);
new TreeMap<String, RefRightsForPattern>(DESCENDING_SORT);
r.put(cat, m);
}
if (r.get(cat).get(right.getRefPattern()) == null) {

17
pom.xml
View File

@@ -548,6 +548,12 @@ limitations under the License.
<version>1.5.4</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>eu.medsea.mimeutil</groupId>
<artifactId>mime-util</artifactId>
@@ -725,6 +731,12 @@ limitations under the License.
<artifactId>juniversalchardet</artifactId>
<version>1.0.3</version>
</dependency>
<dependency>
<groupId>dk.brics.automaton</groupId>
<artifactId>automaton</artifactId>
<version>1.11.2</version>
</dependency>
</dependencies>
</dependencyManagement>
@@ -758,5 +770,10 @@ limitations under the License.
<id>objectweb-repository</id>
<url>http://maven.objectweb.org/maven2/</url>
</repository>
<repository>
<id>clojars-repo</id>
<url>http://clojars.org/repo</url>
</repository>
</repositories>
</project>