blob: 40be5f507a59f73dec7dfd9b8d41577d37dcdc2e [file] [log] [blame]
// 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 com.google.common.collect.Lists;
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.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.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
/** 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().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? */
public 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() || getRefControl().canDeleteChanges(isOwner()));
case MERGED:
default:
return false;
}
}
/** Can this user rebase this change? */
public boolean canRebase(ReviewDb db) throws OrmException {
return (isOwner() || getRefControl().canSubmit(isOwner()) || getRefControl().canRebase())
&& !isPatchSetLocked(db);
}
/** Can this user restore this change? */
public 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? */
public 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? */
public 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;
}
public boolean canEditAssignee() {
return isOwner()
|| getProjectControl().isOwner()
|| getRefControl().canEditAssignee()
|| isAssignee();
}
/** Can this user edit the hashtag name? */
public 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
}
public boolean canSubmit() {
return getRefControl().canSubmit(isOwner());
}
public boolean canSubmitAs() {
return getRefControl().canSubmitAs();
}
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();
}
}