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>
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 4427f87..63db123 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -22,6 +22,7 @@
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 @@
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 @@
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
~~~~~~~~~~~~~~~~~~~~
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddRefRight.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddRefRight.java
index 06f15c22..3c2c93f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddRefRight.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddRefRight.java
@@ -144,17 +144,34 @@
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 @@
refPattern = "-" + refPattern;
}
- if (!controlForRef(projectControl, refPattern).isOwner()) {
+ if (!projectControl.controlForRef(refPattern).isOwner()) {
throw new NoSuchRefException(refPattern);
}
@@ -187,11 +204,4 @@
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);
- }
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java
index db179ed..92154c4 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java
@@ -71,7 +71,7 @@
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 @@
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);
- }
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
index cdd535b..3ff3892f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
@@ -26,7 +26,6 @@
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 @@
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 @@
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 @@
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);
- }
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java
index 0db09e4..ec70051 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java
@@ -25,6 +25,9 @@
/** 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;
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
index e3ce9a0..db8093c 100644
--- a/gerrit-server/pom.xml
+++ b/gerrit-server/pom.xml
@@ -54,6 +54,11 @@
</dependency>
<dependency>
+ <groupId>commons-lang</groupId>
+ <artifactId>commons-lang</artifactId>
+ </dependency>
+
+ <dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
</dependency>
@@ -133,6 +138,11 @@
<groupId>com.google.gerrit</groupId>
<artifactId>juniversalchardet</artifactId>
</dependency>
+
+ <dependency>
+ <groupId>dk.brics.automaton</groupId>
+ <artifactId>automaton</artifactId>
+ </dependency>
</dependencies>
<build>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index 32fa01c..23e2df8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -34,6 +34,9 @@
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.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 @@
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 @@
// 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 @@
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 @@
* @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 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 @@
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);
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_34.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_34.java
index 2f06f70..fa94146 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_34.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_34.java
@@ -19,19 +19,33 @@
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 @@
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) {
diff --git a/pom.xml b/pom.xml
index 4e89770..ea62331 100644
--- a/pom.xml
+++ b/pom.xml
@@ -549,6 +549,12 @@
</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>
<version>2.1.3</version>
@@ -725,6 +731,12 @@
<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 @@
<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>