Merge branch 'main' into avm99963-monorail

Merged commit 34d8229ae2b51fb1a15bd208e6fe6185c94f6266

GitOrigin-RevId: 7ee0917f93a577e475f8e09526dd144d245593f4
diff --git a/businesslogic/work_env.py b/businesslogic/work_env.py
index d15d67f..58d52cb 100644
--- a/businesslogic/work_env.py
+++ b/businesslogic/work_env.py
@@ -1,7 +1,6 @@
-# Copyright 2017 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2017 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """WorkEnv is a context manager and API for high-level operations.
 
@@ -68,6 +67,7 @@
 from framework import framework_helpers
 from framework import framework_views
 from framework import permissions
+from redirect import redirectissue
 from search import frontendsearchpipeline
 from services import features_svc
 from services import tracker_fulltext
@@ -79,10 +79,10 @@
 from tracker import tracker_constants
 from tracker import tracker_helpers
 from project import project_helpers
-from proto import features_pb2
-from proto import project_pb2
-from proto import tracker_pb2
-from proto import user_pb2
+from mrproto import features_pb2
+from mrproto import project_pb2
+from mrproto import tracker_pb2
+from mrproto import user_pb2
 
 
 # TODO(jrobbins): break this file into one facade plus ~5
@@ -176,7 +176,8 @@
     permitted = self._UserCanUsePermInIssue(issue, perm)
     if not permitted:
       raise permissions.PermissionException(
-        'User lacks permission %r in issue' % perm)
+          'User lacks permission %r in issue %s %d', perm, issue.project_name,
+          issue.local_id)
 
   def _AssertUserCanModifyIssues(
       self, issue_delta_pairs, is_description_change, comment_content=None):
@@ -216,6 +217,15 @@
               delta.merged_into, use_cache=False, allow_viewing_deleted=True)
           self._AssertPermInIssue(merged_into_issue, permissions.EDIT_ISSUE)
 
+        # User cannot modify blocking issues on issues they cannot edit.
+        all_block = (
+            delta.blocked_on_add + delta.blocking_add +
+            delta.blocked_on_remove + delta.blocking_remove)
+        for block_iid in all_block:
+          blocked_issue = self.GetIssue(
+              block_iid, use_cache=False, allow_viewing_deleted=True)
+          self._AssertPermInIssue(blocked_issue, permissions.EDIT_ISSUE)
+
         # User cannot change values for restricted fields they cannot edit.
         field_ids = [fv.field_id for fv in delta.field_vals_add]
         field_ids.extend([fv.field_id for fv in delta.field_vals_remove])
@@ -225,7 +235,7 @@
           self._AssertUserCanEditFieldsAndEnumMaskedLabels(
               project, config, field_ids, labels)
         except permissions.PermissionException as e:
-          err_agg.AddErrorMessage(e.message)
+          err_agg.AddErrorMessage(str(e))
 
         if issue_perms.HasPerm(permissions.EDIT_ISSUE, self.mc.auth.user_id,
                                project):
@@ -324,7 +334,7 @@
           try:
             self._AssertUserCanEditValueForFieldDef(project, fd)
           except permissions.PermissionException as e:
-            err_agg.AddErrorMessage(e.message)
+            err_agg.AddErrorMessage(str(e))
 
   def _AssertUserCanViewFieldDef(self, project, field):
     """Make sure the user may view the field."""
@@ -395,9 +405,12 @@
     # the results are filtered by permission to view each project.
 
     with self.mc.profiler.Phase('list projects for %r' % self.mc.auth.user_id):
-      project_ids = self.services.project.GetVisibleLiveProjects(
-          self.mc.cnxn, self.mc.auth.user_pb, self.mc.auth.effective_ids,
-          domain=domain, use_cache=use_cache)
+      project_ids = self.services.project.GetVisibleProjects(
+          self.mc.cnxn,
+          self.mc.auth.user_pb,
+          self.mc.auth.effective_ids,
+          domain=domain,
+          use_cache=use_cache)
 
     return project_ids
 
@@ -935,7 +948,7 @@
             'Ancestor path %s is invalid.' % ancestor_path)
       project_perms = permissions.GetPermissions(
           self.mc.auth.user_pb, self.mc.auth.effective_ids, project)
-      if not permissions.CanEditComponentDef(
+      if not permissions.CanEditComponentDefLegacy(
           self.mc.auth.effective_ids, project_perms, project, ancestor_def,
           config):
         raise permissions.PermissionException(
@@ -976,7 +989,7 @@
 
     project_perms = permissions.GetPermissions(
         self.mc.auth.user_pb, self.mc.auth.effective_ids, project)
-    if not permissions.CanEditComponentDef(
+    if not permissions.CanEditComponentDefLegacy(
         self.mc.auth.effective_ids, project_perms, project, component_def,
         config):
       raise permissions.PermissionException(
@@ -1031,14 +1044,14 @@
       owner_id,  # type: int
       cc_ids,  # type: Sequence[int]
       labels,  # type: Sequence[str]
-      field_values,  # type: Sequence[proto.tracker_pb2.FieldValue]
+      field_values,  # type: Sequence[mrproto.tracker_pb2.FieldValue]
       component_ids,  # type: Sequence[int]
       marked_description,  # type: str
       blocked_on=None,  # type: Sequence[int]
       blocking=None,  # type: Sequence[int]
       attachments=None,  # type: Sequence[Tuple[str, str, str]]
-      phases=None,  # type: Sequence[proto.tracker_pb2.Phase]
-      approval_values=None,  # type: Sequence[proto.tracker_pb2.ApprovalValue]
+      phases=None,  # type: Sequence[mrproto.tracker_pb2.Phase]
+      approval_values=None,  # type: Sequence[mrproto.tracker_pb2.ApprovalValue]
       send_email=True,  # type: bool
       reporter_id=None,  # type: int
       timestamp=None,  # type: int
@@ -1046,7 +1059,8 @@
       dangling_blocking=None,  # type: Sequence[DanglingIssueRef]
       raise_filter_errors=True,  # type: bool
   ):
-    # type: (...) -> (proto.tracker_pb2.Issue, proto.tracker_pb2.IssueComment)
+    # type: (...) ->
+    #   (mrproto.tracker_pb2.Issue, mrproto.tracker_pb2.IssueComment)
     """Create and store a new issue with all the given information.
 
     Args:
@@ -1134,6 +1148,7 @@
       issue.owner_modified_timestamp = timestamp
       issue.status_modified_timestamp = timestamp
       issue.component_modified_timestamp = timestamp
+      issue.migration_modified_timestamp = timestamp
 
       # Validate the issue
       tracker_helpers.AssertValidIssueForCreate(
@@ -1273,7 +1288,18 @@
     self._AssertPermInIssue(issue, permissions.DELETE_ISSUE)
     self._AssertPermInProject(permissions.EDIT_ISSUE, target_project)
 
-    if permissions.GetRestrictions(issue):
+    restrictions = permissions.GetRestrictions(issue)
+    # Issues with allowed labels may move between allowed projects.
+    # Context: https://crbug.com/monorail/11894
+    allowed_project_names = ['chromium', 'webrtc']
+    allowed_labels = frozenset(
+        ['restrict-view-securityteam', 'restrict-view-securitynotify'])
+    if (target_project.project_name.lower()
+        in allowed_project_names) and (issue.project_name.lower()
+                                       in allowed_project_names):
+      restrictions = set(restrictions) - allowed_labels
+
+    if restrictions:
       raise exceptions.InputException(
           'Issues with Restrict labels are not allowed to be moved')
 
@@ -1398,7 +1424,7 @@
       group_by_spec,  # type: str
       sort_spec,  # type: str
       use_cached_searches,  # type: bool
-      project=None  # type: proto.Project
+      project=None  # type: mrproto.Project
   ):
     # type: (...) -> search.frontendsearchpipeline.FrontendSearchPipeline
     """Do an issue search w/ mc + passed in args to return a pipeline object.
@@ -1527,7 +1553,7 @@
           self._AssertUserCanViewIssue(
               issue, allow_viewing_deleted=allow_viewing_deleted)
         except permissions.PermissionException as e:
-          permission_err_agg.AddErrorMessage(e.message)
+          permission_err_agg.AddErrorMessage(str(e))
 
     return issues_by_id
 
@@ -1600,6 +1626,25 @@
         issue, allow_viewing_deleted=allow_viewing_deleted)
     return issue
 
+  def ExtractMigratedIdFromLabels(self, labels):
+    """Returns the issue ID from a migration label if present."""
+    # Assume that there's only one migrated label.
+    # Or at least drop any labels besides the first one.
+    if labels is not None:
+      for label in labels:
+        lower_label = label.lower()
+        for prefix in settings.migrated_buganizer_issue_prefixes:
+          if lower_label.startswith(prefix):
+            return label.replace(prefix, '')
+    return None
+
+  def GetIssueMigratedID(self, project_name, local_id, labels=None):
+    """Return the redirect id for a specific issue."""
+    migrated_id = redirectissue.RedirectIssue.Get(project_name, local_id)
+    if migrated_id is not None:
+      return migrated_id
+    return self.ExtractMigratedIdFromLabels(labels)
+
   def GetRelatedIssueRefs(self, issues):
     """Return a dict {iid: (project_name, local_id)} for all related issues."""
     related_iids = set()
@@ -1609,7 +1654,6 @@
         related_iids.update(issue.blocking_iids)
         if issue.merged_into:
           related_iids.add(issue.merged_into)
-      logging.info('related_iids is %r', related_iids)
       return self.services.issue.LookupIssueRefs(self.mc.cnxn, related_iids)
 
   def GetIssueRefs(self, issue_ids):
@@ -1647,7 +1691,7 @@
   def BulkUpdateIssueApprovalsV3(
       self, delta_specifications, comment_content, send_email):
     # type: (Sequence[Tuple[int, int, tracker_pb2.ApprovalDelta]]], str,
-    #     Boolean -> Sequence[proto.tracker_pb2.ApprovalValue]
+    #     Boolean -> Sequence[mrproto.tracker_pb2.ApprovalValue]
     """Executes the ApprovalDeltas.
 
     Args:
@@ -1691,10 +1735,10 @@
       send_email=True,
       kept_attachments=None,
       update_perms=False):
-    # type: (int, int, proto.tracker_pb2.ApprovalDelta, str, Boolean,
-    #     Optional[Sequence[proto.tracker_pb2.Attachment]], Optional[Boolean],
+    # type: (int, int, mrproto.tracker_pb2.ApprovalDelta, str, Boolean,
+    #     Optional[Sequence[mrproto.tracker_pb2.Attachment]], Optional[Boolean],
     #     Optional[Sequence[int]], Optional[Boolean]) ->
-    #     (proto.tracker_pb2.ApprovalValue, proto.tracker_pb2.IssueComment)
+    #     (mrproto.tracker_pb2.ApprovalValue, mrproto.tracker_pb2.IssueComment)
     """Update an issue's approval.
 
     Raises:
@@ -1769,7 +1813,7 @@
 
   def ConvertIssueApprovalsTemplate(
       self, config, issue, template_name, comment_content, send_email=True):
-    # type: (proto.tracker_pb2.ProjectIssueConfig, proto.tracker_pb2.Issue,
+    # type: (mrproto.tracker_pb2.ProjectIssueConfig, mrproto.tracker_pb2.Issue,
     #     str, str, Optional[Boolean] )
     """Convert an issue's existing approvals structure to match the one of
        the given template.
@@ -1853,6 +1897,7 @@
       # Reject attempts to merge an issue into an issue we cannot view and edit.
       merged_into_issue = self.GetIssue(
           delta.merged_into, use_cache=False, allow_viewing_deleted=True)
+      self._AssertPermInIssue(merged_into_issue, permissions.EDIT_ISSUE)
       self._AssertPermInIssue(issue, permissions.EDIT_ISSUE)
       # Reject attempts to merge an issue into itself.
       if issue.issue_id == delta.merged_into:
@@ -1864,6 +1909,15 @@
         comment_content) > tracker_constants.MAX_COMMENT_CHARS:
       raise exceptions.InputException('Comment is too long')
 
+    # Reject attempts to modifying blocking issues we cannot edit.
+    all_block = (
+        delta.blocked_on_add + delta.blocking_add + delta.blocked_on_remove +
+        delta.blocking_remove)
+    for block_iid in all_block:
+      blocked_issue = self.GetIssue(
+          block_iid, use_cache=False, allow_viewing_deleted=True)
+      self._AssertPermInIssue(blocked_issue, permissions.EDIT_ISSUE)
+
     # Reject attempts to block on issue on itself.
     if (issue.issue_id in delta.blocked_on_add
         or issue.issue_id in delta.blocking_add):
@@ -1877,7 +1931,13 @@
     field_ids = [fv.field_id for fv in delta.field_vals_add]
     field_ids.extend([fvr.field_id for fvr in delta.field_vals_remove])
     field_ids.extend(delta.fields_clear)
+
     labels = itertools.chain(delta.labels_add, delta.labels_remove)
+    labels_err_msg = field_helpers.ValidateLabels(
+        self.mc.cnxn, self.services, issue.project_id, delta.labels_add)
+    if labels_err_msg:
+      raise exceptions.InputException(labels_err_msg)
+
     self._AssertUserCanEditFieldsAndEnumMaskedLabels(
         project, config, field_ids, labels)
 
@@ -2062,6 +2122,7 @@
       changes.issues_to_update_dict[issue.issue_id] = issue
 
       issue.modified_timestamp = now_timestamp
+      issue.migration_modified_timestamp = now_timestamp
 
       if (iid in changes.old_owners_by_iid and
           old_owner != tracker_bizobj.GetOwnerId(issue)):
@@ -2142,8 +2203,8 @@
           self.mc.cnxn, pid, attachment_bytes_used=new_bytes_used, commit=False)
 
     # Reindex issues and commit all DB changes.
-    issues_to_reindex = set(
-        comments_by_iid.keys() + impacted_comments_by_iid.keys())
+    issues_to_reindex = (
+        set(comments_by_iid.keys()) | set(impacted_comments_by_iid.keys()))
     if issues_to_reindex:
       self.services.issue.EnqueueIssuesForIndexing(
           self.mc.cnxn, issues_to_reindex, commit=False)
@@ -2168,9 +2229,9 @@
       # Group issues for each unique delta by project because
       # SendIssueBulkChangeNotification cannot handle cross-project
       # notifications and hostports are specific to each project.
-      issues_by_pid = collections.defaultdict(set)
+      issues_by_pid = collections.defaultdict(list)
       for issue in issues:
-        issues_by_pid[issue.project_id].add(issue)
+        issues_by_pid[issue.project_id].append(issue)
       for project_issues in issues_by_pid.values():
         # Send one email to involved users for the issue.
         if len(project_issues) == 1:
@@ -3806,20 +3867,6 @@
       self.services.features.UpdateHotlistItemsFields(
           self.mc.cnxn, hotlist_id, new_notes=new_notes)
 
-  def expungeUsersFromStars(self, user_ids):
-    """Wipes any starred user or user's stars from all star services.
-
-    This method will not commit the operation. This method will not
-    make changes to in-memory data.
-    """
-
-    self.services.project_star.ExpungeStarsByUsers(self.mc.cnxn, user_ids)
-    self.services.issue_star.ExpungeStarsByUsers(self.mc.cnxn, user_ids)
-    self.services.hotlist_star.ExpungeStarsByUsers(self.mc.cnxn, user_ids)
-    self.services.user_star.ExpungeStarsByUsers(self.mc.cnxn, user_ids)
-    for user_id in user_ids:
-      self.services.user_star.ExpungeStars(self.mc.cnxn, user_id, commit=False)
-
   # Permissions
 
   # ListFooPermission methods will return the list of permissions in addition to