Prevent pushing of patch sets onto invisible patch sets

A previous bug would allow users to push a patch set onto a draft patch
set they cannot see. This commit fixes the bug and adds some tests.

Change-Id: I58a8353f67a47361f6dbd3bf776969130d90a529
This commit is contained in:
Patrick Hiesel
2016-06-15 19:19:32 +02:00
parent 91afae99b8
commit 69ca28943b
4 changed files with 125 additions and 36 deletions

View File

@@ -520,10 +520,16 @@ public abstract class AbstractDaemonTest {
protected PushOneCommit.Result amendChange(String changeId, String ref)
throws Exception {
return amendChange(changeId, ref, admin, testRepo);
}
protected PushOneCommit.Result amendChange(String changeId, String ref,
TestAccount testAccount, TestRepository<?> repo) throws Exception {
Collections.shuffle(RANDOM);
PushOneCommit push =
pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT,
PushOneCommit.FILE_NAME, new String(Chars.toArray(RANDOM)), changeId);
pushFactory.create(db, testAccount.getIdent(), repo,
PushOneCommit.SUBJECT, PushOneCommit.FILE_NAME,
new String(Chars.toArray(RANDOM)), changeId);
return push.to(ref);
}

View File

@@ -34,6 +34,7 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestProjectInput;
@@ -64,6 +65,7 @@ import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.config.AnonymousCowardNameProvider;
import com.google.gerrit.server.git.ProjectConfig;
@@ -74,6 +76,8 @@ import com.google.gerrit.testutil.FakeEmailSender.Message;
import com.google.gerrit.testutil.NoteDbMode;
import com.google.gerrit.testutil.TestTimeUtil;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
@@ -1244,6 +1248,71 @@ public class ChangeIT extends AbstractDaemonTest {
.get();
}
@Test
public void createNewPatchSetOnVisibleDraftPatchSet() throws Exception {
// Clone separate repositories of the same project as admin and as user
TestRepository<InMemoryRepository> adminTestRepo =
cloneProject(project, admin);
TestRepository<InMemoryRepository> userTestRepo =
cloneProject(project, user);
// Create change as admin
PushOneCommit push = pushFactory.create(
db, admin.getIdent(), adminTestRepo);
PushOneCommit.Result r1 = push.to("refs/for/master");
r1.assertOkStatus();
// Amend draft as admin
PushOneCommit.Result r2 = amendChange(
r1.getChangeId(), "refs/drafts/master", admin, adminTestRepo);
r2.assertOkStatus();
// Add user as reviewer to make this patch set visible
AddReviewerInput in = new AddReviewerInput();
in.reviewer = user.email;
gApi.changes()
.id(r1.getChangeId())
.addReviewer(in);
// Fetch change
GitUtil.fetch(userTestRepo, r2.getPatchSet().getRefName() + ":ps");
userTestRepo.reset("ps");
// Amend change as user
PushOneCommit.Result r3 = amendChange(
r2.getChangeId(), "refs/drafts/master", user, userTestRepo);
r3.assertOkStatus();
}
@Test
public void createNewPatchSetOnInvisibleDraftPatchSet() throws Exception {
// Clone separate repositories of the same project as admin and as user
TestRepository<InMemoryRepository> adminTestRepo =
cloneProject(project, admin);
TestRepository<InMemoryRepository> userTestRepo =
cloneProject(project, user);
// Create change as admin
PushOneCommit push = pushFactory.create(
db, admin.getIdent(), adminTestRepo);
PushOneCommit.Result r1 = push.to("refs/for/master");
r1.assertOkStatus();
// Amend draft as admin
PushOneCommit.Result r2 = amendChange(
r1.getChangeId(), "refs/drafts/master", admin, adminTestRepo);
r2.assertOkStatus();
// Fetch change
GitUtil.fetch(userTestRepo, r1.getPatchSet().getRefName() + ":ps");
userTestRepo.reset("ps");
// Amend change as user
PushOneCommit.Result r3 = amendChange(
r1.getChangeId(), "refs/for/master", user, userTestRepo);
r3.assertErrorStatus("cannot replace "
+ r3.getChange().change().getChangeId() + ".");
}
private static Iterable<Account.Id> getReviewers(
Collection<AccountInfo> r) {
return Iterables.transform(r, new Function<AccountInfo, Account.Id>() {

View File

@@ -438,7 +438,8 @@ public class ReceiveCommits {
rp.setRefFilter(new RefFilter() {
@Override
public Map<String, Ref> filter(Map<String, Ref> refs) {
Map<String, Ref> filteredRefs = Maps.newHashMapWithExpectedSize(refs.size());
Map<String, Ref> filteredRefs =
Maps.newHashMapWithExpectedSize(refs.size());
for (Map.Entry<String, Ref> e : refs.entrySet()) {
String name = e.getKey();
if (!name.startsWith(REFS_CHANGES)
@@ -468,7 +469,8 @@ public class ReceiveCommits {
} catch (ServiceMayNotContinueException e) {
throw e;
} catch (IOException e) {
ServiceMayNotContinueException ex = new ServiceMayNotContinueException();
ServiceMayNotContinueException ex =
new ServiceMayNotContinueException();
ex.initCause(e);
throw ex;
}
@@ -698,7 +700,7 @@ public class ReceiveCommits {
new Function<ReplaceRequest, Integer>() {
@Override
public Integer apply(ReplaceRequest in) {
return in.change.getId().get();
return in.notes.getChangeId().get();
}
}));
if (!updated.isEmpty()) {
@@ -706,7 +708,7 @@ public class ReceiveCommits {
addMessage("Updated Changes:");
boolean edit = magicBranch != null && magicBranch.edit;
for (ReplaceRequest u : updated) {
addMessage(formatChangeUrl(canonicalWebUrl, u.change,
addMessage(formatChangeUrl(canonicalWebUrl, u.notes.getChange(),
u.info.getSubject(), edit));
}
addMessage("");
@@ -1832,7 +1834,7 @@ public class ReceiveCommits {
bySha.put(r.commitId, r.change);
}
for (ReplaceRequest r : replace) {
bySha.put(r.newCommitId, r.change);
bySha.put(r.newCommitId, r.notes.getChange());
}
Change tipChange = bySha.get(magicBranch.cmd.getNewId());
checkState(tipChange != null,
@@ -1908,7 +1910,7 @@ public class ReceiveCommits {
for (CheckedFuture<ChangeNotes, OrmException> f : futures) {
ChangeNotes notes = f.checkedGet();
if (notes.getChange() != null) {
replaceByChange.get(notes.getChangeId()).change = notes.getChange();
replaceByChange.get(notes.getChangeId()).notes = notes;
}
}
}
@@ -1918,7 +1920,7 @@ public class ReceiveCommits {
final ObjectId newCommitId;
final ReceiveCommand inputCommand;
final boolean checkMergedInto;
Change change;
ChangeNotes notes;
ChangeControl changeCtl;
BiMap<RevCommit, PatchSet.Id> revisions;
PatchSet.Id psId;
@@ -1954,12 +1956,12 @@ public class ReceiveCommits {
boolean validate(boolean autoClose) throws IOException, OrmException {
if (!autoClose && inputCommand.getResult() != NOT_ATTEMPTED) {
return false;
} else if (change == null) {
} else if (notes == null) {
reject(inputCommand, "change " + ontoChange + " not found");
return false;
}
priorPatchSet = change.currentPatchSetId();
priorPatchSet = notes.getChange().currentPatchSetId();
if (!revisions.containsValue(priorPatchSet)) {
reject(inputCommand, "change " + ontoChange + " missing revisions");
return false;
@@ -1974,7 +1976,7 @@ public class ReceiveCommits {
return false;
}
changeCtl = projectControl.controlFor(db, change);
changeCtl = projectControl.controlFor(notes);
if (!changeCtl.canAddPatchSet(db)) {
String locked = ".";
if (changeCtl.isPatchSetLocked(db)) {
@@ -1982,7 +1984,7 @@ public class ReceiveCommits {
}
reject(inputCommand, "cannot replace " + ontoChange + locked);
return false;
} else if (change.getStatus().isClosed()) {
} else if (notes.getChange().getStatus().isClosed()) {
reject(inputCommand, "change " + ontoChange + " closed");
return false;
} else if (revisions.containsKey(newCommit)) {
@@ -2057,7 +2059,7 @@ public class ReceiveCommits {
}
private boolean newEdit() {
psId = change.currentPatchSetId();
psId = notes.getChange().currentPatchSetId();
Optional<ChangeEdit> edit = null;
try {
@@ -2096,13 +2098,14 @@ public class ReceiveCommits {
newCommitId,
RefNames.refsEdit(
user.getAccountId(),
change.getId(),
notes.getChangeId(),
psId));
}
private void newPatchSet() throws IOException {
RevCommit newCommit = rp.getRevWalk().parseCommit(newCommitId);
psId = ChangeUtil.nextPatchSetId(allRefs, change.currentPatchSetId());
psId = ChangeUtil.nextPatchSetId(
allRefs, notes.getChange().currentPatchSetId());
info = patchSetInfoFactory.get(
rp.getRevWalk(), newCommit, psId);
cmd = new ReceiveCommand(
@@ -2128,12 +2131,12 @@ public class ReceiveCommits {
RevCommit priorCommit = revisions.inverse().get(priorPatchSet);
replaceOp = replaceOpFactory.create(requestScopePropagator,
projectControl, change.getDest(), checkMergedInto, priorPatchSet,
priorCommit, psId, newCommit, info, groups, magicBranch,
rp.getPushCertificate());
bu.addOp(change.getId(), replaceOp);
projectControl, notes.getChange().getDest(), checkMergedInto,
priorPatchSet, priorCommit, psId, newCommit, info, groups,
magicBranch, rp.getPushCertificate());
bu.addOp(notes.getChangeId(), replaceOp);
if (progress != null) {
bu.addOp(change.getId(), new ChangeProgressOp(progress));
bu.addOp(notes.getChangeId(), new ChangeProgressOp(progress));
}
}
@@ -2270,7 +2273,8 @@ public class ReceiveCommits {
return;
}
boolean defaultName = Strings.isNullOrEmpty(user.getAccount().getFullName());
boolean defaultName =
Strings.isNullOrEmpty(user.getAccount().getFullName());
RevWalk walk = rp.getRevWalk();
walk.reset();
walk.sort(RevSort.NONE);
@@ -2363,7 +2367,7 @@ public class ReceiveCommits {
}
SetMultimap<ObjectId, Ref> byCommit = changeRefsById();
Map<Change.Key, Change> byKey = null;
Map<Change.Key, ChangeNotes> byKey = null;
COMMIT: for (RevCommit c; (c = rw.next()) != null;) {
rw.parseBody(c);
@@ -2382,11 +2386,11 @@ public class ReceiveCommits {
byKey = openChangesByBranch(branch);
}
Change onto = byKey.get(new Change.Key(changeId.trim()));
ChangeNotes onto = byKey.get(new Change.Key(changeId.trim()));
if (onto != null) {
Change.Id id = onto.getId();
Change.Id id = onto.getChangeId();
final ReplaceRequest req = new ReplaceRequest(id, c, cmd, false);
req.change = onto;
req.notes = onto;
if (req.validate(true)) {
req.addOps(bu, null);
bu.addOp(
@@ -2414,11 +2418,11 @@ public class ReceiveCommits {
}
}
private Map<Change.Key, Change> openChangesByBranch(Branch.NameKey branch)
throws OrmException {
Map<Change.Key, Change> r = new HashMap<>();
private Map<Change.Key, ChangeNotes> openChangesByBranch(
Branch.NameKey branch) throws OrmException {
Map<Change.Key, ChangeNotes> r = new HashMap<>();
for (ChangeData cd : queryProvider.get().byBranchOpen(branch)) {
r.put(cd.change().getKey(), cd.change());
r.put(cd.change().getKey(), cd.notes());
}
return r;
}

View File

@@ -30,6 +30,7 @@ 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;
@@ -106,14 +107,17 @@ public class ChangeControl {
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) {
ApprovalsUtil approvalsUtil,
PatchSetUtil patchSetUtil) {
this.changeDataFactory = changeDataFactory;
this.notesFactory = notesFactory;
this.approvalsUtil = approvalsUtil;
this.patchSetUtil = patchSetUtil;
}
ChangeControl create(RefControl refControl, ReviewDb db, Project.NameKey
@@ -137,7 +141,7 @@ public class ChangeControl {
ChangeControl create(RefControl refControl, ChangeNotes notes) {
return new ChangeControl(changeDataFactory, approvalsUtil, refControl,
notes);
notes, patchSetUtil);
}
}
@@ -145,16 +149,19 @@ public class ChangeControl {
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) {
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) {
@@ -162,7 +169,7 @@ public class ChangeControl {
return this;
}
return new ChangeControl(changeDataFactory, approvalsUtil,
getRefControl().forUser(who), notes);
getRefControl().forUser(who), notes, patchSetUtil);
}
public RefControl getRefControl() {
@@ -307,8 +314,11 @@ public class ChangeControl {
}
/** Can this user add a patch set to this change? */
public boolean canAddPatchSet(ReviewDb db) throws OrmException {
return getRefControl().canUpload() && !isPatchSetLocked(db);
public boolean canAddPatchSet(ReviewDb db)
throws OrmException {
return getRefControl().canUpload()
&& !isPatchSetLocked(db)
&& isPatchVisible(patchSetUtil.current(db, notes), db);
}
/** Is the current patch set locked against state changes? */