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

Java tutorial

Introduction

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

Source

// Copyright (C) 2009 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 static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.RefConfigSection;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.ChangePermissionOrLabel;
import com.google.gerrit.server.permissions.LabelPermission;
import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/** Access control management for a user accessing a single change. */
public class ChangeControl {
    @Singleton
    public static class GenericFactory {
        private final ProjectControl.GenericFactory projectControl;
        private final ChangeNotes.Factory notesFactory;

        @Inject
        GenericFactory(ProjectControl.GenericFactory p, ChangeNotes.Factory n) {
            projectControl = p;
            notesFactory = n;
        }

        public ChangeControl controlFor(ReviewDb db, Project.NameKey project, Change.Id changeId, CurrentUser user)
                throws OrmException {
            return controlFor(notesFactory.create(db, project, changeId), user);
        }

        public ChangeControl controlFor(ReviewDb db, Change change, CurrentUser user) throws OrmException {
            final Project.NameKey projectKey = change.getProject();
            try {
                return projectControl.controlFor(projectKey, user).controlFor(db, change);
            } catch (NoSuchProjectException e) {
                throw new NoSuchChangeException(change.getId(), e);
            } catch (IOException e) {
                // TODO: propagate this exception
                throw new NoSuchChangeException(change.getId(), e);
            }
        }

        public ChangeControl controlFor(ChangeNotes notes, CurrentUser user) throws NoSuchChangeException {
            try {
                return projectControl.controlFor(notes.getProjectName(), user).controlFor(notes);
            } catch (NoSuchProjectException | IOException e) {
                throw new NoSuchChangeException(notes.getChangeId(), e);
            }
        }

        public ChangeControl validateFor(ReviewDb db, Change.Id changeId, CurrentUser user) throws OrmException {
            return validateFor(db, notesFactory.createChecked(changeId), user);
        }

        public ChangeControl validateFor(ReviewDb db, ChangeNotes notes, CurrentUser user) throws OrmException {
            ChangeControl c = controlFor(notes, user);
            if (!c.isVisible(db)) {
                throw new NoSuchChangeException(c.getId());
            }
            return c;
        }
    }

    @Singleton
    public static class Factory {
        private final ChangeData.Factory changeDataFactory;
        private final ChangeNotes.Factory notesFactory;
        private final ApprovalsUtil approvalsUtil;
        private final PatchSetUtil patchSetUtil;

        @Inject
        Factory(ChangeData.Factory changeDataFactory, ChangeNotes.Factory notesFactory, ApprovalsUtil approvalsUtil,
                PatchSetUtil patchSetUtil) {
            this.changeDataFactory = changeDataFactory;
            this.notesFactory = notesFactory;
            this.approvalsUtil = approvalsUtil;
            this.patchSetUtil = patchSetUtil;
        }

        ChangeControl create(RefControl refControl, ReviewDb db, Project.NameKey project, Change.Id changeId)
                throws OrmException {
            return create(refControl, notesFactory.create(db, project, changeId));
        }

        /**
         * Create a change control for a change that was loaded from index. This method should only be
         * used when database access is harmful and potentially stale data from the index is acceptable.
         *
         * @param refControl ref control
         * @param change change loaded from secondary index
         * @return change control
         */
        ChangeControl createForIndexedChange(RefControl refControl, Change change) {
            return create(refControl, notesFactory.createFromIndexedChange(change));
        }

        ChangeControl create(RefControl refControl, ChangeNotes notes) {
            return new ChangeControl(changeDataFactory, approvalsUtil, refControl, notes, patchSetUtil);
        }
    }

    private final ChangeData.Factory changeDataFactory;
    private final ApprovalsUtil approvalsUtil;
    private final RefControl refControl;
    private final ChangeNotes notes;
    private final PatchSetUtil patchSetUtil;

    ChangeControl(ChangeData.Factory changeDataFactory, ApprovalsUtil approvalsUtil, RefControl refControl,
            ChangeNotes notes, PatchSetUtil patchSetUtil) {
        this.changeDataFactory = changeDataFactory;
        this.approvalsUtil = approvalsUtil;
        this.refControl = refControl;
        this.notes = notes;
        this.patchSetUtil = patchSetUtil;
    }

    public ChangeControl forUser(final CurrentUser who) {
        if (getUser().equals(who)) {
            return this;
        }
        return new ChangeControl(changeDataFactory, approvalsUtil, getRefControl().forUser(who), notes,
                patchSetUtil);
    }

    public RefControl getRefControl() {
        return refControl;
    }

    public CurrentUser getUser() {
        return getRefControl().getUser();
    }

    public ProjectControl getProjectControl() {
        return getRefControl().getProjectControl();
    }

    public Project getProject() {
        return getProjectControl().getProject();
    }

    public Change.Id getId() {
        return notes.getChangeId();
    }

    public Change getChange() {
        return notes.getChange();
    }

    public ChangeNotes getNotes() {
        return notes;
    }

    /** Can this user see this change? */
    public boolean isVisible(ReviewDb db) throws OrmException {
        return isVisible(db, null);
    }

    /** Can this user see this change? */
    public boolean isVisible(ReviewDb db, @Nullable ChangeData cd) throws OrmException {
        if (getChange().isPrivate() && !isPrivateVisible(db, cd)) {
            return false;
        }
        if (getChange().getStatus() == Change.Status.DRAFT && !isDraftVisible(db, cd)) {
            return false;
        }
        return isRefVisible();
    }

    /** Can the user see this change? Does not account for draft status */
    public boolean isRefVisible() {
        return getRefControl().isVisible();
    }

    /** Can this user see the given patchset? */
    public boolean isPatchVisible(PatchSet ps, ReviewDb db) throws OrmException {
        if (ps != null && ps.isDraft() && !isDraftVisible(db, null)) {
            return false;
        }
        return isVisible(db);
    }

    /** Can this user see the given patchset? */
    public boolean isPatchVisible(PatchSet ps, ChangeData cd) throws OrmException {
        checkArgument(cd.getId().equals(ps.getId().getParentKey()), "%s not for change %s", ps, cd.getId());
        if (ps.isDraft() && !isDraftVisible(cd.db(), cd)) {
            return false;
        }
        return isVisible(cd.db());
    }

    /** Can this user abandon this change? */
    private boolean canAbandon(ReviewDb db) throws OrmException {
        return (isOwner() // owner (aka creator) of the change can abandon
                || getRefControl().isOwner() // branch owner can abandon
                || getProjectControl().isOwner() // project owner can abandon
                || getUser().getCapabilities().canAdministrateServer() // site administers are god
                || getRefControl().canAbandon() // user can abandon a specific ref
        ) && !isPatchSetLocked(db);
    }

    /** Can this user change the destination branch of this change to the new ref? */
    public boolean canMoveTo(String ref, ReviewDb db) throws OrmException {
        return getProjectControl().controlForRef(ref).canUpload() && canAbandon(db);
    }

    /** Can this user publish this draft change or any draft patch set of this change? */
    public boolean canPublish(final ReviewDb db) throws OrmException {
        return (isOwner() || getRefControl().canPublishDrafts()) && isVisible(db);
    }

    /** Can this user delete this change or any patch set of this change? */
    public boolean canDelete(ReviewDb db, Change.Status status) throws OrmException {
        if (!isVisible(db)) {
            return false;
        }

        switch (status) {
        case DRAFT:
            return (isOwner() || getRefControl().canDeleteDrafts());
        case NEW:
        case ABANDONED:
            return (isAdmin() || (isOwner() && getRefControl().canDeleteOwnChanges()));
        case MERGED:
        default:
            return false;
        }
    }

    /** Can this user rebase this change? */
    private boolean canRebase(ReviewDb db) throws OrmException {
        return (isOwner() || getRefControl().canSubmit(isOwner()) || getRefControl().canRebase())
                && !isPatchSetLocked(db);
    }

    /** Can this user restore this change? */
    private boolean canRestore(ReviewDb db) throws OrmException {
        return canAbandon(db) // Anyone who can abandon the change can restore it back
                && getRefControl().canUpload(); // as long as you can upload too
    }

    /** All available label types for this change. */
    public LabelTypes getLabelTypes() {
        String destBranch = getChange().getDest().get();
        List<LabelType> all = getProjectControl().getLabelTypes().getLabelTypes();

        List<LabelType> r = Lists.newArrayListWithCapacity(all.size());
        for (LabelType l : all) {
            List<String> refs = l.getRefPatterns();
            if (refs == null) {
                r.add(l);
            } else {
                for (String refPattern : refs) {
                    if (RefConfigSection.isValid(refPattern) && match(destBranch, refPattern)) {
                        r.add(l);
                        break;
                    }
                }
            }
        }

        return new LabelTypes(r);
    }

    /** All value ranges of any allowed label permission. */
    public List<PermissionRange> getLabelRanges() {
        return getRefControl().getLabelRanges(isOwner());
    }

    /** The range of permitted values associated with a label permission. */
    public PermissionRange getRange(String permission) {
        return getRefControl().getRange(permission, isOwner());
    }

    /** Can this user add a patch set to this change? */
    public boolean canAddPatchSet(ReviewDb db) throws OrmException {
        if (!getRefControl().canUpload() || isPatchSetLocked(db)
                || !isPatchVisible(patchSetUtil.current(db, notes), db)) {
            return false;
        }
        if (isOwner()) {
            return true;
        }
        return getRefControl().canAddPatchSet();
    }

    /** Is the current patch set locked against state changes? */
    public boolean isPatchSetLocked(ReviewDb db) throws OrmException {
        if (getChange().getStatus() == Change.Status.MERGED) {
            return false;
        }

        for (PatchSetApproval ap : approvalsUtil.byPatchSet(db, this, getChange().currentPatchSetId())) {
            LabelType type = getLabelTypes().byLabel(ap.getLabel());
            if (type != null && ap.getValue() == 1 && type.getFunctionName().equalsIgnoreCase("PatchSetLock")) {
                return true;
            }
        }
        return false;
    }

    /** Is this user the owner of the change? */
    public boolean isOwner() {
        if (getUser().isIdentifiedUser()) {
            Account.Id id = getUser().asIdentifiedUser().getAccountId();
            return id.equals(getChange().getOwner());
        }
        return false;
    }

    /** Is this user assigned to this change? */
    public boolean isAssignee() {
        Account.Id currentAssignee = notes.getChange().getAssignee();
        if (currentAssignee != null && getUser().isIdentifiedUser()) {
            Account.Id id = getUser().getAccountId();
            return id.equals(currentAssignee);
        }
        return false;
    }

    /** Is this user a reviewer for the change? */
    public boolean isReviewer(ReviewDb db) throws OrmException {
        return isReviewer(db, null);
    }

    /** Is this user a reviewer for the change? */
    public boolean isReviewer(ReviewDb db, @Nullable ChangeData cd) throws OrmException {
        if (getUser().isIdentifiedUser()) {
            Collection<Account.Id> results = changeData(db, cd).reviewers().all();
            return results.contains(getUser().getAccountId());
        }
        return false;
    }

    public boolean isAdmin() {
        return getUser().getCapabilities().canAdministrateServer();
    }

    /** @return true if the user is allowed to remove this reviewer. */
    public boolean canRemoveReviewer(PatchSetApproval approval) {
        return canRemoveReviewer(approval.getAccountId(), approval.getValue());
    }

    public boolean canRemoveReviewer(Account.Id reviewer, int value) {
        if (getChange().getStatus().isOpen()) {
            // A user can always remove themselves.
            //
            if (getUser().isIdentifiedUser()) {
                if (getUser().getAccountId().equals(reviewer)) {
                    return true; // can remove self
                }
            }

            // The change owner may remove any zero or positive score.
            //
            if (isOwner() && 0 <= value) {
                return true;
            }

            // Users with the remove reviewer permission, the branch owner, project
            // owner and site admin can remove anyone
            if (getRefControl().canRemoveReviewer() // has removal permissions
                    || getRefControl().isOwner() // branch owner
                    || getProjectControl().isOwner() // project owner
                    || getUser().getCapabilities().canAdministrateServer()) {
                return true;
            }
        }

        return false;
    }

    /** Can this user edit the topic name? */
    private boolean canEditTopicName() {
        if (getChange().getStatus().isOpen()) {
            return isOwner() // owner (aka creator) of the change can edit topic
                    || getRefControl().isOwner() // branch owner can edit topic
                    || getProjectControl().isOwner() // project owner can edit topic
                    || getUser().getCapabilities().canAdministrateServer() // site administers are god
                    || getRefControl().canEditTopicName() // user can edit topic on a specific ref
            ;
        }
        return getRefControl().canForceEditTopicName();
    }

    /** Can this user edit the description? */
    private boolean canEditDescription() {
        if (getChange().getStatus().isOpen()) {
            return isOwner() // owner (aka creator) of the change can edit desc
                    || getRefControl().isOwner() // branch owner can edit desc
                    || getProjectControl().isOwner() // project owner can edit desc
                    || getUser().getCapabilities().canAdministrateServer() // site administers are god
            ;
        }
        return false;
    }

    private boolean canEditAssignee() {
        return isOwner() || getProjectControl().isOwner() || getRefControl().canEditAssignee() || isAssignee();
    }

    /** Can this user edit the hashtag name? */
    private boolean canEditHashtags() {
        return isOwner() // owner (aka creator) of the change can edit hashtags
                || getRefControl().isOwner() // branch owner can edit hashtags
                || getProjectControl().isOwner() // project owner can edit hashtags
                || getUser().getCapabilities().canAdministrateServer() // site administers are god
                || getRefControl().canEditHashtags(); // user can edit hashtag on a specific ref
    }

    private boolean match(String destBranch, String refPattern) {
        return RefPatternMatcher.getMatcher(refPattern).match(destBranch, getUser());
    }

    private ChangeData changeData(ReviewDb db, @Nullable ChangeData cd) {
        return cd != null ? cd : changeDataFactory.create(db, this);
    }

    public boolean isDraftVisible(ReviewDb db, ChangeData cd) throws OrmException {
        return isOwner() || isReviewer(db, cd) || getRefControl().canViewDrafts() || getUser().isInternalUser();
    }

    public boolean isPrivateVisible(ReviewDb db, ChangeData cd) throws OrmException {
        return isOwner() || isReviewer(db, cd) || getRefControl().canViewPrivateChanges()
                || getUser().isInternalUser();
    }

    ForChange asForChange(@Nullable ChangeData cd, @Nullable Provider<ReviewDb> db) {
        return new ForChangeImpl(cd, db);
    }

    private class ForChangeImpl extends ForChange {
        private ChangeData cd;
        private Map<String, PermissionRange> labels;

        ForChangeImpl(@Nullable ChangeData cd, @Nullable Provider<ReviewDb> db) {
            this.cd = cd;
            this.db = db;
        }

        private ReviewDb db() {
            if (db != null) {
                return db.get();
            } else if (cd != null) {
                return cd.db();
            } else {
                return null;
            }
        }

        private ChangeData changeData() {
            if (cd == null) {
                ReviewDb reviewDb = db();
                checkState(reviewDb != null, "need ReviewDb");
                cd = changeDataFactory.create(reviewDb, ChangeControl.this);
            }
            return cd;
        }

        @Override
        public CurrentUser user() {
            return getUser();
        }

        @Override
        public ForChange user(CurrentUser user) {
            return user().equals(user) ? this : forUser(user).asForChange(cd, db);
        }

        @Override
        public void check(ChangePermissionOrLabel perm) throws AuthException, PermissionBackendException {
            if (!can(perm)) {
                throw new AuthException(perm.describeForException() + " not permitted");
            }
        }

        @Override
        public <T extends ChangePermissionOrLabel> Set<T> test(Collection<T> permSet)
                throws PermissionBackendException {
            Set<T> ok = newSet(permSet);
            for (T perm : permSet) {
                if (can(perm)) {
                    ok.add(perm);
                }
            }
            return ok;
        }

        private boolean can(ChangePermissionOrLabel perm) throws PermissionBackendException {
            if (perm instanceof ChangePermission) {
                return can((ChangePermission) perm);
            } else if (perm instanceof LabelPermission) {
                return can((LabelPermission) perm);
            } else if (perm instanceof LabelPermission.WithValue) {
                return can((LabelPermission.WithValue) perm);
            }
            throw new PermissionBackendException(perm + " unsupported");
        }

        private boolean can(ChangePermission perm) throws PermissionBackendException {
            try {
                switch (perm) {
                case READ:
                    return isVisible(db(), changeData());
                case ABANDON:
                    return canAbandon(db());
                case DELETE:
                    return canDelete(db(), getChange().getStatus());
                case ADD_PATCH_SET:
                    return canAddPatchSet(db());
                case EDIT_ASSIGNEE:
                    return canEditAssignee();
                case EDIT_DESCRIPTION:
                    return canEditDescription();
                case EDIT_HASHTAGS:
                    return canEditHashtags();
                case EDIT_TOPIC_NAME:
                    return canEditTopicName();
                case REBASE:
                    return canRebase(db());
                case RESTORE:
                    return canRestore(db());
                case SUBMIT:
                    return getRefControl().canSubmit(isOwner());

                case REMOVE_REVIEWER: // TODO Honor specific removal filters?
                case SUBMIT_AS:
                    return getRefControl().canPerform(perm.permissionName().get());
                }
            } catch (OrmException e) {
                throw new PermissionBackendException("unavailable", e);
            }
            throw new PermissionBackendException(perm + " unsupported");
        }

        private boolean can(LabelPermission perm) {
            return !label(perm.permissionName().get()).isEmpty();
        }

        private boolean can(LabelPermission.WithValue perm) {
            PermissionRange r = label(perm.permissionName().get());
            if (perm.forUser() == ON_BEHALF_OF && r.isEmpty()) {
                return false;
            }
            return r.contains(perm.value());
        }

        private PermissionRange label(String permission) {
            if (labels == null) {
                labels = Maps.newHashMapWithExpectedSize(4);
            }
            PermissionRange r = labels.get(permission);
            if (r == null) {
                r = getRange(permission);
                labels.put(permission, r);
            }
            return r;
        }
    }

    static <T extends ChangePermissionOrLabel> Set<T> newSet(Collection<T> permSet) {
        if (permSet instanceof EnumSet) {
            @SuppressWarnings({ "unchecked", "rawtypes" })
            Set<T> s = ((EnumSet) permSet).clone();
            s.clear();
            return s;
        }
        return Sets.newHashSetWithExpectedSize(permSet.size());
    }
}