summaryrefslogtreecommitdiffstats
path: root/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
diff options
context:
space:
mode:
Diffstat (limited to 'gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java')
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java412
1 files changed, 342 insertions, 70 deletions
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 3291f1ac3b..d17762ee15 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -14,29 +14,47 @@
package com.google.gerrit.server.project;
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.ChangeUtil;
+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.workflow.CategoryFunction;
-import com.google.gerrit.server.workflow.FunctionState;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
+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;
/** 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;
@@ -91,18 +109,18 @@ public class ChangeControl {
}
public ChangeControl validateFor(final Change.Id id)
- throws NoSuchChangeException {
- return validate(controlFor(id));
+ throws NoSuchChangeException, OrmException {
+ return validate(controlFor(id), db.get());
}
public ChangeControl validateFor(final Change change)
- throws NoSuchChangeException {
- return validate(controlFor(change));
+ throws NoSuchChangeException, OrmException {
+ return validate(controlFor(change), db.get());
}
- private static ChangeControl validate(final ChangeControl c)
- throws NoSuchChangeException {
- if (!c.isVisible()) {
+ 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;
@@ -117,10 +135,6 @@ public class ChangeControl {
this.change = c;
}
- public ChangeControl forAnonymousUser() {
- return new ChangeControl(getRefControl().forAnonymousUser(), getChange());
- }
-
public ChangeControl forUser(final CurrentUser who) {
return new ChangeControl(getRefControl().forUser(who), getChange());
}
@@ -146,26 +160,64 @@ public class ChangeControl {
}
/** Can this user see this change? */
- public boolean isVisible() {
+ public boolean isVisible(ReviewDb db) throws OrmException {
+ if (change.getStatus() == Change.Status.DRAFT && !isDraftVisible(db)) {
+ 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)) {
+ 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().isAdministrator() // site administers are god
+ || getCurrentUser().getCapabilities().canAdministrateServer() // site administers are god
;
}
+ /** 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
}
- public short normalize(ApprovalCategory.Id category, short score) {
- return getRefControl().normalize(category, score);
+ /** 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? */
@@ -182,6 +234,21 @@ public class ChangeControl {
return false;
}
+ /** Is this user a reviewer for the change? */
+ public boolean isReviewer(ReviewDb db) throws OrmException {
+ if (getCurrentUser() instanceof IdentifiedUser) {
+ final IdentifiedUser user = (IdentifiedUser) getCurrentUser();
+ ResultSet<PatchSetApproval> 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()) {
@@ -204,7 +271,7 @@ public class ChangeControl {
//
if (getRefControl().isOwner() // branch owner
|| getProjectControl().isOwner() // project owner
- || getCurrentUser().isAdministrator()) {
+ || getCurrentUser().getCapabilities().canAdministrateServer()) {
return true;
}
}
@@ -212,62 +279,267 @@ public class ChangeControl {
return false;
}
- /** @return {@link CanSubmitResult#OK}, or a result with an error message. */
- public CanSubmitResult canSubmit(final PatchSet.Id patchSetId) {
+ public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet.Id patchSetId) {
if (change.getStatus().isClosed()) {
- return new CanSubmitResult("Change " + change.getId() + " is closed");
+ SubmitRecord rec = new SubmitRecord();
+ rec.status = SubmitRecord.Status.CLOSED;
+ return Collections.singletonList(rec);
}
+
if (!patchSetId.equals(change.currentPatchSetId())) {
- return new CanSubmitResult("Patch set " + patchSetId + " is not current");
+ return ruleError("Patch set " + patchSetId + " is not current");
}
- if (!getRefControl().canSubmit()) {
- return new CanSubmitResult("User does not have permission to submit");
+
+ try {
+ if (change.getStatus() == Change.Status.DRAFT){
+ if (!isVisible(db)) {
+ return ruleError("Patch set " + patchSetId + " not found");
+ } else {
+ return ruleError("Cannot submit draft changes");
+ }
+ }
+ if (isDraftPatchSet(patchSetId, db)) {
+ if (!isVisible(db)) {
+ return ruleError("Patch set " + patchSetId + " not found");
+ } else {
+ return ruleError("Cannot submit draft patch sets");
+ }
+ }
+ } catch (OrmException err) {
+ return logRuleError("Cannot read patch set " + patchSetId, err);
}
- if (!(getCurrentUser() instanceof IdentifiedUser)) {
- return new CanSubmitResult("User is not signed-in");
+
+ 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);
}
- return CanSubmitResult.OK;
- }
- /** @return {@link CanSubmitResult#OK}, or a result with an error message. */
- public CanSubmitResult canSubmit(final PatchSet.Id patchSetId, final ReviewDb db,
- final ApprovalTypes approvalTypes,
- FunctionState.Factory functionStateFactory)
- throws OrmException {
+ try {
+ env.set(StoredValues.REVIEW_DB, db);
+ env.set(StoredValues.CHANGE, change);
+ env.set(StoredValues.PATCH_SET_ID, patchSetId);
+ 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());
+ }
+
+ 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 {
+ 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);
+ }
+ }
- CanSubmitResult result = canSubmit(patchSetId);
- if (result != CanSubmitResult.OK) {
- return result;
+ parentState = parentState.getParentState();
+ childEnv = parentEnv;
+ }
+ } finally {
+ env.close();
}
- final List<PatchSetApproval> allApprovals =
- new ArrayList<PatchSetApproval>(db.patchSetApprovals().byPatchSet(
- patchSetId).toList());
- final PatchSetApproval myAction =
- ChangeUtil.createSubmitApproval(patchSetId,
- (IdentifiedUser) getCurrentUser(), db);
-
- final ApprovalType actionType =
- approvalTypes.getApprovalType(myAction.getCategoryId());
- if (actionType == null || !actionType.getCategory().isAction()) {
- return new CanSubmitResult("Invalid action " + myAction.getCategoryId());
+ 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");
}
- final FunctionState fs =
- functionStateFactory.create(change, patchSetId, allApprovals);
- for (ApprovalType c : approvalTypes.getApprovalTypes()) {
- CategoryFunction.forCategory(c.getCategory()).run(c, fs);
+ // 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 ("impossible".equals(status.name())) {
+ lbl.status = SubmitRecord.Label.Status.IMPOSSIBLE;
+
+ } else {
+ return logInvalidResult(submitRule, submitRecord);
+ }
+ }
+
+ if (rec.status == SubmitRecord.Status.OK) {
+ break;
+ }
}
- if (!CategoryFunction.forCategory(actionType.getCategory()).isValid(
- getCurrentUser(), actionType, fs)) {
- return new CanSubmitResult(actionType.getCategory().getName()
- + " not permitted");
+ 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());
+ }
}
- fs.normalize(actionType, myAction);
- if (myAction.getValue() <= 0) {
- return new CanSubmitResult(actionType.getCategory().getName()
- + " not permitted");
+ }
+
+ private boolean isDraftVisible(ReviewDb db) throws OrmException {
+ return isOwner() || isReviewer(db);
+ }
+
+ private boolean isDraftPatchSet(PatchSet.Id id, ReviewDb db) throws OrmException {
+ PatchSet ps = db.patchSets().get(id);
+ if (ps == null) {
+ throw new OrmException("Patch set " + id + " not found");
+ }
+ return ps.isDraft();
+ }
+
+ 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 CanSubmitResult.OK;
+ return list;
}
}