Ensure users are active on the change when adding to attention set

Until now, it was technically possible to add users to the attention set
even if they had no interactions on the change. This is a minor bug
since the frontend took care of this even if such a thing happened, and
also it required users to manually do it using the REST API.

This change ensures users can't be added to the attention set if they
are not active (reviewer, cc, owner, uploader) on the change.

On reply, we don't throw an error when trying to add an inactive user
(but rather just ignore it in ChangeUpdate). The reason for this is the
fact it's complicated to calculate the current reviewers since on reply
we can also add new reviewers (which would require "resolving" all
new reviewers unnecessarily, which is a latency concern, In
ChangeUpdate it's done anyway so we can check it there).

Tests that allowed adding to the attention set when the user was not
active in the change had to be updated.

Change-Id: Icd779ce3b4c766a1a941680cde5d2b87d055b553
This commit is contained in:
Gal Paikin
2020-10-29 12:19:11 +01:00
parent 0f65e01a3b
commit e631de23ea
6 changed files with 170 additions and 37 deletions

View File

@@ -5935,6 +5935,8 @@ A user can only be added if they are not in the attention set.
If a user is added while already in the attention set, the
request is silently ignored.
The user must be a reviewer, cc, uploader, or owner on the change.
.Request
----
POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/attention HTTP/1.0
@@ -7662,7 +7664,8 @@ If true, mark the change as work in progress. It is an error for both
`ready` and `work_in_progress` to be true.
|`add_to_attention_set` |optional|
list of link:#attention-set-input[AttentionSetInput] entities to add
to the link:#attention-set[attention set].
to the link:#attention-set[attention set]. Users that are not reviewers,
ccs, owner, or uploader are silently ignored.
|`remove_from_attention_set` |optional|
list of link:#attention-set-input[AttentionSetInput] entities to remove
from the link:#attention-set[attention set].

View File

@@ -65,6 +65,7 @@ import com.google.gerrit.entities.RobotComment;
import com.google.gerrit.entities.SubmissionId;
import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.ServiceUserClassifier;
@@ -87,6 +88,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.ObjectId;
@@ -821,6 +823,22 @@ public class ChangeUpdate extends AbstractChangeUpdate {
AttentionSetUtil.additionsOnly(getNotes().getAttentionSet()).stream()
.map(AttentionSetUpdate::account)
.collect(Collectors.toSet());
// Current reviewers/ccs are the reviewers/ccs before the update + the new reviewers/ccs - the
// deleted reviewers/ccs.
Set<Account.Id> currentReviewers =
Stream.concat(
getNotes().getReviewers().all().stream(),
reviewers.entrySet().stream()
.filter(r -> r.getValue().asReviewerState() != ReviewerState.REMOVED)
.map(r -> r.getKey()))
.collect(Collectors.toSet());
currentReviewers.removeAll(
reviewers.entrySet().stream()
.filter(r -> r.getValue().asReviewerState() == ReviewerState.REMOVED)
.map(r -> r.getKey())
.collect(ImmutableSet.toImmutableSet()));
for (AttentionSetUpdate attentionSetUpdate : plannedAttentionSetUpdates.values()) {
if (attentionSetUpdate.operation() == AttentionSetUpdate.Operation.ADD
&& currentUsersInAttentionSet.contains(attentionSetUpdate.account())) {
@@ -848,10 +866,26 @@ public class ChangeUpdate extends AbstractChangeUpdate {
continue;
}
// Don't add accounts that are not active in the change to the attention set.
if (attentionSetUpdate.operation() == AttentionSetUpdate.Operation.ADD
&& !isActiveOnChange(currentReviewers, attentionSetUpdate.account())) {
continue;
}
addFooter(msg, FOOTER_ATTENTION, noteUtil.attentionSetUpdateToJson(attentionSetUpdate));
}
}
/**
* Returns whether {@code accountId} is active on a change based on the {@code currentReviewers}.
* Activity is defined as being a part of the reviewers, an uploader, or an owner of a change.
*/
private boolean isActiveOnChange(Set<Account.Id> currentReviewers, Account.Id accountId) {
return currentReviewers.contains(accountId)
|| getChange().getOwner().equals(accountId)
|| getNotes().getCurrentPatchSet().uploader().equals(accountId);
}
/**
* When set, default attention set rules are ignored (E.g, adding reviewers -> adds to attention
* set, etc).

View File

@@ -29,6 +29,7 @@ import com.google.gerrit.server.change.AddToAttentionSetOp;
import com.google.gerrit.server.change.AttentionSetEntryResource;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.update.BatchUpdate;
@@ -72,8 +73,15 @@ public class AddToAttentionSet
public Response<AccountInfo> apply(ChangeResource changeResource, AttentionSetInput input)
throws Exception {
AttentionSetUtil.validateInput(input);
Account.Id attentionUserId = accountResolver.resolve(input.user).asUnique().account().id();
if (!isActiveOnTheChange(changeResource.getNotes(), attentionUserId)) {
throw new BadRequestException(
String.format(
"%s is not active on the change as an owner, uploader, reviewer, or "
+ "cc so they can't be added to the attention set",
input.user));
}
if (serviceUserClassifier.isServiceUser(attentionUserId)) {
throw new BadRequestException(
String.format(
@@ -100,4 +108,14 @@ public class AddToAttentionSet
return Response.ok(accountLoaderFactory.create(true).fillOne(attentionUserId));
}
}
/**
* Returns whether {@code accountId} is active on a change. Activity is defined as being a part of
* the reviewers, an uploader, or an owner of a change.
*/
private boolean isActiveOnTheChange(ChangeNotes changeNotes, Account.Id attentionUserId) {
return changeNotes.getChange().getOwner().equals(attentionUserId)
|| changeNotes.getCurrentPatchSet().uploader().equals(attentionUserId)
|| changeNotes.getReviewers().all().stream().anyMatch(id -> id.equals(attentionUserId));
}
}

View File

@@ -35,6 +35,7 @@ import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.AttentionSetUpdate.Operation;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
@@ -115,48 +116,48 @@ public class AttentionSetIT extends AbstractDaemonTest {
PushOneCommit.Result r = createChange();
requestScopeOperations.setApiUser(user.id());
int accountId =
change(r).addToAttentionSet(new AttentionSetInput(user.email(), "first"))._accountId;
assertThat(accountId).isEqualTo(user.id().get());
change(r).addToAttentionSet(new AttentionSetInput(admin.email(), "first"))._accountId;
assertThat(accountId).isEqualTo(admin.id().get());
AttentionSetUpdate expectedAttentionSetUpdate =
AttentionSetUpdate.createFromRead(
fakeClock.now(), user.id(), AttentionSetUpdate.Operation.ADD, "first");
fakeClock.now(), admin.id(), AttentionSetUpdate.Operation.ADD, "first");
assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate);
// Second add is ignored.
accountId =
change(r).addToAttentionSet(new AttentionSetInput(user.email(), "second"))._accountId;
assertThat(accountId).isEqualTo(user.id().get());
change(r).addToAttentionSet(new AttentionSetInput(admin.email(), "second"))._accountId;
assertThat(accountId).isEqualTo(admin.id().get());
assertThat(r.getChange().attentionSet()).containsExactly(expectedAttentionSetUpdate);
// Only one email since the second add was ignored.
String emailBody = Iterables.getOnlyElement(email.getMessages()).body();
assertThat(emailBody)
.contains(
user.fullName()
+ " added themselves to the attention set of this change.\n The reason is: first.");
String.format(
"%s requires the attention of %s to this change.\n The reason is: first.",
user.fullName(), admin.fullName()));
}
@Test
public void addMultipleUsers() throws Exception {
PushOneCommit.Result r = createChange();
Instant timestamp1 = fakeClock.now();
int accountId1 =
change(r).addToAttentionSet(new AttentionSetInput(user.email(), "user"))._accountId;
assertThat(accountId1).isEqualTo(user.id().get());
// implictly adds the user to the attention set when adding as reviewer
change(r).addReviewer(user.email());
fakeClock.advance(Duration.ofSeconds(42));
Instant timestamp2 = fakeClock.now();
int accountId2 =
change(r)
.addToAttentionSet(new AttentionSetInput(admin.id().toString(), "admin"))
.addToAttentionSet(new AttentionSetInput(admin.id().toString(), "manual update"))
._accountId;
assertThat(accountId2).isEqualTo(admin.id().get());
AttentionSetUpdate expectedAttentionSetUpdate1 =
AttentionSetUpdate.createFromRead(
timestamp1, user.id(), AttentionSetUpdate.Operation.ADD, "user");
timestamp1, user.id(), AttentionSetUpdate.Operation.ADD, "Reviewer was added");
AttentionSetUpdate expectedAttentionSetUpdate2 =
AttentionSetUpdate.createFromRead(
timestamp2, admin.id(), AttentionSetUpdate.Operation.ADD, "admin");
timestamp2, admin.id(), AttentionSetUpdate.Operation.ADD, "manual update");
assertThat(r.getChange().attentionSet())
.containsExactly(expectedAttentionSetUpdate1, expectedAttentionSetUpdate2);
}
@@ -164,7 +165,9 @@ public class AttentionSetIT extends AbstractDaemonTest {
@Test
public void removeUser() throws Exception {
PushOneCommit.Result r = createChange();
change(r).addToAttentionSet(new AttentionSetInput(user.email(), "added"));
// implictly adds the user to the attention set when adding as reviewer
change(r).addReviewer(user.email());
sender.clear();
requestScopeOperations.setApiUser(user.id());
fakeClock.advance(Duration.ofSeconds(42));
@@ -223,7 +226,8 @@ public class AttentionSetIT extends AbstractDaemonTest {
@Test
public void abandonRemovesUsers() throws Exception {
PushOneCommit.Result r = createChange();
change(r).addToAttentionSet(new AttentionSetInput(user.email(), "user"));
// implictly adds the user to the attention set when adding as reviewer
change(r).addReviewer(user.email());
change(r).addToAttentionSet(new AttentionSetInput(admin.email(), "admin"));
change(r).abandon();
@@ -244,7 +248,8 @@ public class AttentionSetIT extends AbstractDaemonTest {
@Test
public void workInProgressRemovesUsers() throws Exception {
PushOneCommit.Result r = createChange();
change(r).addToAttentionSet(new AttentionSetInput(user.email(), "reason"));
// implictly adds the user to the attention set when adding as reviewer
change(r).addReviewer(user.email());
change(r).setWorkInProgress();
@@ -258,13 +263,10 @@ public class AttentionSetIT extends AbstractDaemonTest {
public void submitRemovesUsersForAllSubmittedChanges() throws Exception {
PushOneCommit.Result r1 = createChange("refs/heads/master", "file1", "content");
change(r1)
.current()
.review(ReviewInput.approve().addUserToAttentionSet(user.email(), "reason"));
// implictly adds the user to the attention set when adding as reviewer
change(r1).current().review(ReviewInput.approve().reviewer(user.email()));
PushOneCommit.Result r2 = createChange("refs/heads/master", "file2", "content");
change(r2)
.current()
.review(ReviewInput.approve().addUserToAttentionSet(user.email(), "reason"));
change(r2).current().review(ReviewInput.approve().reviewer(user.email()));
change(r2).current().submit();
@@ -286,11 +288,10 @@ public class AttentionSetIT extends AbstractDaemonTest {
@Test
public void robotSubmitsRemovesUsers() throws Exception {
PushOneCommit.Result r1 = createChange("refs/heads/master", "file1", "content");
PushOneCommit.Result r = createChange("refs/heads/master", "file1", "content");
change(r1)
.current()
.review(ReviewInput.approve().addUserToAttentionSet(user.email(), "reason"));
// implictly adds the user to the attention set when adding as reviewer
change(r).addReviewer(user.email());
TestAccount robot =
accountCreator.create(
@@ -301,11 +302,12 @@ public class AttentionSetIT extends AbstractDaemonTest {
ServiceUserClassifier.SERVICE_USERS,
"Administrators");
requestScopeOperations.setApiUser(robot.id());
change(r1).current().submit();
change(r).current().review(ReviewInput.approve());
change(r).current().submit();
// Attention set updates that relate to the admin (the person who replied) are filtered out.
AttentionSetUpdate attentionSet =
Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r1, user));
Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user));
assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id());
assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.REMOVE);
@@ -533,7 +535,9 @@ public class AttentionSetIT extends AbstractDaemonTest {
@Test
public void reviewersAreNotAddedForNoReasonBecauseOfAnUpdate() throws Exception {
PushOneCommit.Result r = createChange();
change(r).addToAttentionSet(new AttentionSetInput(user.email(), "user"));
// implictly adds the user to the attention set when adding as reviewer
change(r).addReviewer(user.email());
change(r).attention(user.id().toString()).remove(new AttentionSetInput("removed"));
HashtagsInput hashtagsInput = new HashtagsInput();
@@ -567,7 +571,8 @@ public class AttentionSetIT extends AbstractDaemonTest {
@Test
public void reviewRemovesManuallyRemovedUserFromAttentionSet() throws Exception {
PushOneCommit.Result r = createChange();
change(r).addToAttentionSet(new AttentionSetInput(user.email(), "reason"));
// implictly adds the user to the attention set when adding as reviewer
change(r).addReviewer(user.email());
requestScopeOperations.setApiUser(user.id());
ReviewInput reviewInput =
@@ -587,7 +592,8 @@ public class AttentionSetIT extends AbstractDaemonTest {
@Test
public void reviewWithManualAdditionToAttentionSetFailsWithoutReason() throws Exception {
PushOneCommit.Result r = createChange();
change(r).addToAttentionSet(new AttentionSetInput(user.email(), "reason"));
// implictly adds the user to the attention set when adding as reviewer
change(r).addReviewer(user.email());
ReviewInput reviewInput = ReviewInput.create().addUserToAttentionSet(user.email(), "");
@@ -611,7 +617,8 @@ public class AttentionSetIT extends AbstractDaemonTest {
@Test
public void reviewAddReviewerWhileRemovingFromAttentionSetJustRemovesUser() throws Exception {
PushOneCommit.Result r = createChange();
change(r).addToAttentionSet(new AttentionSetInput(user.email(), "addition"));
// implictly adds the user to the attention set when adding as reviewer
change(r).addReviewer(user.email());
ReviewInput reviewInput =
ReviewInput.create()
@@ -772,7 +779,15 @@ public class AttentionSetIT extends AbstractDaemonTest {
requestScopeOperations.setApiUser(user.id());
change(r).addToAttentionSet(new AttentionSetInput(user.email(), "reason"));
// add the user to the attention set.
change(r)
.current()
.review(
ReviewInput.create()
.reviewer(user.email(), ReviewerState.CC, true)
.addUserToAttentionSet(user.email(), "reason"));
// add the user as reviewer but still be removed on reply.
ReviewInput reviewInput = ReviewInput.create().reviewer(user.email());
change(r).current().review(reviewInput);
@@ -1144,6 +1159,9 @@ public class AttentionSetIT extends AbstractDaemonTest {
@Test
public void repliesWhileAddingAsReviewerStillRemovesUser() throws Exception {
PushOneCommit.Result r = createChange();
// implictly adds the user to the attention set when adding as reviewer
change(r).addReviewer(user.email());
change(r).addToAttentionSet(new AttentionSetInput(user.email(), "remove"));
requestScopeOperations.setApiUser(user.id());
@@ -1237,6 +1255,8 @@ public class AttentionSetIT extends AbstractDaemonTest {
accountCreator.create(
"robot1", "robot1@example.com", "Ro Bot", "Ro", ServiceUserClassifier.SERVICE_USERS);
PushOneCommit.Result r = createChange();
// Make the robot active on the change.
change(r).addReviewer(robot.email());
// Throw an error when adding a robot explicitly.
BadRequestException exception =
@@ -1335,13 +1355,15 @@ public class AttentionSetIT extends AbstractDaemonTest {
public void addUsersToAttentionSetInPrivateChanges() throws Exception {
PushOneCommit.Result r = createChange();
change(r).setPrivate(true);
change(r).current().review(new ReviewInput().addUserToAttentionSet(user.email(), "reason"));
// implictly adds the user to the attention set when adding as reviewer
change(r).addReviewer(user.email());
AttentionSetUpdate attentionSet =
Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user));
assertThat(attentionSet).hasAccountIdThat().isEqualTo(user.id());
assertThat(attentionSet).hasOperationThat().isEqualTo(AttentionSetUpdate.Operation.ADD);
assertThat(attentionSet).hasReasonThat().isEqualTo("reason");
assertThat(attentionSet).hasReasonThat().isEqualTo("Reviewer was added");
}
@Test
@@ -1532,6 +1554,52 @@ public class AttentionSetIT extends AbstractDaemonTest {
assertThat(sender.getMessages()).isNotEmpty();
}
@Test
public void cannotAddIrrelevantUserToAttentionSet() throws Exception {
PushOneCommit.Result r = createChange();
BadRequestException exception =
assertThrows(
BadRequestException.class,
() -> change(r).addToAttentionSet(new AttentionSetInput(user.email(), "reason")));
assertThat(exception.getMessage())
.isEqualTo(
String.format(
"%s is not active on the change as an owner, uploader, reviewer, or cc so they "
+ "can't be added to the attention set",
user.email()));
}
@Test
public void irrelevantUsersAddedToAttentionSetAreIgnoredOnReply() throws Exception {
PushOneCommit.Result r = createChange();
change(r).current().review(ReviewInput.create().addUserToAttentionSet(user.email(), "reason"));
assertThat(getAttentionSetUpdatesForUser(r, user)).isEmpty();
}
@Test
public void newReviewerCanBeAddedToTheAttentionSetManually() throws Exception {
PushOneCommit.Result r = createChange();
change(r)
.current()
.review(
ReviewInput.create()
.reviewer(user.email())
.addUserToAttentionSet(user.email(), "reason")
.blockAutomaticAttentionSetRules());
assertThat(Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)).operation())
.isEqualTo(Operation.ADD);
}
@Test
public void newReviewerCanBeAddedToTheAttentionSetAutomatically() throws Exception {
PushOneCommit.Result r = createChange();
change(r).current().review(ReviewInput.create().reviewer(user.email()));
assertThat(Iterables.getOnlyElement(getAttentionSetUpdatesForUser(r, user)).operation())
.isEqualTo(Operation.ADD);
}
private List<AttentionSetUpdate> getAttentionSetUpdatesForUser(
PushOneCommit.Result r, TestAccount account) {
return getAttentionSetUpdates(r.getChange().getId()).stream()

View File

@@ -809,6 +809,8 @@ public class ChangeNotesTest extends AbstractChangeNotesTest {
public void addAttentionStatusForMultipleUsers() throws Exception {
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
// put the user as cc to ensure that the user took part in this change.
update.putReviewer(otherUser.getAccount().id(), CC);
AttentionSetUpdate attentionSetUpdate0 =
AttentionSetUpdate.createForWrite(changeOwner.getAccountId(), Operation.ADD, "test");
AttentionSetUpdate attentionSetUpdate1 =

View File

@@ -3055,6 +3055,14 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
gApi.changes().id(change.getChangeId()).addToAttentionSet(input);
Account.Id user2Id =
accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
// Add the second user as cc to ensure that user took part of the change and can be added to the
// attention set.
AddReviewerInput addReviewerInput = new AddReviewerInput();
addReviewerInput.reviewer = user2Id.toString();
addReviewerInput.state = ReviewerState.CC;
gApi.changes().id(change.getChangeId()).addReviewer(addReviewerInput);
input = new AttentionSetInput(user2Id.toString(), "reason 2");
gApi.changes().id(change.getChangeId()).addToAttentionSet(input);