blob: ff1d09fd53e4f8d7a29b2d140421e34de232d7bc [file] [log] [blame]
// 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.project;
import com.google.gerrit.common.CollectionsUtil;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.ParamertizedString;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.reviewdb.AccountGroup;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
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;
import org.eclipse.jgit.revwalk.RevTag;
import org.eclipse.jgit.revwalk.RevWalk;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
/** Manages access control for Git references (aka branches, tags). */
public class RefControl {
public interface Factory {
RefControl create(ProjectControl projectControl, String ref);
}
private final ProjectControl projectControl;
private final String refName;
private Map<String, List<PermissionRule>> permissions;
private Boolean owner;
private Boolean canForgeAuthor;
private Boolean canForgeCommitter;
@Inject
protected RefControl(@Assisted final ProjectControl projectControl,
@Assisted String ref) {
if (isRE(ref)) {
ref = shortestExample(ref);
} else if (ref.endsWith("/*")) {
ref = ref.substring(0, ref.length() - 1);
}
this.projectControl = projectControl;
this.refName = ref;
}
public String getRefName() {
return refName;
}
public ProjectControl getProjectControl() {
return projectControl;
}
public CurrentUser getCurrentUser() {
return getProjectControl().getCurrentUser();
}
public RefControl forAnonymousUser() {
return getProjectControl().forAnonymousUser().controlForRef(getRefName());
}
public RefControl forUser(final CurrentUser who) {
return getProjectControl().forUser(who).controlForRef(getRefName());
}
/** Is this user a ref owner? */
public boolean isOwner() {
if (owner == null) {
if (canPerform(Permission.OWNER)) {
owner = true;
} else if (getRefName().equals(
AccessSection.ALL.substring(0, AccessSection.ALL.length() - 1))) {
// We have to prevent infinite recursion here, the project control
// calls us to find out if there is ownership of all references in
// order to determine project level ownership.
//
owner = getCurrentUser().isAdministrator();
} else {
owner = getProjectControl().isOwner();
}
}
return owner;
}
/** Can this user see this reference exists? */
public boolean isVisible() {
return getProjectControl().visibleForReplication()
|| canPerform(Permission.READ);
}
/**
* Determines whether the user can upload a change to the ref controlled by
* this object.
*
* @return {@code true} if the user specified can upload a change to the Git
* ref
*/
public boolean canUpload() {
return getProjectControl()
.controlForRef("refs/for/" + getRefName())
.canPerform(Permission.PUSH);
}
/** @return true if this user can submit merge patch sets to this ref */
public boolean canUploadMerges() {
return getProjectControl()
.controlForRef("refs/for/" + getRefName())
.canPerform(Permission.PUSH_MERGE);
}
/** @return true if this user can submit patch sets to this ref */
public boolean canSubmit() {
if (GitRepositoryManager.REF_CONFIG.equals(refName)) {
// Always allow project owners to submit configuration changes.
// Submitting configuration changes modifies the access control
// rules. Allowing this to be done by a non-project-owner opens
// a security hole enabling editing of access rules, and thus
// granting of powers beyond submitting to the configuration.
return getProjectControl().isOwner();
}
return canPerform(Permission.SUBMIT);
}
/** @return true if the user can update the reference as a fast-forward. */
public boolean canUpdate() {
if (GitRepositoryManager.REF_CONFIG.equals(refName)
&& !getProjectControl().isOwner()) {
// Pushing requires being at least project owner, in addition to push.
// Pushing configuration changes modifies the access control
// rules. Allowing this to be done by a non-project-owner opens
// a security hole enabling editing of access rules, and thus
// granting of powers beyond pushing to the configuration.
return false;
}
return canPerform(Permission.PUSH);
}
/** @return true if the user can rewind (force push) the reference. */
public boolean canForceUpdate() {
return canPushWithForce() || canDelete();
}
private boolean canPushWithForce() {
if (GitRepositoryManager.REF_CONFIG.equals(refName)
&& !getProjectControl().isOwner()) {
// Pushing requires being at least project owner, in addition to push.
// Pushing configuration changes modifies the access control
// rules. Allowing this to be done by a non-project-owner opens
// a security hole enabling editing of access rules, and thus
// granting of powers beyond pushing to the configuration.
return false;
}
for (PermissionRule rule : access(Permission.PUSH)) {
if (rule.getForce()) {
return true;
}
}
return false;
}
/**
* Determines whether the user can create a new Git ref.
*
* @param rw revision pool {@code object} was parsed in.
* @param object the object the user will start the reference with.
* @return {@code true} if the user specified can create a new Git ref
*/
public boolean canCreate(RevWalk rw, RevObject object) {
boolean owner;
switch (getCurrentUser().getAccessPath()) {
case WEB_UI:
owner = isOwner();
break;
default:
owner = false;
}
if (object instanceof RevCommit) {
return owner || canPerform(Permission.CREATE);
} else if (object instanceof RevTag) {
final RevTag tag = (RevTag) object;
try {
rw.parseBody(tag);
} catch (IOException e) {
return false;
}
// If tagger is present, require it matches the user's email.
//
final PersonIdent tagger = tag.getTaggerIdent();
if (tagger != null) {
boolean valid;
if (getCurrentUser() instanceof IdentifiedUser) {
final IdentifiedUser user = (IdentifiedUser) getCurrentUser();
final String addr = tagger.getEmailAddress();
valid = user.getEmailAddresses().contains(addr);
} else {
valid = false;
}
if (!valid && !owner && !canForgeCommitter()) {
return false;
}
}
// If the tag has a PGP signature, allow a lower level of permission
// than if it doesn't have a PGP signature.
//
if (tag.getFullMessage().contains("-----BEGIN PGP SIGNATURE-----\n")) {
return owner || canPerform(Permission.PUSH_TAG);
} else {
return owner || canPerform(Permission.PUSH_TAG);
}
} else {
return false;
}
}
/**
* Determines whether the user can delete the Git ref controlled by this
* object.
*
* @return {@code true} if the user specified can delete a Git ref.
*/
public boolean canDelete() {
if (GitRepositoryManager.REF_CONFIG.equals(refName)) {
// Never allow removal of the refs/meta/config branch.
// Deleting the branch would destroy all Gerrit specific
// metadata about the project, including its access rules.
// If a project is to be removed from Gerrit, its repository
// should be removed first.
return false;
}
switch (getCurrentUser().getAccessPath()) {
case WEB_UI:
return isOwner() || canPushWithForce();
case GIT:
return canPushWithForce();
default:
return false;
}
}
/** @return true if this user can forge the author line in a commit. */
public boolean canForgeAuthor() {
if (canForgeAuthor == null) {
canForgeAuthor = canPerform(Permission.FORGE_AUTHOR);
}
return canForgeAuthor;
}
/** @return true if this user can forge the committer line in a commit. */
public boolean canForgeCommitter() {
if (canForgeCommitter == null) {
canForgeCommitter = canPerform(Permission.FORGE_COMMITTER);
}
return canForgeCommitter;
}
/** @return true if this user can forge the server on the committer line. */
public boolean canForgeGerritServerIdentity() {
return canPerform(Permission.FORGE_SERVER);
}
/** All value ranges of any allowed label permission. */
public List<PermissionRange> getLabelRanges() {
List<PermissionRange> r = new ArrayList<PermissionRange>();
for (Map.Entry<String, List<PermissionRule>> e : permissions().entrySet()) {
if (Permission.isLabel(e.getKey())) {
r.add(toRange(e.getKey(), e.getValue()));
}
}
return r;
}
/** The range of permitted values associated with a label permission. */
public PermissionRange getRange(String permission) {
if (Permission.isLabel(permission)) {
return toRange(permission, access(permission));
}
return null;
}
private static PermissionRange toRange(String permissionName, List<PermissionRule> ruleList) {
int min = 0;
int max = 0;
for (PermissionRule rule : ruleList) {
min = Math.min(min, rule.getMin());
max = Math.max(max, rule.getMax());
}
return new PermissionRange(permissionName, min, max);
}
/** True if the user has this permission. Works only for non labels. */
boolean canPerform(String permissionName) {
return !access(permissionName).isEmpty();
}
/** Rules for the given permission, or the empty list. */
private List<PermissionRule> access(String permissionName) {
List<PermissionRule> r = permissions().get(permissionName);
return r != null ? r : Collections.<PermissionRule> emptyList();
}
/** All rules that pertain to this user, on this reference. */
private Map<String, List<PermissionRule>> permissions() {
if (permissions == null) {
List<AccessSection> sections = new ArrayList<AccessSection>();
for (AccessSection section : projectControl.access()) {
if (appliesToRef(section)) {
sections.add(section);
}
}
Collections.sort(sections, new MostSpecificComparator(getRefName()));
Set<SeenRule> seen = new HashSet<SeenRule>();
Set<String> exclusiveGroupPermissions = new HashSet<String>();
permissions = new HashMap<String, List<PermissionRule>>();
for (AccessSection section : sections) {
for (Permission permission : section.getPermissions()) {
if (exclusiveGroupPermissions.contains(permission.getName())) {
continue;
}
for (PermissionRule rule : permission.getRules()) {
if (matchGroup(rule.getGroup().getUUID())) {
SeenRule s = new SeenRule(section, permission, rule);
if (seen.add(s) && !rule.getDeny()) {
List<PermissionRule> r = permissions.get(permission.getName());
if (r == null) {
r = new ArrayList<PermissionRule>(2);
permissions.put(permission.getName(), r);
}
r.add(rule);
}
}
}
if (permission.getExclusiveGroup()) {
exclusiveGroupPermissions.add(permission.getName());
}
}
}
}
return permissions;
}
private boolean appliesToRef(AccessSection section) {
String refPattern = section.getRefPattern();
if (isTemplate(refPattern)) {
ParamertizedString template = new ParamertizedString(refPattern);
HashMap<String, String> p = new HashMap<String, String>();
if (getCurrentUser() instanceof IdentifiedUser) {
p.put("username", ((IdentifiedUser) getCurrentUser()).getUserName());
} else {
// Right now we only template the username. If not available
// this rule cannot be matched at all.
//
return false;
}
if (isRE(refPattern)) {
for (Map.Entry<String, String> ent : p.entrySet()) {
ent.setValue(escape(ent.getValue()));
}
}
refPattern = template.replace(p);
}
if (isRE(refPattern)) {
return Pattern.matches(refPattern, getRefName());
} else if (refPattern.endsWith("/*")) {
String prefix = refPattern.substring(0, refPattern.length() - 1);
return getRefName().startsWith(prefix);
} else {
return getRefName().equals(refPattern);
}
}
private boolean matchGroup(AccountGroup.UUID uuid) {
Set<AccountGroup.UUID> userGroups = getCurrentUser().getEffectiveGroups();
if (AccountGroup.PROJECT_OWNERS.equals(uuid)) {
ProjectState state = projectControl.getProjectState();
return CollectionsUtil.isAnyIncludedIn(state.getAllOwners(), userGroups);
} else {
return userGroups.contains(uuid);
}
}
private static boolean isTemplate(String refPattern) {
return 0 <= refPattern.indexOf("${");
}
private static String escape(String value) {
// Right now the only special character allowed in a
// variable value is a . in the username.
//
return value.replace(".", "\\.");
}
private static boolean isRE(String refPattern) {
return refPattern.startsWith(AccessSection.REGEX_PREFIX);
}
public static String shortestExample(String pattern) {
if (isRE(pattern)) {
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 (isRE(refPattern)) {
refPattern = refPattern.substring(1);
}
return new RegExp(refPattern, RegExp.NONE);
}
/** Tracks whether or not a permission has been overridden. */
private static class SeenRule {
final String refPattern;
final String permissionName;
final AccountGroup.UUID group;
SeenRule(AccessSection section, Permission permission, PermissionRule rule) {
refPattern = section.getRefPattern();
permissionName = permission.getName();
group = rule.getGroup().getUUID();
}
@Override
public int hashCode() {
int hc = refPattern.hashCode();
hc = hc * 31 + permissionName.hashCode();
if (group != null) {
hc = hc * 31 + group.hashCode();
}
return hc;
}
@Override
public boolean equals(Object other) {
if (other instanceof SeenRule) {
SeenRule a = this;
SeenRule b = (SeenRule) other;
return a.refPattern.equals(b.refPattern) //
&& a.permissionName.equals(b.permissionName) //
&& eq(a.group, b.group);
}
return false;
}
private boolean eq(AccountGroup.UUID a, AccountGroup.UUID b) {
return a != null && b != null && a.equals(b);
}
}
/**
* 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 static final class MostSpecificComparator implements
Comparator<AccessSection> {
private final String refName;
MostSpecificComparator(String refName) {
this.refName = refName;
}
public int compare(AccessSection a, AccessSection b) {
return compare(a.getRefPattern(), b.getRefPattern());
}
private int compare(final String pattern1, final String pattern2) {
int cmp = distance(pattern1) - distance(pattern2);
if (cmp == 0) {
boolean p1_finite = finite(pattern1);
boolean p2_finite = finite(pattern2);
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 (isRE(pattern)) {
example = shortestExample(pattern);
} else if (pattern.endsWith("/*")) {
example = pattern.substring(0, pattern.length() - 1) + '1';
} else if (pattern.equals(refName)) {
return 0;
} else {
return Math.max(pattern.length(), refName.length());
}
return StringUtils.getLevenshteinDistance(example, refName);
}
private boolean finite(String pattern) {
if (isRE(pattern)) {
return toRegExp(pattern).toAutomaton().isFinite();
} else if (pattern.endsWith("/*")) {
return false;
} else {
return true;
}
}
private int transitions(String pattern) {
if (isRE(pattern)) {
return toRegExp(pattern).toAutomaton().getNumberOfTransitions();
} else if (pattern.endsWith("/*")) {
return pattern.length();
} else {
return pattern.length();
}
}
}
}