diff options
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.java | 412 |
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; } } |