Package com.google.gerrit.server.project

Source Code of com.google.gerrit.server.project.ChangeControl$Factory

// 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.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.SubmitRecord;
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.rules.PrologEnvironment;
import com.google.gerrit.rules.StoredValues;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
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.util.Providers;

import com.googlecode.prolog_cafe.compiler.CompileException;
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.PrologException;
import com.googlecode.prolog_cafe.lang.StructureTerm;
import com.googlecode.prolog_cafe.lang.Term;
import com.googlecode.prolog_cafe.lang.VariableTerm;

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

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.Nullable;


/** 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;

    @Inject
    GenericFactory(ProjectControl.GenericFactory p) {
      projectControl = p;
    }

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

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

    @Inject
    Factory(final ProjectControl.Factory p, final Provider<ReviewDb> 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;
    }
  }

  private final RefControl refControl;
  private final Change change;

  ChangeControl(final RefControl r, final Change c) {
    this.refControl = r;
    this.change = c;
  }

  public ChangeControl forUser(final CurrentUser who) {
    return new ChangeControl(getRefControl().forUser(who), getChange());
  }

  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 change;
  }

  /** Can this user see this change? */
  public boolean isVisible(ReviewDb db) throws OrmException {
    if (change.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.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() && 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() && 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
  }

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

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

  /** 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() instanceof IdentifiedUser) {
      final IdentifiedUser i = (IdentifiedUser) getCurrentUser();
      return i.getAccountId().equals(change.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() instanceof IdentifiedUser) {
      final IdentifiedUser user = (IdentifiedUser) getCurrentUser();
      Iterable<PatchSetApproval> results;
      if (cd != null) {
        results = cd.currentApprovals(Providers.of(db));
      } else {
        results = db.patchSetApprovals().byChange(change.getId());
      }
      for (PatchSetApproval approval : results) {
        if (user.getAccountId().equals(approval.getAccountId())) {
          return true;
        }
      }
    }
    return false;
  }

  /** @return true if the user is allowed to remove this reviewer. */
  public boolean canRemoveReviewer(PatchSetApproval approval) {
    if (getChange().getStatus().isOpen()) {
      // A user can always remove themselves.
      //
      if (getCurrentUser() instanceof IdentifiedUser) {
        final IdentifiedUser i = (IdentifiedUser) getCurrentUser();
        if (i.getAccountId().equals(approval.getAccountId())) {
          return true; // can remove self
        }
      }

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

      // The branch owner, project owner, site admin can remove anyone.
      //
      if (getRefControl().isOwner() // branch owner
          || getProjectControl().isOwner() // project owner
          || getCurrentUser().getCapabilities().canAdministrateServer()) {
        return true;
      }
    }

    return false;
  }

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

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

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

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

    try {
      if (change.getStatus() == Change.Status.DRAFT) {
        if (!isDraftVisible(db, cd)) {
          return ruleError("Patch set " + patchSet.getPatchSetId() + " not found");
        } else {
          return ruleError("Cannot submit draft changes");
        }
      }
      if (patchSet.isDraft()) {
        if (!isDraftVisible(db, cd)) {
          return ruleError("Patch set " + patchSet.getPatchSetId() + " not found");
        } else {
          return ruleError("Cannot submit draft patch sets");
        }
      }
    } catch (OrmException err) {
      return logRuleError("Cannot read patch set " + patchSet.getId(), err);
    }

    List<Term> results = new ArrayList<Term>();
    Term submitRule;
    ProjectState projectState = getProjectControl().getProjectState();
    PrologEnvironment env;

    try {
      env = projectState.newPrologEnvironment();
    } catch (CompileException err) {
      return logRuleError("Cannot consult rules.pl for "
          + getProject().getName(), err);
    }

    try {
      env.set(StoredValues.REVIEW_DB, db);
      env.set(StoredValues.CHANGE, change);
      env.set(StoredValues.CHANGE_DATA, cd);
      env.set(StoredValues.PATCH_SET, patchSet);
      env.set(StoredValues.CHANGE_CONTROL, this);

      submitRule = env.once(
        "gerrit", "locate_submit_rule",
        new VariableTerm());
      if (submitRule == null) {
        return logRuleError("No user:submit_rule found for "
            + getProject().getName());
      }

      if (fastEvalLabels) {
        env.once("gerrit", "assume_range_from_label");
      }

      try {
        for (Term[] template : env.all(
            "gerrit", "can_submit",
            submitRule,
            new VariableTerm())) {
          results.add(template[1]);
        }
      } catch (PrologException err) {
        return logRuleError("Exception calling " + submitRule + " on change "
            + change.getId() + " of " + getProject().getName(), err);
      } catch (RuntimeException err) {
        return logRuleError("Exception calling " + submitRule + " on change "
            + change.getId() + " of " + getProject().getName(), err);
      }

      ProjectState parentState = projectState.getParentState();
      PrologEnvironment childEnv = env;
      Set<Project.NameKey> projectsSeen = new HashSet<Project.NameKey>();
      projectsSeen.add(getProject().getNameKey());

      while (parentState != null) {
        if (!projectsSeen.add(parentState.getProject().getNameKey())) {
          //parent has been seen before, stop walk up inheritance tree
          break;
        }
        PrologEnvironment parentEnv;
        try {
          parentEnv = parentState.newPrologEnvironment();
        } catch (CompileException err) {
          return logRuleError("Cannot consult rules.pl for "
              + parentState.getProject().getName(), err);
        }

        parentEnv.copyStoredValues(childEnv);
        Term filterRule =
            parentEnv.once("gerrit", "locate_submit_filter", new VariableTerm());
        if (filterRule != null) {
          try {
            if (fastEvalLabels) {
              env.once("gerrit", "assume_range_from_label");
            }

            Term resultsTerm = toListTerm(results);
            results.clear();
            Term[] template = parentEnv.once(
                "gerrit", "filter_submit_results",
                filterRule,
                resultsTerm,
                new VariableTerm());
            @SuppressWarnings("unchecked")
            final List<? extends Term> termList = ((ListTerm) template[2]).toJava();
            results.addAll(termList);
          } catch (PrologException err) {
            return logRuleError("Exception calling " + filterRule + " on change "
                + change.getId() + " of " + parentState.getProject().getName(), err);
          } catch (RuntimeException err) {
            return logRuleError("Exception calling " + filterRule + " on change "
                + change.getId() + " of " + parentState.getProject().getName(), err);
          }
        }

        parentState = parentState.getParentState();
        childEnv = parentEnv;
      }
    } finally {
      env.close();
    }

    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 " + submitRule + " for change " + change.getId()
          + " of " + getProject().getName() + " has no solution.");
      return ruleError("Project submit rule has no solution");
    }

    // 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.
    //
    List<SubmitRecord> out = new ArrayList<SubmitRecord>(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.Label> (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);

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

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

    return out;
  }

  private List<SubmitRecord> logInvalidResult(Term rule, Term record) {
    return logRuleError("Submit rule " + rule + " for change " + change.getId()
        + " of " + getProject().getName() + " output invalid result: " + record);
  }

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

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

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

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

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

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

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

Related Classes of com.google.gerrit.server.project.ChangeControl$Factory

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.