blob: 2e706b804c819c07f9a5682f246f6cda751c135d [file] [log] [blame]
// Copyright (C) 2016 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.acceptance.rest.account;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
import static com.google.gerrit.entities.RefNames.changeMetaRef;
import static com.google.gerrit.entities.RefNames.patchSetRef;
import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.UseLocalDisk;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.entities.HumanComment;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RobotComment;
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.api.groups.GroupInput;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.project.testing.TestLabels;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import org.apache.http.Header;
import org.apache.http.message.BasicHeader;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ReflogEntry;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class ImpersonationIT extends AbstractDaemonTest {
@Inject private AccountControl.Factory accountControlFactory;
@Inject private ApprovalsUtil approvalsUtil;
@Inject private ChangeMessagesUtil cmUtil;
@Inject private CommentsUtil commentsUtil;
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
private TestAccount admin2;
private GroupInfo newGroup;
@Before
public void setUp() throws Exception {
admin2 = accountCreator.admin2();
GroupInput gi = new GroupInput();
gi.name = name("New-Group");
gi.members = ImmutableList.of(user.id().toString());
newGroup = gApi.groups().create(gi).get();
}
@After
public void tearDown() throws Exception {
removeRunAs();
}
@Test
@UseLocalDisk
public void voteOnBehalfOf() throws Exception {
allowCodeReviewOnBehalfOf();
TestAccount realUser = admin;
TestAccount impersonatedUser = user;
PushOneCommit.Result r = createChange();
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
try (Repository repo = repoManager.openRepository(project)) {
String changeMetaRef = changeMetaRef(r.getChange().getId());
createRefLogFileIfMissing(repo, changeMetaRef);
ReviewInput in = ReviewInput.recommend();
in.onBehalfOf = impersonatedUser.id().toString();
in.message = "Message on behalf of";
revision.review(in);
PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
assertThat(psa.patchSetId().get()).isEqualTo(1);
assertThat(psa.label()).isEqualTo("Code-Review");
assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
assertThat(psa.value()).isEqualTo(1);
assertThat(psa.realAccountId()).isEqualTo(realUser.id());
assertLastChangeMessage(r.getChange(), in.message, impersonatedUser, realUser);
// The change meta commit is created by the server and has the impersonated user as the
// author.
// Person idents of users in NoteDb commits are obfuscated due to privacy reasons.
RevCommit changeMetaCommit = projectOperations.project(project).getHead(changeMetaRef);
assertThat(changeMetaCommit.getCommitterIdent().getEmailAddress())
.isEqualTo(serverIdent.get().getEmailAddress());
assertThat(changeMetaCommit.getAuthorIdent().getEmailAddress())
.isEqualTo(changeNoteUtil.getAccountIdAsEmailAddress(impersonatedUser.id()));
// The ref log for the change meta ref records the impersonated user.
ReflogEntry changeMetaRefLogEntry = repo.getReflogReader(changeMetaRef).getLastEntry();
assertThat(changeMetaRefLogEntry.getWho().getEmailAddress())
.isEqualTo(impersonatedUser.email());
}
}
@Test
public void overrideImpersonatedVoteWithOtherImpersonatedVote_sameValue() throws Exception {
allowCodeReviewOnBehalfOf();
TestAccount realUser = admin;
TestAccount realUser2 = admin2;
TestAccount impersonatedUser = user;
PushOneCommit.Result r = createChange();
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
// realUser votes Code-Review+1 on behalf of impersonatedUser
ReviewInput in = ReviewInput.recommend();
in.onBehalfOf = impersonatedUser.id().toString();
in.message = "Message on behalf of";
revision.review(in);
PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
assertThat(psa.patchSetId().get()).isEqualTo(1);
assertThat(psa.label()).isEqualTo("Code-Review");
assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
assertThat(psa.value()).isEqualTo(1);
assertThat(psa.realAccountId()).isEqualTo(realUser.id());
assertLastChangeMessage(r.getChange(), in.message, impersonatedUser, realUser);
// realUser2 votes Code-Review+1 on behalf of impersonatedUser, this should override the
// impersonated Code-Review+1 of realUser with an impersonated Code-Review+1 of realUser2
requestScopeOperations.setApiUser(realUser2.id());
in = ReviewInput.recommend();
in.onBehalfOf = impersonatedUser.id().toString();
in.message = "Another message on behalf of";
gApi.changes().id(r.getChangeId()).current().review(in);
psa = Iterables.getOnlyElement(r.getChange().approvals().values());
assertThat(psa.patchSetId().get()).isEqualTo(1);
assertThat(psa.label()).isEqualTo("Code-Review");
assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
assertThat(psa.value()).isEqualTo(1);
assertThat(psa.realAccountId()).isEqualTo(realUser2.id());
assertLastChangeMessage(r.getChange(), in.message, impersonatedUser, realUser2);
}
@Test
public void overrideImpersonatedVoteWithOtherImpersonatedVote_differentValue() throws Exception {
allowCodeReviewOnBehalfOf();
TestAccount realUser = admin;
TestAccount realUser2 = admin2;
TestAccount impersonatedUser = user;
PushOneCommit.Result r = createChange();
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
// realUser votes Code-Review+1 on behalf of impersonatedUser
ReviewInput in = ReviewInput.recommend();
in.onBehalfOf = impersonatedUser.id().toString();
in.message = "Message on behalf of";
revision.review(in);
PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
assertThat(psa.patchSetId().get()).isEqualTo(1);
assertThat(psa.label()).isEqualTo("Code-Review");
assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
assertThat(psa.value()).isEqualTo(1);
assertThat(psa.realAccountId()).isEqualTo(realUser.id());
assertLastChangeMessage(r.getChange(), in.message, impersonatedUser, realUser);
// realUser2 votes Code-Review-1 on behalf of impersonatedUser, this should override the
// impersonated Code-Review+1 of realUser with an impersonated Code-Review-1 of realUser2
requestScopeOperations.setApiUser(realUser2.id());
in = ReviewInput.dislike();
in.onBehalfOf = impersonatedUser.id().toString();
in.message = "Another message on behalf of";
gApi.changes().id(r.getChangeId()).current().review(in);
psa = Iterables.getOnlyElement(r.getChange().approvals().values());
assertThat(psa.patchSetId().get()).isEqualTo(1);
assertThat(psa.label()).isEqualTo("Code-Review");
assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
assertThat(psa.value()).isEqualTo(-1);
assertThat(psa.realAccountId()).isEqualTo(realUser2.id());
assertLastChangeMessage(r.getChange(), in.message, impersonatedUser, realUser2);
}
@Test
public void overrideImpersonatedVoteWithNonImpersonatedVote_sameValue() throws Exception {
allowCodeReviewOnBehalfOf();
TestAccount realUser = admin;
TestAccount impersonatedUser = user;
PushOneCommit.Result r = createChange();
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
// realUser votes Code-Review+1 on behalf of impersonatedUser
ReviewInput in = ReviewInput.recommend();
in.onBehalfOf = impersonatedUser.id().toString();
in.message = "Message on behalf of";
revision.review(in);
PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
assertThat(psa.patchSetId().get()).isEqualTo(1);
assertThat(psa.label()).isEqualTo("Code-Review");
assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
assertThat(psa.value()).isEqualTo(1);
assertThat(psa.realAccountId()).isEqualTo(realUser.id());
assertLastChangeMessage(r.getChange(), in.message, impersonatedUser, realUser);
// impersonatedUser votes Code-Review+1 themselves, this should override the impersonated
// Code-Review+1 with a non-impersonated Code-Review+1
requestScopeOperations.setApiUser(impersonatedUser.id());
in = ReviewInput.recommend();
in.message = "Message";
gApi.changes().id(r.getChangeId()).current().review(in);
psa = Iterables.getOnlyElement(r.getChange().approvals().values());
assertThat(psa.patchSetId().get()).isEqualTo(1);
assertThat(psa.label()).isEqualTo("Code-Review");
assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
assertThat(psa.value()).isEqualTo(1);
assertThat(psa.realAccountId()).isEqualTo(impersonatedUser.id());
assertLastChangeMessage(r.getChange(), in.message, impersonatedUser, impersonatedUser);
}
@Test
public void overrideImpersonatedVoteWithNonImpersonatedVote_differentValue() throws Exception {
allowCodeReviewOnBehalfOf();
TestAccount realUser = admin;
TestAccount impersonatedUser = user;
PushOneCommit.Result r = createChange();
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
// realUser votes Code-Review+1 on behalf of impersonatedUser
ReviewInput in = ReviewInput.recommend();
in.onBehalfOf = impersonatedUser.id().toString();
in.message = "Message on behalf of";
revision.review(in);
PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
assertThat(psa.patchSetId().get()).isEqualTo(1);
assertThat(psa.label()).isEqualTo("Code-Review");
assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
assertThat(psa.value()).isEqualTo(1);
assertThat(psa.realAccountId()).isEqualTo(realUser.id());
assertLastChangeMessage(r.getChange(), in.message, impersonatedUser, realUser);
// impersonatedUser votes Code-Review-1 themselves, this should override the impersonated
// Code-Review+1 with a non-impersonated Code-Review-1
requestScopeOperations.setApiUser(impersonatedUser.id());
in = ReviewInput.dislike();
in.message = "Message";
gApi.changes().id(r.getChangeId()).current().review(in);
psa = Iterables.getOnlyElement(r.getChange().approvals().values());
assertThat(psa.patchSetId().get()).isEqualTo(1);
assertThat(psa.label()).isEqualTo("Code-Review");
assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
assertThat(psa.value()).isEqualTo(-1);
assertThat(psa.realAccountId()).isEqualTo(impersonatedUser.id());
assertLastChangeMessage(r.getChange(), in.message, impersonatedUser, impersonatedUser);
}
@Test
public void voteOnBehalfOfRequiresLabel() throws Exception {
allowCodeReviewOnBehalfOf();
PushOneCommit.Result r = createChange();
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
ReviewInput in = new ReviewInput();
in.onBehalfOf = user.id().toString();
in.message = "Message on behalf of";
AuthException thrown = assertThrows(AuthException.class, () -> revision.review(in));
assertThat(thrown)
.hasMessageThat()
.contains("label required to post review on behalf of \"" + in.onBehalfOf + '"');
}
@Test
@GerritConfig(name = "change.strictLabels", value = "true")
public void voteOnBehalfOfInvalidLabel() throws Exception {
allowCodeReviewOnBehalfOf();
String changeId = createChange().getChangeId();
ReviewInput in = new ReviewInput().label("Not-A-Label", 5);
in.onBehalfOf = user.id().toString();
BadRequestException thrown =
assertThrows(
BadRequestException.class, () -> gApi.changes().id(changeId).current().review(in));
assertThat(thrown).hasMessageThat().contains("label \"Not-A-Label\" is not a configured label");
}
@Test
public void voteOnBehalfOfInvalidLabelIgnoredWithoutStrictLabels() throws Exception {
allowCodeReviewOnBehalfOf();
String changeId = createChange().getChangeId();
ReviewInput in = new ReviewInput().label("Code-Review", 1).label("Not-A-Label", 5);
in.onBehalfOf = user.id().toString();
gApi.changes().id(changeId).current().review(in);
assertThat(gApi.changes().id(changeId).get().labels).doesNotContainKey("Not-A-Label");
}
@Test
public void voteOnBehalfOfLabelNotPermitted() throws Exception {
try (ProjectConfigUpdate u = updateProject(project)) {
LabelType verified = TestLabels.verified();
u.getConfig().upsertLabelType(verified);
u.save();
}
PushOneCommit.Result r = createChange();
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
ReviewInput in = new ReviewInput();
in.onBehalfOf = user.id().toString();
in.label(LabelId.VERIFIED, 1);
AuthException thrown = assertThrows(AuthException.class, () -> revision.review(in));
assertThat(thrown)
.hasMessageThat()
.contains(
"not permitted to modify label \"Verified\" on behalf of \"" + in.onBehalfOf + '"');
}
@Test
public void voteOnBehalfOfWithComment() throws Exception {
testVoteOnBehalfOfWithComment();
}
@Test
public void voteOnBehalfOfWithCommentWritingJson() throws Exception {
testVoteOnBehalfOfWithComment();
}
private void testVoteOnBehalfOfWithComment() throws Exception {
allowCodeReviewOnBehalfOf();
PushOneCommit.Result r = createChange();
ReviewInput in = new ReviewInput();
in.onBehalfOf = user.id().toString();
in.label("Code-Review", 1);
CommentInput ci = new CommentInput();
ci.path = Patch.COMMIT_MSG;
ci.side = Side.REVISION;
ci.line = 1;
ci.message = "message";
in.comments = ImmutableMap.of(ci.path, ImmutableList.of(ci));
gApi.changes().id(r.getChangeId()).current().review(in);
PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
assertThat(psa.patchSetId().get()).isEqualTo(1);
assertThat(psa.label()).isEqualTo("Code-Review");
assertThat(psa.accountId()).isEqualTo(user.id());
assertThat(psa.value()).isEqualTo(1);
assertThat(psa.realAccountId()).isEqualTo(admin.id());
ChangeData cd = r.getChange();
HumanComment c =
Iterables.getOnlyElement(commentsUtil.publishedHumanCommentsByChange(cd.notes()));
assertThat(c.message).isEqualTo(ci.message);
assertThat(c.author.getId()).isEqualTo(user.id());
assertThat(c.getRealAuthor().getId()).isEqualTo(admin.id());
}
@Test
public void voteOnBehalfOfWithRobotComment() throws Exception {
allowCodeReviewOnBehalfOf();
PushOneCommit.Result r = createChange();
ReviewInput in = new ReviewInput();
in.onBehalfOf = user.id().toString();
in.label("Code-Review", 1);
RobotCommentInput ci = new RobotCommentInput();
ci.robotId = "my-robot";
ci.robotRunId = "abcd1234";
ci.path = Patch.COMMIT_MSG;
ci.side = Side.REVISION;
ci.line = 1;
ci.message = "message";
in.robotComments = ImmutableMap.of(ci.path, ImmutableList.of(ci));
gApi.changes().id(r.getChangeId()).current().review(in);
ChangeData cd = r.getChange();
RobotComment c = Iterables.getOnlyElement(commentsUtil.robotCommentsByChange(cd.notes()));
assertThat(c.message).isEqualTo(ci.message);
assertThat(c.robotId).isEqualTo(ci.robotId);
assertThat(c.robotRunId).isEqualTo(ci.robotRunId);
assertThat(c.author.getId()).isEqualTo(user.id());
assertThat(c.getRealAuthor().getId()).isEqualTo(admin.id());
}
@Test
public void voteOnBehalfOfCannotModifyDrafts() throws Exception {
allowCodeReviewOnBehalfOf();
PushOneCommit.Result r = createChange();
requestScopeOperations.setApiUser(user.id());
DraftInput di = new DraftInput();
di.path = Patch.COMMIT_MSG;
di.side = Side.REVISION;
di.line = 1;
di.message = "message";
gApi.changes().id(r.getChangeId()).current().createDraft(di);
requestScopeOperations.setApiUser(admin.id());
ReviewInput in = new ReviewInput();
in.onBehalfOf = user.id().toString();
in.label("Code-Review", 1);
in.drafts = DraftHandling.PUBLISH;
AuthException thrown =
assertThrows(
AuthException.class, () -> gApi.changes().id(r.getChangeId()).current().review(in));
assertThat(thrown).hasMessageThat().contains("not allowed to modify other user's drafts");
}
@Test
public void voteOnBehalfOfMissingUser() throws Exception {
allowCodeReviewOnBehalfOf();
PushOneCommit.Result r = createChange();
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
ReviewInput in = new ReviewInput();
in.onBehalfOf = "doesnotexist";
in.label("Code-Review", 1);
UnprocessableEntityException thrown =
assertThrows(UnprocessableEntityException.class, () -> revision.review(in));
assertThat(thrown).hasMessageThat().contains("not found");
assertThat(thrown).hasMessageThat().contains("doesnotexist");
}
@Test
public void voteOnBehalfOfFailsWhenUserCannotSeeDestinationRef() throws Exception {
blockRead(newGroup);
allowCodeReviewOnBehalfOf();
PushOneCommit.Result r = createChange();
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
ReviewInput in = new ReviewInput();
in.onBehalfOf = user.id().toString();
in.label("Code-Review", 1);
ResourceConflictException thrown =
assertThrows(ResourceConflictException.class, () -> revision.review(in));
assertThat(thrown)
.hasMessageThat()
.contains("on_behalf_of account " + user.id() + " cannot see change");
}
@GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
@Test
public void voteOnBehalfOfInvisibleUserNotAllowed() throws Exception {
allowCodeReviewOnBehalfOf();
requestScopeOperations.setApiUser(accountCreator.user2().id());
assertThat(accountControlFactory.get().canSee(user.id())).isFalse();
PushOneCommit.Result r = createChange();
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
ReviewInput in = new ReviewInput();
in.onBehalfOf = user.id().toString();
in.label("Code-Review", 1);
UnprocessableEntityException thrown =
assertThrows(UnprocessableEntityException.class, () -> revision.review(in));
assertThat(thrown).hasMessageThat().contains("not found");
assertThat(thrown).hasMessageThat().contains(in.onBehalfOf);
}
@Test
@UseLocalDisk
public void submitOnBehalfOf_mergeAlways() throws Exception {
TestAccount realUser = admin;
TestAccount impersonatedUser = admin2;
// Create a project with MERGE_ALWAYS submit strategy so that a merge commit is created on
// submit and we can verify its committer and author and the ref log for the update of the
// target branch.
Project.NameKey project =
projectOperations.newProject().submitType(SubmitType.MERGE_ALWAYS).create();
testSubmitOnBehalfOf(project, realUser, impersonatedUser);
// The merge commit is created by the server and has the impersonated user as the author.
RevCommit mergeCommit = projectOperations.project(project).getHead("refs/heads/master");
assertThat(mergeCommit.getCommitterIdent().getEmailAddress())
.isEqualTo(serverIdent.get().getEmailAddress());
assertThat(mergeCommit.getAuthorIdent().getEmailAddress()).isEqualTo(impersonatedUser.email());
// The ref log for the target branch records the impersonated user.
try (Repository repo = repoManager.openRepository(project)) {
ReflogEntry targetBranchRefLogEntry =
repo.getReflogReader("refs/heads/master").getLastEntry();
assertThat(targetBranchRefLogEntry.getWho().getEmailAddress())
.isEqualTo(impersonatedUser.email());
}
}
@Test
@UseLocalDisk
public void submitOnBehalfOf_rebaseAlways() throws Exception {
TestAccount originalAuthor = admin; // user that creates and authors the change that is rebased
TestAccount realUser = admin2;
TestAccount impersonatedUser = user;
// Create a project with REBASE_ALWAYS submit strategy so that a new patch set is created on
// submit and we can verify its committer and author and the ref log for the update of the
// patch set ref and the target branch.
Project.NameKey project =
projectOperations.newProject().submitType(SubmitType.REBASE_ALWAYS).create();
ChangeData cd = testSubmitOnBehalfOf(project, realUser, impersonatedUser);
// Rebase on submit is expected to create a new patch set.
assertThat(cd.currentPatchSet().id().get()).isEqualTo(2);
// The patch set commit is created by the impersonated user and has the author of the rebased
// commit as the author.
RevCommit newPatchSetCommit =
projectOperations.project(project).getHead(cd.currentPatchSet().refName());
assertThat(newPatchSetCommit.getCommitterIdent().getEmailAddress())
.isEqualTo(impersonatedUser.email());
assertThat(newPatchSetCommit.getAuthorIdent().getEmailAddress())
.isEqualTo(originalAuthor.email());
try (Repository repo = repoManager.openRepository(project)) {
// The ref log for the patch set ref records the impersonated user.
ReflogEntry patchSetRefLogEntry =
repo.getReflogReader(cd.currentPatchSet().refName()).getLastEntry();
assertThat(patchSetRefLogEntry.getWho().getEmailAddress())
.isEqualTo(impersonatedUser.email());
// The ref log for the target branch records the impersonated user.
ReflogEntry targetBranchRefLogEntry =
repo.getReflogReader("refs/heads/master").getLastEntry();
assertThat(targetBranchRefLogEntry.getWho().getEmailAddress())
.isEqualTo(impersonatedUser.email());
}
}
@CanIgnoreReturnValue
private ChangeData testSubmitOnBehalfOf(
Project.NameKey project, TestAccount realUser, TestAccount impersonatedUser)
throws Exception {
allowSubmitOnBehalfOf(project);
TestRepository<InMemoryRepository> testRepo = cloneProject(project, realUser);
PushOneCommit.Result r = createChange(testRepo);
String changeId = project.get() + "~master~" + r.getChangeId();
gApi.changes().id(changeId).current().review(ReviewInput.approve());
SubmitInput in = new SubmitInput();
in.onBehalfOf = impersonatedUser.email();
try (Repository repo = repoManager.openRepository(project)) {
String changeMetaRef = changeMetaRef(r.getChange().getId());
createRefLogFileIfMissing(repo, changeMetaRef);
createRefLogFileIfMissing(repo, "refs/heads/master");
createRefLogFileIfMissing(repo, patchSetRef(PatchSet.id(r.getChange().getId(), 2)));
requestScopeOperations.setApiUser(realUser.id());
gApi.changes().id(changeId).current().submit(in);
ChangeData cd = r.getChange();
assertThat(cd.change().isMerged()).isTrue();
PatchSetApproval submitter =
approvalsUtil.getSubmitter(cd.notes(), cd.change().currentPatchSetId());
assertThat(submitter.accountId()).isEqualTo(impersonatedUser.id());
assertThat(submitter.realAccountId()).isEqualTo(realUser.id());
// The change meta commit is created by the server and has the impersonated user as the
// author.
// Person idents of users in NoteDb commits are obfuscated due to privacy reasons.
RevCommit changeMetaCommit = projectOperations.project(project).getHead(changeMetaRef);
assertThat(changeMetaCommit.getCommitterIdent().getEmailAddress())
.isEqualTo(serverIdent.get().getEmailAddress());
assertThat(changeMetaCommit.getAuthorIdent().getEmailAddress())
.isEqualTo(changeNoteUtil.getAccountIdAsEmailAddress(impersonatedUser.id()));
// The ref log for the change meta ref records the impersonated user.
ReflogEntry changeMetaRefLogEntry = repo.getReflogReader(changeMetaRef).getLastEntry();
assertThat(changeMetaRefLogEntry.getWho().getEmailAddress())
.isEqualTo(impersonatedUser.email());
return cd;
}
}
@Test
public void submitOnBehalfOfInvalidUser() throws Exception {
allowSubmitOnBehalfOf();
PushOneCommit.Result r = createChange();
String changeId = project.get() + "~master~" + r.getChangeId();
gApi.changes().id(changeId).current().review(ReviewInput.approve());
SubmitInput in = new SubmitInput();
in.onBehalfOf = "doesnotexist";
UnprocessableEntityException thrown =
assertThrows(
UnprocessableEntityException.class,
() -> gApi.changes().id(changeId).current().submit(in));
assertThat(thrown).hasMessageThat().contains("not found");
assertThat(thrown).hasMessageThat().contains("doesnotexist");
}
@Test
public void submitOnBehalfOfNotPermitted() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes()
.id(project.get() + "~master~" + r.getChangeId())
.current()
.review(ReviewInput.approve());
SubmitInput in = new SubmitInput();
in.onBehalfOf = admin2.email();
AuthException thrown =
assertThrows(
AuthException.class,
() ->
gApi.changes()
.id(project.get() + "~master~" + r.getChangeId())
.current()
.submit(in));
assertThat(thrown).hasMessageThat().contains("submit on behalf of other users not permitted");
}
@Test
public void submitOnBehalfOfFailsWhenUserCannotSeeDestinationRef() throws Exception {
blockRead(newGroup);
allowSubmitOnBehalfOf();
PushOneCommit.Result r = createChange();
String changeId = project.get() + "~master~" + r.getChangeId();
gApi.changes().id(changeId).current().review(ReviewInput.approve());
SubmitInput in = new SubmitInput();
in.onBehalfOf = user.email();
UnprocessableEntityException thrown =
assertThrows(
UnprocessableEntityException.class,
() -> gApi.changes().id(changeId).current().submit(in));
assertThat(thrown)
.hasMessageThat()
.contains("on_behalf_of account " + user.id() + " cannot see change");
}
@GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
@Test
public void submitOnBehalfOfInvisibleUserNotAllowed() throws Exception {
allowSubmitOnBehalfOf();
requestScopeOperations.setApiUser(accountCreator.user2().id());
assertThat(accountControlFactory.get().canSee(user.id())).isFalse();
PushOneCommit.Result r = createChange();
String changeId = project.get() + "~master~" + r.getChangeId();
gApi.changes().id(changeId).current().review(ReviewInput.approve());
SubmitInput in = new SubmitInput();
in.onBehalfOf = user.email();
UnprocessableEntityException thrown =
assertThrows(
UnprocessableEntityException.class,
() -> gApi.changes().id(changeId).current().submit(in));
assertThat(thrown).hasMessageThat().contains("not found");
assertThat(thrown).hasMessageThat().contains(in.onBehalfOf);
}
@Test
public void runAsValidUser() throws Exception {
allowRunAs();
RestResponse res = adminRestSession.getWithHeaders("/accounts/self", runAsHeader(user.id()));
res.assertOK();
AccountInfo account = newGson().fromJson(res.getEntityContent(), AccountInfo.class);
assertThat(account._accountId).isEqualTo(user.id().get());
}
@GerritConfig(name = "auth.enableRunAs", value = "false")
@Test
public void runAsDisabledByConfig() throws Exception {
allowRunAs();
RestResponse res = adminRestSession.getWithHeaders("/changes/", runAsHeader(user.id()));
res.assertForbidden();
assertThat(res.getEntityContent())
.isEqualTo("X-Gerrit-RunAs disabled by auth.enableRunAs = false");
}
@Test
public void runAsNotPermitted() throws Exception {
RestResponse res = adminRestSession.getWithHeaders("/changes/", runAsHeader(user.id()));
res.assertForbidden();
assertThat(res.getEntityContent()).isEqualTo("not permitted to use X-Gerrit-RunAs");
}
@Test
public void runAsNeverPermittedForAnonymousUsers() throws Exception {
allowRunAs();
RestResponse res = anonymousRestSession.getWithHeaders("/changes/", runAsHeader(user.id()));
res.assertForbidden();
assertThat(res.getEntityContent()).isEqualTo("not permitted to use X-Gerrit-RunAs");
}
@Test
public void runAsInvalidUser() throws Exception {
allowRunAs();
RestResponse res = adminRestSession.getWithHeaders("/changes/", runAsHeader("doesnotexist"));
res.assertForbidden();
assertThat(res.getEntityContent()).isEqualTo("no account matches X-Gerrit-RunAs");
}
@Test
public void voteUsingRunAsAvoidsRestrictionsOfOnBehalfOf() throws Exception {
allowRunAs();
PushOneCommit.Result r = createChange();
requestScopeOperations.setApiUser(user.id());
DraftInput di = new DraftInput();
di.path = Patch.COMMIT_MSG;
di.side = Side.REVISION;
di.line = 1;
di.message = "inline comment";
gApi.changes().id(r.getChangeId()).current().createDraft(di);
requestScopeOperations.setApiUser(admin.id());
// Things that aren't allowed with on_behalf_of:
// - no labels.
// - publish other user's drafts.
ReviewInput in = new ReviewInput();
in.message = "message";
in.drafts = DraftHandling.PUBLISH;
RestResponse res =
adminRestSession.postWithHeaders(
"/changes/" + r.getChangeId() + "/revisions/current/review",
in,
runAsHeader(user.id()));
res.assertOK();
ChangeMessageInfo m = Iterables.getLast(gApi.changes().id(r.getChangeId()).get().messages);
assertThat(m.message).endsWith(in.message);
assertThat(m.author._accountId).isEqualTo(user.id().get());
CommentInfo c =
Iterables.getOnlyElement(
gApi.changes().id(r.getChangeId()).commentsRequest().get().get(di.path));
assertThat(c.author._accountId).isEqualTo(user.id().get());
assertThat(c.message).isEqualTo(di.message);
requestScopeOperations.setApiUser(user.id());
assertThat(gApi.changes().id(r.getChangeId()).drafts()).isEmpty();
}
@Test
public void runAsWithOnBehalfOf() throws Exception {
// - Has the same restrictions as on_behalf_of (e.g. requires labels).
// - Takes the effective user from on_behalf_of (user).
// - Takes the real user from the real caller, not the intermediate
// X-Gerrit-RunAs user (user2).
allowRunAs();
allowCodeReviewOnBehalfOf();
TestAccount user2 = accountCreator.user2();
PushOneCommit.Result r = createChange();
ReviewInput in = new ReviewInput();
in.onBehalfOf = user.id().toString();
in.message = "Message on behalf of";
String endpoint = "/changes/" + r.getChangeId() + "/revisions/current/review";
RestResponse res = adminRestSession.postWithHeaders(endpoint, in, runAsHeader(user2.id()));
res.assertForbidden();
assertThat(res.getEntityContent())
.isEqualTo("label required to post review on behalf of \"" + in.onBehalfOf + '"');
in.label("Code-Review", 1);
adminRestSession.postWithHeaders(endpoint, in, runAsHeader(user2.id())).assertOK();
PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
assertThat(psa.patchSetId().get()).isEqualTo(1);
assertThat(psa.label()).isEqualTo("Code-Review");
assertThat(psa.accountId()).isEqualTo(user.id());
assertThat(psa.value()).isEqualTo(1);
assertThat(psa.realAccountId()).isEqualTo(admin.id()); // not user2
assertLastChangeMessage(r.getChange(), in.message, user, admin);
}
@Test
public void changeMessageCreatedOnBehalfOfHasRealUser() throws Exception {
allowCodeReviewOnBehalfOf();
PushOneCommit.Result r = createChange();
ReviewInput in = new ReviewInput();
in.onBehalfOf = user.id().toString();
in.message = "Message on behalf of";
in.label("Code-Review", 1);
requestScopeOperations.setApiUser(accountCreator.user2().id());
gApi.changes().id(r.getChangeId()).revision(r.getPatchSetId().getId()).review(in);
ChangeInfo info = gApi.changes().id(r.getChangeId()).get(MESSAGES);
assertThat(info.messages).hasSize(2);
assertLastChangeMessage(r.getChange(), in.message, user, accountCreator.user2());
}
private void assertLastChangeMessage(
ChangeData changeData,
String expectedMessage,
TestAccount expectedAuthor,
TestAccount expectedRealAuthor)
throws RestApiException {
ChangeMessage m = Iterables.getLast(cmUtil.byChange(changeData.notes()));
assertThat(m.getMessage()).endsWith(expectedMessage);
assertThat(m.getAuthor()).isEqualTo(expectedAuthor.id());
assertThat(m.getRealAuthor()).isEqualTo(expectedRealAuthor.id());
ChangeMessageInfo lastChangeMessageInfo =
Iterables.getLast(gApi.changes().id(changeData.getId().get()).get().messages);
assertThat(lastChangeMessageInfo.message).endsWith(expectedMessage);
assertThat(lastChangeMessageInfo.author._accountId).isEqualTo(expectedAuthor.id().get());
if (expectedAuthor.id().equals(expectedRealAuthor.id())) {
assertThat(lastChangeMessageInfo.realAuthor).isNull();
} else {
assertThat(lastChangeMessageInfo.realAuthor._accountId)
.isEqualTo(expectedRealAuthor.id().get());
}
}
private void allowCodeReviewOnBehalfOf() throws Exception {
projectOperations
.project(project)
.forUpdate()
.add(
allowLabel(TestLabels.codeReview().getName())
.impersonation(true)
.ref("refs/heads/*")
.group(REGISTERED_USERS)
.range(-1, 1))
.update();
}
private void allowSubmitOnBehalfOf() throws Exception {
allowSubmitOnBehalfOf(project);
}
private void allowSubmitOnBehalfOf(Project.NameKey project) throws Exception {
String heads = "refs/heads/*";
projectOperations
.project(project)
.forUpdate()
.add(allow(Permission.SUBMIT_AS).ref(heads).group(REGISTERED_USERS))
.add(allow(Permission.SUBMIT).ref(heads).group(REGISTERED_USERS))
.add(
allowLabel(TestLabels.codeReview().getName())
.ref(heads)
.group(REGISTERED_USERS)
.range(-2, 2))
.update();
}
private void blockRead(GroupInfo group) throws Exception {
projectOperations
.project(project)
.forUpdate()
.add(block(Permission.READ).ref("refs/heads/master").group(AccountGroup.uuid(group.id)))
.update();
}
private void allowRunAs() throws Exception {
projectOperations
.allProjectsForUpdate()
.add(allowCapability(GlobalCapability.RUN_AS).group(ANONYMOUS_USERS))
.update();
}
private void removeRunAs() throws Exception {
projectOperations
.allProjectsForUpdate()
.remove(capabilityKey(GlobalCapability.RUN_AS).group(ANONYMOUS_USERS))
.update();
}
private static Header runAsHeader(Object user) {
return new BasicHeader("X-Gerrit-RunAs", user.toString());
}
}