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:
@@ -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
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
17
pom.xml
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user