All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.project.ChangeControl Maven / Gradle / Ivy

// 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 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.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitTypeRecord;
import com.google.gerrit.extensions.common.SubmitType;
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.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
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.Provider;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;

import com.googlecode.prolog_cafe.lang.IntegerTerm;
import com.googlecode.prolog_cafe.lang.ListTerm;
import com.googlecode.prolog_cafe.lang.Prolog;
import com.googlecode.prolog_cafe.lang.StructureTerm;
import com.googlecode.prolog_cafe.lang.SymbolTerm;
import com.googlecode.prolog_cafe.lang.Term;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;


/** Access control management for a user accessing a single change. */
public class ChangeControl {
  private static final Logger log = LoggerFactory
      .getLogger(ChangeControl.class);

  public static class GenericFactory {
    private final ProjectControl.GenericFactory projectControl;
    private final Provider db;

    @Inject
    GenericFactory(ProjectControl.GenericFactory p, Provider d) {
      projectControl = p;
      db = d;
    }

    public ChangeControl controlFor(Change change, CurrentUser user)
        throws NoSuchChangeException {
      final Project.NameKey projectKey = change.getProject();
      try {
        return projectControl.controlFor(projectKey, user).controlFor(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 validateFor(Change change, CurrentUser user)
        throws NoSuchChangeException, OrmException {
      ChangeControl c = controlFor(change, user);
      if (!c.isVisible(db.get())) {
        throw new NoSuchChangeException(c.getChange().getId());
      }
      return c;
    }
  }

  public static class Factory {
    private final ProjectControl.Factory projectControl;
    private final Provider db;

    @Inject
    Factory(final ProjectControl.Factory p, final Provider d) {
      projectControl = p;
      db = d;
    }

    public ChangeControl controlFor(final Change.Id id)
        throws NoSuchChangeException {
      final Change change;
      try {
        change = db.get().changes().get(id);
        if (change == null) {
          throw new NoSuchChangeException(id);
        }
      } catch (OrmException e) {
        throw new NoSuchChangeException(id, e);
      }
      return controlFor(change);
    }

    public ChangeControl controlFor(final Change change)
        throws NoSuchChangeException {
      try {
        final Project.NameKey projectKey = change.getProject();
        return projectControl.validateFor(projectKey).controlFor(change);
      } catch (NoSuchProjectException e) {
        throw new NoSuchChangeException(change.getId(), e);
      }
    }

    public ChangeControl validateFor(final Change.Id id)
        throws NoSuchChangeException, OrmException {
      return validate(controlFor(id), db.get());
    }

    public ChangeControl validateFor(final Change change)
        throws NoSuchChangeException, OrmException {
      return validate(controlFor(change), db.get());
    }

    private static ChangeControl validate(final ChangeControl c, final ReviewDb db)
        throws NoSuchChangeException, OrmException{
      if (!c.isVisible(db)) {
        throw new NoSuchChangeException(c.getChange().getId());
      }
      return c;
    }
  }

  public interface AssistedFactory {
    ChangeControl create(RefControl refControl, Change change);
    ChangeControl create(RefControl refControl, ChangeNotes notes);
  }

  /**
   * Exception thrown when the label term of a submit record
   * unexpectedly didn't contain a user term.
   */
  private static class UserTermExpected extends Exception {
    private static final long serialVersionUID = 1L;

    public UserTermExpected(SubmitRecord.Label label) {
      super(String.format("A label with the status %s must contain a user.",
          label.toString()));
    }
  }

  private final ChangeData.Factory changeDataFactory;
  private final RefControl refControl;
  private final ChangeNotes notes;

  @AssistedInject
  ChangeControl(
      ChangeData.Factory changeDataFactory,
      ChangeNotes.Factory notesFactory,
      @Assisted RefControl refControl,
      @Assisted Change change) {
    this(changeDataFactory, refControl,
        notesFactory.create(change));
  }

  @AssistedInject
  ChangeControl(
      ChangeData.Factory changeDataFactory,
      @Assisted RefControl refControl,
      @Assisted ChangeNotes notes) {
    this.changeDataFactory = changeDataFactory;
    this.refControl = refControl;
    this.notes = notes;
  }

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

  public RefControl getRefControl() {
    return refControl;
  }

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

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

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

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

  public ChangeNotes getNotes() {
    return notes;
  }

  /** Can this user see this change? */
  public boolean isVisible(ReviewDb db) throws OrmException {
    if (getChange().getStatus() == Change.Status.DRAFT
        && !isDraftVisible(db, null)) {
      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 abandon this change? */
  public boolean canAbandon() {
    return isOwner() // owner (aka creator) of the change can abandon
        || getRefControl().isOwner() // branch owner can abandon
        || getProjectControl().isOwner() // project owner can abandon
        || getCurrentUser().getCapabilities().canAdministrateServer() // site administers are god
        || getRefControl().canAbandon() // user can abandon a specific ref
    ;
  }

  /** 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 draft change or any draft patch set of this change? */
  public boolean canDeleteDraft(final ReviewDb db) throws OrmException {
    return (isOwner() || getRefControl().canDeleteDrafts())
        && isVisible(db);
  }

  /** Can this user rebase this change? */
  public boolean canRebase() {
    return isOwner() || getRefControl().canSubmit()
        || getRefControl().canRebase();
  }

  /** Can this user restore this change? */
  public boolean canRestore() {
    return canAbandon() // 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 all = getProjectControl().getLabelTypes().getLabelTypes();

    List r = Lists.newArrayListWithCapacity(all.size());
    for (LabelType l : all) {
      List 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 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() {
    return getRefControl().canUpload();
  }

  /** Is this user the owner of the change? */
  public boolean isOwner() {
    if (getCurrentUser().isIdentifiedUser()) {
      final IdentifiedUser i = (IdentifiedUser) getCurrentUser();
      return i.getAccountId().equals(getChange().getOwner());
    }
    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 (getCurrentUser().isIdentifiedUser()) {
      Collection results = changeData(db, cd).reviewers().values();
      IdentifiedUser user = (IdentifiedUser) getCurrentUser();
      return results.contains(user.getAccountId());
    }
    return false;
  }

  /** @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 (getCurrentUser().isIdentifiedUser()) {
        final IdentifiedUser i = (IdentifiedUser) getCurrentUser();
        if (i.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
          || getCurrentUser().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
          || getCurrentUser().getCapabilities().canAdministrateServer() // site administers are god
          || getRefControl().canEditTopicName() // user can edit topic on a specific ref
      ;
    } else {
      return getRefControl().canForceEditTopicName();
    }
  }

  public List getSubmitRecords(ReviewDb db, PatchSet patchSet) {
    return canSubmit(db, patchSet, null, false, true, false);
  }

  public boolean canSubmit() {
    return getRefControl().canSubmit();
  }

  public boolean canSubmitAs() {
    return getRefControl().canSubmitAs();
  }

  public List canSubmit(ReviewDb db, PatchSet patchSet) {
    return canSubmit(db, patchSet, null, false, false, false);
  }

  public List canSubmit(ReviewDb db, PatchSet patchSet,
      @Nullable ChangeData cd, boolean fastEvalLabels, boolean allowClosed,
      boolean allowDraft) {
    if (!allowClosed && getChange().getStatus().isClosed()) {
      SubmitRecord rec = new SubmitRecord();
      rec.status = SubmitRecord.Status.CLOSED;
      return Collections.singletonList(rec);
    }

    if (!patchSet.getId().equals(getChange().currentPatchSetId())) {
      return ruleError("Patch set " + patchSet.getPatchSetId() + " is not current");
    }

    cd = changeData(db, cd);
    if ((getChange().getStatus() == Change.Status.DRAFT || patchSet.isDraft())
        && !allowDraft) {
      return cannotSubmitDraft(db, patchSet, cd);
    }

    List results;
    SubmitRuleEvaluator evaluator;
    try {
      evaluator = new SubmitRuleEvaluator(db, patchSet,
          getProjectControl(),
          this, getChange(), cd,
          fastEvalLabels,
          "locate_submit_rule", "can_submit",
          "locate_submit_filter", "filter_submit_results");
      results = evaluator.evaluate();
    } catch (RuleEvalException e) {
      return logRuleError(e.getMessage(), e);
    }

    if (results.isEmpty()) {
      // This should never occur. A well written submit rule will always produce
      // at least one result informing the caller of the labels that are
      // required for this change to be submittable. Each label will indicate
      // whether or not that is actually possible given the permissions.
      log.error("Submit rule '" + evaluator.getSubmitRule() + "' for change "
          + getChange().getId() + " of " + getProject().getName()
          + " has no solution.");
      return ruleError("Project submit rule has no solution");
    }

    return resultsToSubmitRecord(evaluator.getSubmitRule(), results);
  }

  private boolean match(String destBranch, String refPattern) {
    return RefPatternMatcher.getMatcher(refPattern).match(destBranch,
        this.getRefControl().getCurrentUser().getUserName());
  }

  private List cannotSubmitDraft(ReviewDb db, PatchSet patchSet,
      @Nullable ChangeData cd) {
    try {
      if (!isDraftVisible(db, cd)) {
        return ruleError("Patch set " + patchSet.getPatchSetId() + " not found");
      } else if (patchSet.isDraft()) {
        return ruleError("Cannot submit draft patch sets");
      } else {
        return ruleError("Cannot submit draft changes");
      }
    } catch (OrmException err) {
      return logRuleError("Cannot read patch set " + patchSet.getId(), err);
    }
  }

  /**
   * Convert the results from Prolog Cafe's format to Gerrit's common format.
   *
   * can_submit/1 terminates when an ok(P) record is found. Therefore walk
   * the results backwards, using only that ok(P) record if it exists. This
   * skips partial results that occur early in the output. Later after the loop
   * the out collection is reversed to restore it to the original ordering.
   */
  public List resultsToSubmitRecord(Term submitRule, List results) {
    List out = new ArrayList<>(results.size());
    for (int resultIdx = results.size() - 1; 0 <= resultIdx; resultIdx--) {
      Term submitRecord = results.get(resultIdx);
      SubmitRecord rec = new SubmitRecord();
      out.add(rec);

      if (!submitRecord.isStructure() || 1 != submitRecord.arity()) {
        return logInvalidResult(submitRule, submitRecord);
      }

      if ("ok".equals(submitRecord.name())) {
        rec.status = SubmitRecord.Status.OK;

      } else if ("not_ready".equals(submitRecord.name())) {
        rec.status = SubmitRecord.Status.NOT_READY;

      } else {
        return logInvalidResult(submitRule, submitRecord);
      }

      // Unpack the one argument. This should also be a structure with one
      // argument per label that needs to be reported on to the caller.
      //
      submitRecord = submitRecord.arg(0);

      if (!submitRecord.isStructure()) {
        return logInvalidResult(submitRule, submitRecord);
      }

      rec.labels = new ArrayList<>(submitRecord.arity());

      for (Term state : ((StructureTerm) submitRecord).args()) {
        if (!state.isStructure() || 2 != state.arity() || !"label".equals(state.name())) {
          return logInvalidResult(submitRule, submitRecord);
        }

        SubmitRecord.Label lbl = new SubmitRecord.Label();
        rec.labels.add(lbl);

        lbl.label = state.arg(0).name();
        Term status = state.arg(1);

        try {
          if ("ok".equals(status.name())) {
            lbl.status = SubmitRecord.Label.Status.OK;
            appliedBy(lbl, status);

          } else if ("reject".equals(status.name())) {
            lbl.status = SubmitRecord.Label.Status.REJECT;
            appliedBy(lbl, status);

          } else if ("need".equals(status.name())) {
            lbl.status = SubmitRecord.Label.Status.NEED;

          } else if ("may".equals(status.name())) {
            lbl.status = SubmitRecord.Label.Status.MAY;

          } else if ("impossible".equals(status.name())) {
            lbl.status = SubmitRecord.Label.Status.IMPOSSIBLE;

          } else {
            return logInvalidResult(submitRule, submitRecord);
          }
        } catch (UserTermExpected e) {
          return logInvalidResult(submitRule, submitRecord, e.getMessage());
        }
      }

      if (rec.status == SubmitRecord.Status.OK) {
        break;
      }
    }
    Collections.reverse(out);

    return out;
  }

  public SubmitTypeRecord getSubmitTypeRecord(ReviewDb db, PatchSet patchSet) {
    return getSubmitTypeRecord(db, patchSet, null);
  }

  public SubmitTypeRecord getSubmitTypeRecord(ReviewDb db, PatchSet patchSet,
      @Nullable ChangeData cd) {
    cd = changeData(db, cd);
    try {
      if (getChange().getStatus() == Change.Status.DRAFT
          && !isDraftVisible(db, cd)) {
        return typeRuleError("Patch set " + patchSet.getPatchSetId()
            + " not found");
      }
      if (patchSet.isDraft() && !isDraftVisible(db, cd)) {
        return typeRuleError("Patch set " + patchSet.getPatchSetId()
            + " not found");
      }
    } catch (OrmException err) {
      return logTypeRuleError("Cannot read patch set " + patchSet.getId(),
          err);
    }

    List results;
    SubmitRuleEvaluator evaluator;
    try {
      evaluator = new SubmitRuleEvaluator(db, patchSet,
          getProjectControl(), this, getChange(), cd,
          false,
          "locate_submit_type", "get_submit_type",
          "locate_submit_type_filter", "filter_submit_type_results");
      results = evaluator.evaluate();
    } catch (RuleEvalException e) {
      return logTypeRuleError(e.getMessage(), e);
    }

    if (results.isEmpty()) {
      // Should never occur for a well written rule
      log.error("Submit rule '" + evaluator.getSubmitRule() + "' for change "
          + getChange().getId() + " of " + getProject().getName()
          + " has no solution.");
      return typeRuleError("Project submit rule has no solution");
    }

    Term typeTerm = results.get(0);
    if (!typeTerm.isSymbol()) {
      log.error("Submit rule '" + evaluator.getSubmitRule() + "' for change "
          + getChange().getId() + " of " + getProject().getName()
          + " did not return a symbol.");
      return typeRuleError("Project submit rule has invalid solution");
    }

    String typeName = ((SymbolTerm)typeTerm).name();
    try {
      return SubmitTypeRecord.OK(
          SubmitType.valueOf(typeName.toUpperCase()));
    } catch (IllegalArgumentException e) {
      return logInvalidType(evaluator.getSubmitRule(), typeName);
    }
  }

  private List logInvalidResult(Term rule, Term record, String reason) {
    return logRuleError("Submit rule " + rule + " for change " + getChange().getId()
        + " of " + getProject().getName() + " output invalid result: " + record
        + (reason == null ? "" : ". Reason: " + reason));
  }

  private List logInvalidResult(Term rule, Term record) {
    return logInvalidResult(rule, record, null);
  }

  private List logRuleError(String err, Exception e) {
    log.error(err, e);
    return ruleError("Error evaluating project rules, check server log");
  }

  private List logRuleError(String err) {
    log.error(err);
    return ruleError("Error evaluating project rules, check server log");
  }

  private List ruleError(String err) {
    SubmitRecord rec = new SubmitRecord();
    rec.status = SubmitRecord.Status.RULE_ERROR;
    rec.errorMessage = err;
    return Collections.singletonList(rec);
  }

  private SubmitTypeRecord logInvalidType(Term rule, String record) {
    return logTypeRuleError("Submit type rule " + rule + " for change "
        + getChange().getId() + " of " + getProject().getName()
        + " output invalid result: " + record);
  }

  private SubmitTypeRecord logTypeRuleError(String err, Exception e) {
    log.error(err, e);
    return typeRuleError("Error evaluating project type rules, check server log");
  }

  private SubmitTypeRecord logTypeRuleError(String err) {
    log.error(err);
    return typeRuleError("Error evaluating project type rules, check server log");
  }

  private SubmitTypeRecord typeRuleError(String err) {
    SubmitTypeRecord rec = new SubmitTypeRecord();
    rec.status = SubmitTypeRecord.Status.RULE_ERROR;
    rec.errorMessage = err;
    return rec;
  }

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

  private void appliedBy(SubmitRecord.Label label, Term status)
      throws UserTermExpected {
    if (status.isStructure() && status.arity() == 1) {
      Term who = status.arg(0);
      if (isUser(who)) {
        label.appliedBy = new Account.Id(((IntegerTerm) who.arg(0)).intValue());
      } else {
        throw new UserTermExpected(label);
      }
    }
  }

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

  private static boolean isUser(Term who) {
    return who.isStructure()
        && who.arity() == 1
        && who.name().equals("user")
        && who.arg(0).isInteger();
  }

  public static Term toListTerm(List terms) {
    Term list = Prolog.Nil;
    for (int i = terms.size() - 1; i >= 0; i--) {
      list = new ListTerm(terms.get(i), list);
    }
    return list;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy