LCOV - code coverage report
Current view: top level - server/restapi/change - ReplyAttentionSetUpdates.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 131 137 95.6 %
Date: 2022-11-19 15:00:39 Functions: 20 20 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2020 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.restapi.change;
      16             : 
      17             : import static com.google.common.collect.ImmutableSet.toImmutableSet;
      18             : 
      19             : import com.google.common.collect.ImmutableSet;
      20             : import com.google.common.collect.Sets;
      21             : import com.google.common.collect.Sets.SetView;
      22             : import com.google.common.flogger.FluentLogger;
      23             : import com.google.gerrit.entities.Account;
      24             : import com.google.gerrit.entities.AttentionSetUpdate;
      25             : import com.google.gerrit.entities.HumanComment;
      26             : import com.google.gerrit.entities.PatchSet;
      27             : import com.google.gerrit.extensions.api.changes.AttentionSetInput;
      28             : import com.google.gerrit.extensions.api.changes.ReviewInput;
      29             : import com.google.gerrit.extensions.restapi.AuthException;
      30             : import com.google.gerrit.extensions.restapi.BadRequestException;
      31             : import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
      32             : import com.google.gerrit.server.CommentsUtil;
      33             : import com.google.gerrit.server.CurrentUser;
      34             : import com.google.gerrit.server.account.AccountResolver;
      35             : import com.google.gerrit.server.account.ServiceUserClassifier;
      36             : import com.google.gerrit.server.approval.ApprovalsUtil;
      37             : import com.google.gerrit.server.change.AddToAttentionSetOp;
      38             : import com.google.gerrit.server.change.AttentionSetUnchangedOp;
      39             : import com.google.gerrit.server.change.CommentThread;
      40             : import com.google.gerrit.server.change.CommentThreads;
      41             : import com.google.gerrit.server.change.RemoveFromAttentionSetOp;
      42             : import com.google.gerrit.server.notedb.ChangeNotes;
      43             : import com.google.gerrit.server.notedb.ChangeUpdate;
      44             : import com.google.gerrit.server.permissions.ChangePermission;
      45             : import com.google.gerrit.server.permissions.PermissionBackend;
      46             : import com.google.gerrit.server.permissions.PermissionBackendException;
      47             : import com.google.gerrit.server.update.BatchUpdate;
      48             : import com.google.gerrit.server.util.AttentionSetUtil;
      49             : import com.google.gerrit.server.util.time.TimeUtil;
      50             : import com.google.inject.Inject;
      51             : import java.io.IOException;
      52             : import java.util.ArrayList;
      53             : import java.util.Collection;
      54             : import java.util.HashSet;
      55             : import java.util.List;
      56             : import java.util.Set;
      57             : import java.util.stream.Collectors;
      58             : import java.util.stream.Stream;
      59             : import org.eclipse.jgit.errors.ConfigInvalidException;
      60             : 
      61             : /**
      62             :  * This class is used to update the attention set when performing a review or replying on a change.
      63             :  */
      64             : public class ReplyAttentionSetUpdates {
      65         145 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      66             : 
      67             :   private final PermissionBackend permissionBackend;
      68             :   private final AddToAttentionSetOp.Factory addToAttentionSetOpFactory;
      69             :   private final RemoveFromAttentionSetOp.Factory removeFromAttentionSetOpFactory;
      70             :   private final ApprovalsUtil approvalsUtil;
      71             :   private final AccountResolver accountResolver;
      72             :   private final ServiceUserClassifier serviceUserClassifier;
      73             :   private final CommentsUtil commentsUtil;
      74             : 
      75             :   @Inject
      76             :   ReplyAttentionSetUpdates(
      77             :       PermissionBackend permissionBackend,
      78             :       AddToAttentionSetOp.Factory addToAttentionSetOpFactory,
      79             :       RemoveFromAttentionSetOp.Factory removeFromAttentionSetOpFactory,
      80             :       ApprovalsUtil approvalsUtil,
      81             :       AccountResolver accountResolver,
      82             :       ServiceUserClassifier serviceUserClassifier,
      83         145 :       CommentsUtil commentsUtil) {
      84         145 :     this.permissionBackend = permissionBackend;
      85         145 :     this.addToAttentionSetOpFactory = addToAttentionSetOpFactory;
      86         145 :     this.removeFromAttentionSetOpFactory = removeFromAttentionSetOpFactory;
      87         145 :     this.approvalsUtil = approvalsUtil;
      88         145 :     this.accountResolver = accountResolver;
      89         145 :     this.serviceUserClassifier = serviceUserClassifier;
      90         145 :     this.commentsUtil = commentsUtil;
      91         145 :   }
      92             : 
      93             :   /** Adjusts the attention set but only based on the automatic rules. */
      94             :   public void processAutomaticAttentionSetRulesOnReply(
      95             :       BatchUpdate bu,
      96             :       ChangeNotes changeNotes,
      97             :       boolean readyForReview,
      98             :       CurrentUser currentUser,
      99             :       List<HumanComment> commentsToBePublished) {
     100           4 :     if (serviceUserClassifier.isServiceUser(currentUser.getAccountId())) {
     101           0 :       return;
     102             :     }
     103           4 :     processRules(
     104             :         bu,
     105             :         changeNotes,
     106             :         readyForReview,
     107             :         currentUser,
     108           4 :         commentsToBePublished.stream().collect(toImmutableSet()));
     109           4 :   }
     110             : 
     111             :   /**
     112             :    * Adjusts the attention set by adding and removing users. If the same user should be added and
     113             :    * removed or added/removed twice, the user will only be added/removed once, based on first
     114             :    * addition/removal.
     115             :    */
     116             :   public void updateAttentionSet(
     117             :       BatchUpdate bu, ChangeNotes changeNotes, ReviewInput input, CurrentUser currentUser)
     118             :       throws BadRequestException, IOException, PermissionBackendException,
     119             :           UnprocessableEntityException, ConfigInvalidException {
     120          65 :     processManualUpdates(bu, changeNotes, input);
     121          65 :     if (input.ignoreAutomaticAttentionSetRules) {
     122             : 
     123             :       // If we ignore automatic attention set rules it means we need to pass this information to
     124             :       // ChangeUpdate. Also, we should stop all other attention set updates that are part of
     125             :       // this method and happen in PostReview.
     126           2 :       bu.addOp(changeNotes.getChangeId(), new AttentionSetUnchangedOp());
     127           2 :       return;
     128             :     }
     129          65 :     boolean isReadyForReview = isReadyForReview(changeNotes, input);
     130             : 
     131          65 :     if (isReadyForReview && serviceUserClassifier.isServiceUser(currentUser.getAccountId())) {
     132           1 :       botsWithNegativeLabelsAddOwnerAndUploader(bu, changeNotes, input);
     133           1 :       return;
     134             :     }
     135             : 
     136          65 :     processRules(
     137             :         bu,
     138             :         changeNotes,
     139             :         isReadyForReview,
     140             :         currentUser,
     141          65 :         getAllNewComments(changeNotes, input, currentUser));
     142          65 :   }
     143             : 
     144             :   private ImmutableSet<HumanComment> getAllNewComments(
     145             :       ChangeNotes changeNotes, ReviewInput input, CurrentUser currentUser) {
     146          65 :     Set<HumanComment> newComments = new HashSet<>();
     147          65 :     if (input.comments != null) {
     148             :       for (ReviewInput.CommentInput commentInput :
     149          18 :           input.comments.values().stream().flatMap(x -> x.stream()).collect(Collectors.toList())) {
     150          17 :         newComments.add(
     151          17 :             commentsUtil.newHumanComment(
     152             :                 changeNotes,
     153             :                 currentUser,
     154          17 :                 TimeUtil.now(),
     155             :                 commentInput.path,
     156          17 :                 commentInput.patchSet == null
     157          17 :                     ? changeNotes.getChange().currentPatchSetId()
     158          17 :                     : PatchSet.id(changeNotes.getChange().getId(), commentInput.patchSet),
     159          17 :                 commentInput.side(),
     160             :                 commentInput.message,
     161             :                 commentInput.unresolved,
     162             :                 commentInput.inReplyTo));
     163          17 :       }
     164             :     }
     165          65 :     List<HumanComment> drafts = new ArrayList<>();
     166          65 :     if (input.drafts == ReviewInput.DraftHandling.PUBLISH) {
     167           7 :       drafts =
     168           7 :           commentsUtil.draftByPatchSetAuthor(
     169           7 :               changeNotes.getChange().currentPatchSetId(), currentUser.getAccountId(), changeNotes);
     170             :     }
     171          65 :     if (input.drafts == ReviewInput.DraftHandling.PUBLISH_ALL_REVISIONS) {
     172           2 :       drafts = commentsUtil.draftByChangeAuthor(changeNotes, currentUser.getAccountId());
     173             :     }
     174          65 :     return Stream.concat(newComments.stream(), drafts.stream()).collect(toImmutableSet());
     175             :   }
     176             : 
     177             :   /**
     178             :    * Process the automatic rules of the attention set. All of the automatic rules except
     179             :    * adding/removing reviewers and entering/exiting WIP state are done here, and the rest are done
     180             :    * in {@link ChangeUpdate}
     181             :    */
     182             :   private void processRules(
     183             :       BatchUpdate bu,
     184             :       ChangeNotes changeNotes,
     185             :       boolean readyForReview,
     186             :       CurrentUser currentUser,
     187             :       ImmutableSet<HumanComment> allNewComments) {
     188             :     // Replying removes the publishing user from the attention set.
     189          65 :     removeFromAttentionSet(bu, changeNotes, currentUser.getAccountId(), "removed on reply", false);
     190             : 
     191          65 :     Account.Id uploader = changeNotes.getCurrentPatchSet().uploader();
     192          65 :     Account.Id owner = changeNotes.getChange().getOwner();
     193             : 
     194             :     // The rest of the conditions only apply if the change is open.
     195          65 :     if (changeNotes.getChange().getStatus().isClosed()) {
     196             :       // We still add the owner if a new comment thread was created, on closed changes.
     197          10 :       if (allNewComments.stream().anyMatch(c -> c.parentUuid == null)) {
     198           1 :         addToAttentionSet(bu, changeNotes, owner, "A new comment thread was created", false);
     199             :       }
     200          10 :       return;
     201             :     }
     202             :     // The rest of the conditions only apply if the change is ready for review.
     203          65 :     if (!readyForReview) {
     204          14 :       return;
     205             :     }
     206             : 
     207          65 :     if (!currentUser.getAccountId().equals(owner)) {
     208          29 :       addToAttentionSet(bu, changeNotes, owner, "Someone else replied on the change", false);
     209             :     }
     210          65 :     if (!owner.equals(uploader) && !currentUser.getAccountId().equals(uploader)) {
     211           1 :       addToAttentionSet(bu, changeNotes, uploader, "Someone else replied on the change", false);
     212             :     }
     213             : 
     214          65 :     addAllAuthorsOfCommentThreads(bu, changeNotes, allNewComments);
     215          65 :   }
     216             : 
     217             :   /** Adds all authors of all comment threads that received a reply during this update */
     218             :   private void addAllAuthorsOfCommentThreads(
     219             :       BatchUpdate bu, ChangeNotes changeNotes, ImmutableSet<HumanComment> allNewComments) {
     220          65 :     List<HumanComment> publishedComments = commentsUtil.publishedHumanCommentsByChange(changeNotes);
     221          65 :     ImmutableSet<CommentThread<HumanComment>> repliedToCommentThreads =
     222          65 :         CommentThreads.forComments(publishedComments).getThreadsForChildren(allNewComments);
     223             : 
     224          65 :     ImmutableSet<Account.Id> repliedToUsers =
     225          65 :         repliedToCommentThreads.stream()
     226          65 :             .map(CommentThread::comments)
     227          65 :             .flatMap(Collection::stream)
     228          65 :             .map(comment -> comment.author.getId())
     229          65 :             .collect(toImmutableSet());
     230          65 :     ImmutableSet<Account.Id> possibleUsersToAdd = approvalsUtil.getReviewers(changeNotes).all();
     231          65 :     SetView<Account.Id> usersToAdd = Sets.intersection(possibleUsersToAdd, repliedToUsers);
     232             : 
     233          65 :     for (Account.Id user : usersToAdd) {
     234           3 :       addToAttentionSet(
     235             :           bu, changeNotes, user, "Someone else replied on a comment you posted", false);
     236           3 :     }
     237          65 :   }
     238             : 
     239             :   /** Process the manual updates of the attention set. */
     240             :   private void processManualUpdates(BatchUpdate bu, ChangeNotes changeNotes, ReviewInput input)
     241             :       throws BadRequestException, IOException, PermissionBackendException,
     242             :           UnprocessableEntityException, ConfigInvalidException {
     243          65 :     Set<Account.Id> accountsChangedInCommit = new HashSet<>();
     244             :     // If we specify a user to remove, and the user is in the attention set, we remove it.
     245          65 :     if (input.removeFromAttentionSet != null) {
     246           1 :       for (AttentionSetInput remove : input.removeFromAttentionSet) {
     247           1 :         removeFromAttentionSet(bu, changeNotes, remove, accountsChangedInCommit);
     248           1 :       }
     249             :     }
     250             : 
     251             :     // If we don't specify a user to remove, but we specify addition for that user, the user will be
     252             :     // added if they are not in the attention set yet.
     253          65 :     if (input.addToAttentionSet != null) {
     254           7 :       for (AttentionSetInput add : input.addToAttentionSet) {
     255           7 :         addToAttentionSet(bu, changeNotes, add, accountsChangedInCommit);
     256           7 :       }
     257             :     }
     258          65 :   }
     259             : 
     260             :   /**
     261             :    * Bots don't process automatic rules, the only attention set change they do is this rule: Add
     262             :    * owner and uploader when a bot votes negatively.
     263             :    */
     264             :   private void botsWithNegativeLabelsAddOwnerAndUploader(
     265             :       BatchUpdate bu, ChangeNotes changeNotes, ReviewInput input) {
     266           1 :     if (input.labels != null && input.labels.values().stream().anyMatch(vote -> vote < 0)) {
     267           1 :       Account.Id uploader = changeNotes.getCurrentPatchSet().uploader();
     268           1 :       Account.Id owner = changeNotes.getChange().getOwner();
     269           1 :       addToAttentionSet(bu, changeNotes, owner, "A robot voted negatively on a label", false);
     270           1 :       if (!owner.equals(uploader)) {
     271           0 :         addToAttentionSet(bu, changeNotes, uploader, "A robot voted negatively on a label", false);
     272             :       }
     273             :     }
     274           1 :   }
     275             : 
     276             :   /**
     277             :    * Adds the user to the attention set
     278             :    *
     279             :    * @param bu BatchUpdate to perform the updates to the attention set
     280             :    * @param changeNotes current change
     281             :    * @param user user to add to the attention set
     282             :    * @param reason reason for adding
     283             :    * @param notify whether or not to notify about this addition
     284             :    */
     285             :   private void addToAttentionSet(
     286             :       BatchUpdate bu, ChangeNotes changeNotes, Account.Id user, String reason, boolean notify) {
     287          30 :     AddToAttentionSetOp addToAttentionSet = addToAttentionSetOpFactory.create(user, reason, notify);
     288          30 :     bu.addOp(changeNotes.getChangeId(), addToAttentionSet);
     289          30 :   }
     290             : 
     291             :   /**
     292             :    * Removes the user from the attention set
     293             :    *
     294             :    * @param bu BatchUpdate to perform the updates to the attention set.
     295             :    * @param changeNotes current change.
     296             :    * @param user user to add remove from the attention set.
     297             :    * @param reason reason for removing.
     298             :    * @param notify whether or not to notify about this removal.
     299             :    */
     300             :   private void removeFromAttentionSet(
     301             :       BatchUpdate bu, ChangeNotes changeNotes, Account.Id user, String reason, boolean notify) {
     302          65 :     RemoveFromAttentionSetOp removeFromAttentionSetOp =
     303          65 :         removeFromAttentionSetOpFactory.create(user, reason, notify);
     304          65 :     bu.addOp(changeNotes.getChangeId(), removeFromAttentionSetOp);
     305          65 :   }
     306             : 
     307             :   private static boolean isReadyForReview(ChangeNotes changeNotes, ReviewInput input) {
     308          65 :     return (!changeNotes.getChange().isWorkInProgress() && !input.workInProgress) || input.ready;
     309             :   }
     310             : 
     311             :   private void addToAttentionSet(
     312             :       BatchUpdate bu,
     313             :       ChangeNotes changeNotes,
     314             :       AttentionSetInput add,
     315             :       Set<Account.Id> accountsChangedInCommit)
     316             :       throws BadRequestException, IOException, PermissionBackendException,
     317             :           UnprocessableEntityException, ConfigInvalidException {
     318           7 :     AttentionSetUtil.validateInput(add);
     319             :     try {
     320           7 :       Account.Id attentionUserId =
     321           7 :           getAccountIdAndValidateUser(
     322             :               changeNotes, add.user, accountsChangedInCommit, AttentionSetUpdate.Operation.ADD);
     323           7 :       addToAttentionSet(bu, changeNotes, attentionUserId, add.reason, false);
     324           1 :     } catch (AccountResolver.UnresolvableAccountException ex) {
     325             :       // This happens only when the account doesn't exist. Silently ignore it. If we threw an error
     326             :       // message here, then it would be possible to probe whether an account exists.
     327           1 :     } catch (AuthException ex) {
     328             :       // adding users without permission to the attention set should fail silently.
     329           1 :       logger.atFine().log("%s", ex.getMessage());
     330           7 :     }
     331           7 :   }
     332             : 
     333             :   private void removeFromAttentionSet(
     334             :       BatchUpdate bu,
     335             :       ChangeNotes changeNotes,
     336             :       AttentionSetInput remove,
     337             :       Set<Account.Id> accountsChangedInCommit)
     338             :       throws BadRequestException, IOException, PermissionBackendException,
     339             :           UnprocessableEntityException, ConfigInvalidException {
     340           1 :     AttentionSetUtil.validateInput(remove);
     341             :     try {
     342           1 :       Account.Id attentionUserId =
     343           1 :           getAccountIdAndValidateUser(
     344             :               changeNotes,
     345             :               remove.user,
     346             :               accountsChangedInCommit,
     347             :               AttentionSetUpdate.Operation.REMOVE);
     348           1 :       removeFromAttentionSet(bu, changeNotes, attentionUserId, remove.reason, false);
     349           0 :     } catch (AccountResolver.UnresolvableAccountException ex) {
     350             :       // This happens only when the account doesn't exist. Silently ignore it. If we threw an error
     351             :       // message here, then it would be possible to probe whether an account exists.
     352           0 :     } catch (AuthException ex) {
     353             :       // this should never happen since removing users with permissions should work.
     354           0 :       logger.atSevere().log("%s", ex.getMessage());
     355           1 :     }
     356           1 :   }
     357             : 
     358             :   private Account.Id getAccountId(
     359             :       ChangeNotes changeNotes, String user, AttentionSetUpdate.Operation operation)
     360             :       throws ConfigInvalidException, IOException, UnprocessableEntityException,
     361             :           PermissionBackendException, AuthException {
     362           7 :     Account.Id attentionUserId = accountResolver.resolve(user).asUnique().account().id();
     363             :     try {
     364           7 :       permissionBackend
     365           7 :           .absentUser(attentionUserId)
     366           7 :           .change(changeNotes)
     367           7 :           .check(ChangePermission.READ);
     368           1 :     } catch (AuthException e) {
     369             :       // If the change is private, it is okay to add the user to the attention set since that
     370             :       // person will be granted visibility when a reviewer.
     371           1 :       if (!changeNotes.getChange().isPrivate()) {
     372             : 
     373             :         // Removing users without access is allowed, adding is not allowed
     374           1 :         if (operation == AttentionSetUpdate.Operation.ADD) {
     375           1 :           throw new AuthException(
     376             :               "Can't modify attention set: Read not permitted for " + attentionUserId, e);
     377             :         }
     378             :       }
     379           7 :     }
     380           7 :     return attentionUserId;
     381             :   }
     382             : 
     383             :   private Account.Id getAccountIdAndValidateUser(
     384             :       ChangeNotes changeNotes,
     385             :       String user,
     386             :       Set<Account.Id> accountsChangedInCommit,
     387             :       AttentionSetUpdate.Operation operation)
     388             :       throws ConfigInvalidException, IOException, PermissionBackendException,
     389             :           UnprocessableEntityException, BadRequestException, AuthException {
     390             :     try {
     391           7 :       Account.Id attentionUserId = getAccountId(changeNotes, user, operation);
     392           7 :       if (accountsChangedInCommit.contains(attentionUserId)) {
     393           1 :         throw new BadRequestException(
     394           1 :             String.format(
     395             :                 "%s can not be added/removed twice, and can not be added and "
     396             :                     + "removed at the same time",
     397             :                 user));
     398             :       }
     399           7 :       accountsChangedInCommit.add(attentionUserId);
     400           7 :       return attentionUserId;
     401           1 :     } catch (AccountResolver.UnresolvableAccountException ex) {
     402             :       // This can only happen if this user can't see the account or the account doesn't exist.
     403             :       // Silently modify the account's attention set anyway, if the account exists.
     404           0 :       return accountResolver.resolveIgnoreVisibility(user).asUnique().account().id();
     405             :     }
     406             :   }
     407             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750