com.google.gerrit.server.project.RefControl.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gerrit.server.project.RefControl.java

Source

// 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();
            }
        }
    }
}