Example #1
0
def FilterIssues(mr, issues, services):
    """Return a list of issues that the user is allowed to view."""
    allowed_issues = []
    project_ids = GetAllProjectsOfIssues(issues)
    issue_projects = services.project.GetProjects(mr.cnxn, project_ids)
    configs_by_project_id = services.config.GetProjectConfigs(
        mr.cnxn, project_ids)
    perms_by_project_id = {
        pid: permissions.GetPermissions(mr.auth.user_pb, mr.auth.effective_ids,
                                        p)
        for pid, p in issue_projects.items()
    }
    for issue in issues:
        if (mr.can == 1) or not issue.closed_timestamp:
            issue_project = issue_projects[issue.project_id]
            config = configs_by_project_id[issue.project_id]
            perms = perms_by_project_id[issue.project_id]
            granted_perms = tracker_bizobj.GetGrantedPerms(
                issue, mr.auth.effective_ids, config)
            permit_view = permissions.CanViewIssue(mr.auth.effective_ids,
                                                   perms,
                                                   issue_project,
                                                   issue,
                                                   granted_perms=granted_perms)
            if permit_view:
                allowed_issues.append(issue)

    return allowed_issues
Example #2
0
    def _MakeEmailTasks(self, cnxn, issue, project, config, comment, hostport,
                        users_by_id, pings):
        """Return a list of dicts for tasks to notify people."""
        detail_url = framework_helpers.IssueCommentURL(
            hostport, project, issue.local_id, seq_num=comment.sequence)
        fields = sorted((field_def for (field_def, _date_value) in pings),
                        key=lambda fd: fd.field_name)
        email_data = {
            'issue': tracker_views.IssueView(issue, users_by_id, config),
            'summary': issue.summary,
            'ping_comment_content': comment.content,
            'detail_url': detail_url,
            'fields': fields,
        }

        # Generate two versions of email body: members version has all
        # full email addresses exposed.
        body_for_non_members = self.email_template.GetResponse(email_data)
        framework_views.RevealAllEmails(users_by_id)
        body_for_members = self.email_template.GetResponse(email_data)
        logging.info('body for non-members is:\n%r' % body_for_non_members)
        logging.info('body for members is:\n%r' % body_for_members)

        contributor_could_view = permissions.CanViewIssue(
            set(), permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET, project,
            issue)
        # Note: We never notify the reporter of any issue just because they
        # reported it, only if they star it.
        # TODO(jrobbins): add a user preference for notifying starrers.
        starrer_ids = []

        # TODO(jrobbins): consider IsNoisy() when we support notifying starrers.
        group_reason_list = notify_reasons.ComputeGroupReasonList(
            cnxn,
            self.services,
            project,
            issue,
            config,
            users_by_id, [],
            contributor_could_view,
            starrer_ids=starrer_ids,
            commenter_in_project=True)

        commenter_view = users_by_id[comment.user_id]
        email_tasks = notify_helpers.MakeBulletedEmailWorkItems(
            group_reason_list,
            issue,
            body_for_non_members,
            body_for_members,
            project,
            hostport,
            commenter_view,
            detail_url,
            seq_num=comment.sequence,
            subject_prefix='Follow up on issue ',
            compact_subject_prefix='Follow up ')

        return email_tasks
Example #3
0
  def _ProcessUpstreamIssue(
      self, cnxn, upstream_issue, upstream_project, upstream_config,
      issue, omit_ids, hostport, commenter_view):
    """Compute notifications for one upstream issue that is now blocking."""
    upstream_detail_url = framework_helpers.FormatAbsoluteURLForDomain(
        hostport, upstream_issue.project_name, urls.ISSUE_DETAIL,
        id=upstream_issue.local_id)
    logging.info('upstream_detail_url = %r', upstream_detail_url)
    detail_url = framework_helpers.FormatAbsoluteURLForDomain(
        hostport, issue.project_name, urls.ISSUE_DETAIL,
        id=issue.local_id)

    # Only issues that any contributor could view are sent to mailing lists.
    contributor_could_view = permissions.CanViewIssue(
        set(), permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET,
        upstream_project, upstream_issue)

    # Now construct the e-mail to send

    # Note: we purposely do not notify users who starred an issue
    # about changes in blocking.
    users_by_id = framework_views.MakeAllUserViews(
        cnxn, self.services.user,
        tracker_bizobj.UsersInvolvedInIssues([upstream_issue]), omit_ids)

    is_blocking = upstream_issue.issue_id in issue.blocked_on_iids

    email_data = {
        'issue': tracker_views.IssueView(
            upstream_issue, users_by_id, upstream_config),
        'summary': upstream_issue.summary,
        'detail_url': upstream_detail_url,
        'is_blocking': ezt.boolean(is_blocking),
        'downstream_issue_ref': tracker_bizobj.FormatIssueRef(
            (None, issue.local_id)),
        'downstream_issue_url': detail_url,
        }

    # TODO(jrobbins): Generate two versions of email body: members
    # vesion has other member full email addresses exposed.  But, don't
    # expose too many as we iterate through upstream projects.
    body_link_only = self.link_only_email_template.GetResponse(
        {'detail_url': upstream_detail_url, 'was_created': ezt.boolean(False)})
    body = self.email_template.GetResponse(email_data)

    omit_addrs = {users_by_id[omit_id].email for omit_id in omit_ids}

    # Get the transitive set of owners and Cc'd users, and their UserView's.
    # Give each user a bullet-list of all the reasons that apply for that user.
    # Starrers are not notified of blocking changes to reduce noise.
    group_reason_list = notify_reasons.ComputeGroupReasonList(
        cnxn, self.services, upstream_project, upstream_issue,
        upstream_config, users_by_id, omit_addrs, contributor_could_view)
    one_issue_email_tasks = notify_helpers.MakeBulletedEmailWorkItems(
        group_reason_list, upstream_issue, body_link_only, body, body,
        upstream_project, hostport, commenter_view, detail_url)

    return one_issue_email_tasks
Example #4
0
def GetAttachmentIfAllowed(mr, services):
    """Retrieve the requested attachment, or raise an appropriate exception.

  Args:
    mr: commonly used info parsed from the request.
    services: connections to backend services.

  Returns:
    The requested Attachment PB, and the Issue that it belongs to.

  Raises:
    NoSuchAttachmentException: attachment was not found or was marked deleted.
    NoSuchIssueException: issue that contains attachment was not found.
    PermissionException: the user is not allowed to view the attachment.
  """
    attachment = None

    attachment, cid, issue_id = services.issue.GetAttachmentAndContext(
        mr.cnxn, mr.aid)

    issue = services.issue.GetIssue(mr.cnxn, issue_id)
    config = services.config.GetProjectConfig(mr.cnxn, issue.project_id)
    granted_perms = tracker_bizobj.GetGrantedPerms(issue,
                                                   mr.auth.effective_ids,
                                                   config)
    permit_view = permissions.CanViewIssue(mr.auth.effective_ids,
                                           mr.perms,
                                           mr.project,
                                           issue,
                                           granted_perms=granted_perms)
    if not permit_view:
        raise permissions.PermissionException(
            'Cannot view attachment\'s issue')

    comment = services.issue.GetComment(mr.cnxn, cid)
    can_delete = False
    if mr.auth.user_id and mr.project:
        can_delete = permissions.CanDelete(mr.auth.user_id,
                                           mr.auth.effective_ids,
                                           mr.perms,
                                           comment.deleted_by,
                                           comment.user_id,
                                           mr.project,
                                           permissions.GetRestrictions(issue),
                                           granted_perms=granted_perms)
    if comment.deleted_by and not can_delete:
        raise permissions.PermissionException(
            'Cannot view attachment\'s comment')

    return attachment, issue
Example #5
0
 def AssertBasePermission(self, mr):
   """Check that the user has permission to even visit this page."""
   super(IssuePeek, self).AssertBasePermission(mr)
   try:
     issue = self._GetIssue(mr)
   except issue_svc.NoSuchIssueException:
     return
   if not issue:
     return
   config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
   granted_perms = tracker_bizobj.GetGrantedPerms(
       issue, mr.auth.effective_ids, config)
   permit_view = permissions.CanViewIssue(
       mr.auth.effective_ids, mr.perms, mr.project, issue,
       allow_viewing_deleted=self._ALLOW_VIEWING_DELETED,
       granted_perms=granted_perms)
   if not permit_view:
     logging.warning('Issue is %r', issue)
     raise permissions.PermissionException(
         'User is not allowed to view this issue')
Example #6
0
  def AssertBasePermission(self, mr):
    """Make sure that the logged in user has permission to view this page."""
    super(IssueOriginal, self).AssertBasePermission(mr)
    issue, comment = self._GetIssueAndComment(mr)

    # TODO(jrobbins): take granted perms into account here.
    if issue and not permissions.CanViewIssue(
        mr.auth.effective_ids, mr.perms, mr.project, issue,
        allow_viewing_deleted=True):
      raise permissions.PermissionException(
          'User is not allowed to view this issue')

    can_view_inbound_message = self.CheckPerm(
        mr, permissions.VIEW_INBOUND_MESSAGES, art=issue)
    issue_perms = permissions.UpdateIssuePermissions(
        mr.perms, mr.project, issue, mr.auth.effective_ids)
    commenter = self.services.user.GetUser(mr.cnxn, comment.user_id)
    can_view_comment = permissions.CanViewComment(
        comment, commenter, mr.auth.user_id, issue_perms)
    if not can_view_inbound_message or not can_view_comment:
      raise permissions.PermissionException(
          'Only project members may view original email text')
Example #7
0
def FilterOutNonViewableIssues(effective_ids, user, project_dict, config_dict,
                               issues):
    """Return a filtered list of issues that the user can view."""
    perms_dict = GetPermissionsInAllProjects(user, effective_ids,
                                             list(project_dict.values()))

    denied_project_ids = {
        pid
        for pid, p in project_dict.items()
        if not permissions.CanView(effective_ids, perms_dict[pid], p, [])
    }

    results = []
    for issue in issues:
        if issue.deleted or issue.project_id in denied_project_ids:
            continue

        if not permissions.HasRestrictions(issue):
            may_view = True
        else:
            perms = perms_dict[issue.project_id]
            project = project_dict[issue.project_id]
            config = config_dict.get(issue.project_id,
                                     config_dict.get('harmonized'))
            granted_perms = tracker_bizobj.GetGrantedPerms(
                issue, effective_ids, config)
            may_view = permissions.CanViewIssue(effective_ids,
                                                perms,
                                                project,
                                                issue,
                                                granted_perms=granted_perms)

        if may_view:
            results.append(issue)

    return results
Example #8
0
def ComputeIssueChangeAddressPermList(
    cnxn, ids_to_consider, project, issue, services, omit_addrs,
    users_by_id, pref_check_function=lambda u: u.notify_issue_change):
  """Return a list of user email addresses to notify of an issue change.

  User email addresses are determined by looking up the given user IDs
  in the given users_by_id dict.

  Args:
    cnxn: connection to SQL database.
    ids_to_consider: list of user IDs for users interested in this issue.
    project: Project PB for the project containing this issue.
    issue: Issue PB for the issue that was updated.
    services: Services.
    omit_addrs: set of strings for email addresses to not notify because
        they already know.
    users_by_id: dict {user_id: user_view} user info.
    pref_check_function: optional function to use to check if a certain
        User PB has a preference set to receive the email being sent.  It
        defaults to "If I am in the issue's owner or cc field", but it
        can be set to check "If I starred the issue."

  Returns:
    A list of AddrPerm objects.
  """
  memb_addr_perm_list = []
  logging.info('Considering %r ', ids_to_consider)
  all_user_prefs = services.user.GetUsersPrefs(cnxn, ids_to_consider)
  for user_id in ids_to_consider:
    if user_id == framework_constants.NO_USER_SPECIFIED:
      continue
    user = services.user.GetUser(cnxn, user_id)
    # Notify people who have a pref set, or if they have no User PB
    # because the pref defaults to True.
    if user and not pref_check_function(user):
      logging.info('Not notifying %r: user preference', user.email)
      continue
    # TODO(jrobbins): doing a bulk operation would reduce DB load.
    auth = authdata.AuthData.FromUserID(cnxn, user_id, services)
    perms = permissions.GetPermissions(user, auth.effective_ids, project)
    config = services.config.GetProjectConfig(cnxn, project.project_id)
    granted_perms = tracker_bizobj.GetGrantedPerms(
        issue, auth.effective_ids, config)

    if not permissions.CanViewIssue(
        auth.effective_ids, perms, project, issue,
        granted_perms=granted_perms):
      logging.info('Not notifying %r: user cannot view issue', user.email)
      continue

    addr = users_by_id[user_id].email
    if addr in omit_addrs:
      logging.info('Not notifying %r: user already knows', user.email)
      continue

    recipient_is_member = bool(framework_bizobj.UserIsInProject(
        project, auth.effective_ids))

    reply_perm = REPLY_NOT_ALLOWED
    if project.process_inbound_email:
      if permissions.CanEditIssue(auth.effective_ids, perms, project, issue):
        reply_perm = REPLY_MAY_UPDATE
      elif permissions.CanCommentIssue(
          auth.effective_ids, perms, project, issue):
        reply_perm = REPLY_MAY_COMMENT

    memb_addr_perm_list.append(
      AddrPerm(recipient_is_member, addr, user, reply_perm,
               all_user_prefs[user_id]))

  logging.info('For %s %s, will notify: %r',
               project.project_name, issue.local_id,
               [ap.address for ap in memb_addr_perm_list])

  return memb_addr_perm_list
Example #9
0
  def HandleRequest(self, mr):
    """Process the task to notify users after an issue change.

    Args:
      mr: common information parsed from the HTTP request.

    Returns:
      Results dictionary in JSON format which is useful just for debugging.
      The main goal is the side-effect of sending emails.
    """
    issue_id = mr.GetPositiveIntParam('issue_id')
    if not issue_id:
      return {
          'params': {},
          'notified': [],
          'message': 'Cannot proceed without a valid issue ID.',
      }
    commenter_id = mr.GetPositiveIntParam('commenter_id')
    seq_num = mr.seq
    omit_ids = [commenter_id]
    hostport = mr.GetParam('hostport')
    old_owner_id = mr.GetPositiveIntParam('old_owner_id')
    send_email = bool(mr.GetIntParam('send_email'))
    comment_id = mr.GetPositiveIntParam('comment_id')
    params = dict(
        issue_id=issue_id, commenter_id=commenter_id,
        seq_num=seq_num, hostport=hostport, old_owner_id=old_owner_id,
        omit_ids=omit_ids, send_email=send_email, comment_id=comment_id)

    logging.info('issue change params are %r', params)
    # TODO(jrobbins): Re-enable the issue cache for notifications after
    # the stale issue defect (monorail:2514) is 100% resolved.
    issue = self.services.issue.GetIssue(mr.cnxn, issue_id, use_cache=False)
    project = self.services.project.GetProject(mr.cnxn, issue.project_id)
    config = self.services.config.GetProjectConfig(mr.cnxn, issue.project_id)

    if issue.is_spam:
      # Don't send email for spam issues.
      return {
          'params': params,
          'notified': [],
      }

    all_comments = self.services.issue.GetCommentsForIssue(
        mr.cnxn, issue.issue_id)
    if comment_id:
      logging.info('Looking up comment by comment_id')
      for c in all_comments:
        if c.id == comment_id:
          comment = c
          logging.info('Comment was found by comment_id')
          break
      else:
        raise ValueError('Comment %r was not found' % comment_id)
    else:
      logging.info('Looking up comment by seq_num')
      comment = all_comments[seq_num]

    # Only issues that any contributor could view sent to mailing lists.
    contributor_could_view = permissions.CanViewIssue(
        set(), permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET,
        project, issue)
    starrer_ids = self.services.issue_star.LookupItemStarrers(
        mr.cnxn, issue.issue_id)
    users_by_id = framework_views.MakeAllUserViews(
        mr.cnxn, self.services.user,
        tracker_bizobj.UsersInvolvedInIssues([issue]), [old_owner_id],
        tracker_bizobj.UsersInvolvedInComment(comment),
        issue.cc_ids, issue.derived_cc_ids, starrer_ids, omit_ids)

    # Make followup tasks to send emails
    tasks = []
    if send_email:
      tasks = self._MakeEmailTasks(
          mr.cnxn, project, issue, config, old_owner_id, users_by_id,
          all_comments, comment, starrer_ids, contributor_could_view,
          hostport, omit_ids, mr.perms)

    notified = notify_helpers.AddAllEmailTasks(tasks)

    return {
        'params': params,
        'notified': notified,
        }
Example #10
0
  def _BulkEditEmailTasks(
      self, cnxn, issues, old_owner_ids, omit_addrs, project,
      non_private_issues, users_by_id, ids_in_issues, starrers,
      commenter_view, hostport, comment_text, amendments, config):
    """Generate Email PBs to notify interested users after a bulk edit."""
    # 1. Get the user IDs of everyone who could be notified,
    # and make all their user proxies. Also, build a dictionary
    # of all the users to notify and the issues that they are
    # interested in.  Also, build a dictionary of additional email
    # addresses to notify and the issues to notify them of.
    users_by_id = {}
    ids_to_notify_of_issue = {}
    additional_addrs_to_notify_of_issue = collections.defaultdict(list)

    users_to_queries = notify_reasons.GetNonOmittedSubscriptions(
        cnxn, self.services, [project.project_id], {})
    config = self.services.config.GetProjectConfig(
        cnxn, project.project_id)
    for issue, old_owner_id in zip(issues, old_owner_ids):
      issue_participants = set(
          [tracker_bizobj.GetOwnerId(issue), old_owner_id] +
          tracker_bizobj.GetCcIds(issue))
      # users named in user-value fields that notify.
      for fd in config.field_defs:
        issue_participants.update(
            notify_reasons.ComputeNamedUserIDsToNotify(issue.field_values, fd))
      for user_id in ids_in_issues[issue.local_id]:
        # TODO(jrobbins): implement batch GetUser() for speed.
        if not user_id:
          continue
        auth = authdata.AuthData.FromUserID(
            cnxn, user_id, self.services)
        if (auth.user_pb.notify_issue_change and
            not auth.effective_ids.isdisjoint(issue_participants)):
          ids_to_notify_of_issue.setdefault(user_id, []).append(issue)
        elif (auth.user_pb.notify_starred_issue_change and
              user_id in starrers[issue.local_id]):
          # Skip users who have starred issues that they can no longer view.
          starrer_perms = permissions.GetPermissions(
              auth.user_pb, auth.effective_ids, project)
          granted_perms = tracker_bizobj.GetGrantedPerms(
              issue, auth.effective_ids, config)
          starrer_can_view = permissions.CanViewIssue(
              auth.effective_ids, starrer_perms, project, issue,
              granted_perms=granted_perms)
          if starrer_can_view:
            ids_to_notify_of_issue.setdefault(user_id, []).append(issue)
        logging.info(
            'ids_to_notify_of_issue[%s] = %s',
            user_id,
            [i.local_id for i in ids_to_notify_of_issue.get(user_id, [])])

      # Find all subscribers that should be notified.
      subscribers_to_consider = notify_reasons.EvaluateSubscriptions(
          cnxn, issue, users_to_queries, self.services, config)
      for sub_id in subscribers_to_consider:
        auth = authdata.AuthData.FromUserID(cnxn, sub_id, self.services)
        sub_perms = permissions.GetPermissions(
            auth.user_pb, auth.effective_ids, project)
        granted_perms = tracker_bizobj.GetGrantedPerms(
            issue, auth.effective_ids, config)
        sub_can_view = permissions.CanViewIssue(
            auth.effective_ids, sub_perms, project, issue,
            granted_perms=granted_perms)
        if sub_can_view:
          ids_to_notify_of_issue.setdefault(sub_id, [])
          if issue not in ids_to_notify_of_issue[sub_id]:
            ids_to_notify_of_issue[sub_id].append(issue)

      if issue in non_private_issues:
        for notify_addr in issue.derived_notify_addrs:
          additional_addrs_to_notify_of_issue[notify_addr].append(issue)

    # 2. Compose an email specifically for each user, and one email to each
    # notify_addr with all the issues that it.
    # Start from non-members first, then members to reveal email addresses.
    email_tasks = []
    needed_user_view_ids = [uid for uid in ids_to_notify_of_issue
                            if uid not in users_by_id]
    users_by_id.update(framework_views.MakeAllUserViews(
        cnxn, self.services.user, needed_user_view_ids))
    member_ids_to_notify_of_issue = {}
    non_member_ids_to_notify_of_issue = {}
    member_additional_addrs = {}
    non_member_additional_addrs = {}
    addr_to_addrperm = {}  # {email_address: AddrPerm object}
    all_user_prefs = self.services.user.GetUsersPrefs(
        cnxn, ids_to_notify_of_issue)

    # TODO(jrobbins): Merge ids_to_notify_of_issue entries for linked accounts.

    for user_id in ids_to_notify_of_issue:
      if not user_id:
        continue  # Don't try to notify NO_USER_SPECIFIED
      if users_by_id[user_id].email in omit_addrs:
        logging.info('Omitting %s', user_id)
        continue
      user_issues = ids_to_notify_of_issue[user_id]
      if not user_issues:
        continue  # user's prefs indicate they don't want these notifications
      auth = authdata.AuthData.FromUserID(
          cnxn, user_id, self.services)
      is_member = bool(framework_bizobj.UserIsInProject(
          project, auth.effective_ids))
      if is_member:
        member_ids_to_notify_of_issue[user_id] = user_issues
      else:
        non_member_ids_to_notify_of_issue[user_id] = user_issues
      addr = users_by_id[user_id].email
      omit_addrs.add(addr)
      addr_to_addrperm[addr] = notify_reasons.AddrPerm(
          is_member, addr, users_by_id[user_id].user,
          notify_reasons.REPLY_NOT_ALLOWED, all_user_prefs[user_id])

    for addr, addr_issues in additional_addrs_to_notify_of_issue.items():
      auth = None
      try:
        auth = authdata.AuthData.FromEmail(cnxn, addr, self.services)
      except:  # pylint: disable=bare-except
        logging.warning('Cannot find user of email %s ', addr)
      if auth:
        is_member = bool(framework_bizobj.UserIsInProject(
            project, auth.effective_ids))
      else:
        is_member = False
      if is_member:
        member_additional_addrs[addr] = addr_issues
      else:
        non_member_additional_addrs[addr] = addr_issues
      omit_addrs.add(addr)
      addr_to_addrperm[addr] = notify_reasons.AddrPerm(
          is_member, addr, None, notify_reasons.REPLY_NOT_ALLOWED, None)

    for user_id, user_issues in non_member_ids_to_notify_of_issue.items():
      addr = users_by_id[user_id].email
      email = self._FormatBulkIssuesEmail(
          addr_to_addrperm[addr], user_issues, users_by_id,
          commenter_view, hostport, comment_text, amendments, config, project)
      email_tasks.append(email)
      logging.info('about to bulk notify non-member %s (%s) of %s',
                   users_by_id[user_id].email, user_id,
                   [issue.local_id for issue in user_issues])

    for addr, addr_issues in non_member_additional_addrs.items():
      email = self._FormatBulkIssuesEmail(
          addr_to_addrperm[addr], addr_issues, users_by_id, commenter_view,
          hostport, comment_text, amendments, config, project)
      email_tasks.append(email)
      logging.info('about to bulk notify non-member additional addr %s of %s',
                   addr, [addr_issue.local_id for addr_issue in addr_issues])

    framework_views.RevealAllEmails(users_by_id)
    commenter_view.RevealEmail()

    for user_id, user_issues in member_ids_to_notify_of_issue.items():
      addr = users_by_id[user_id].email
      email = self._FormatBulkIssuesEmail(
          addr_to_addrperm[addr], user_issues, users_by_id,
          commenter_view, hostport, comment_text, amendments, config, project)
      email_tasks.append(email)
      logging.info('about to bulk notify member %s (%s) of %s',
                   addr, user_id, [issue.local_id for issue in user_issues])

    for addr, addr_issues in member_additional_addrs.items():
      email = self._FormatBulkIssuesEmail(
          addr_to_addrperm[addr], addr_issues, users_by_id, commenter_view,
          hostport, comment_text, amendments, config, project)
      email_tasks.append(email)
      logging.info('about to bulk notify member additional addr %s of %s',
                   addr, [addr_issue.local_id for addr_issue in addr_issues])

    # 4. Add in the project's issue_notify_address.  This happens even if it
    # is the same as the commenter's email address (which would be an unusual
    # but valid project configuration).  Only issues that any contributor could
    # view are included in emails to the all-issue-activity mailing lists.
    if (project.issue_notify_address
        and project.issue_notify_address not in omit_addrs):
      non_private_issues_live = []
      for issue in issues:
        contributor_could_view = permissions.CanViewIssue(
            set(), permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET,
            project, issue)
        if contributor_could_view:
          non_private_issues_live.append(issue)

      if non_private_issues_live:
        project_notify_addrperm = notify_reasons.AddrPerm(
            True, project.issue_notify_address, None,
            notify_reasons.REPLY_NOT_ALLOWED, None)
        email = self._FormatBulkIssuesEmail(
            project_notify_addrperm, non_private_issues_live,
            users_by_id, commenter_view, hostport, comment_text, amendments,
            config, project)
        email_tasks.append(email)
        omit_addrs.add(project.issue_notify_address)
        logging.info('about to bulk notify all-issues %s of %s',
                     project.issue_notify_address,
                     [issue.local_id for issue in non_private_issues])

    return email_tasks
Example #11
0
  def HandleRequest(self, mr):
    """Process the task to notify users after an issue blocking change.

    Args:
      mr: common information parsed from the HTTP request.

    Returns:
      Results dictionary in JSON format which is useful just for debugging.
      The main goal is the side-effect of sending emails.
    """
    issue_ids = mr.GetIntListParam('issue_ids')
    hostport = mr.GetParam('hostport')
    if not issue_ids:
      return {
          'params': {},
          'notified': [],
          'message': 'Cannot proceed without a valid issue IDs.',
      }

    old_owner_ids = mr.GetIntListParam('old_owner_ids')
    comment_text = mr.GetParam('comment_text')
    commenter_id = mr.GetPositiveIntParam('commenter_id')
    amendments = mr.GetParam('amendments')
    send_email = bool(mr.GetIntParam('send_email'))
    params = dict(
        issue_ids=issue_ids, commenter_id=commenter_id, hostport=hostport,
        old_owner_ids=old_owner_ids, comment_text=comment_text,
        send_email=send_email, amendments=amendments)

    logging.info('bulk edit params are %r', params)
    issues = self.services.issue.GetIssues(mr.cnxn, issue_ids)
    # TODO(jrobbins): For cross-project bulk edits, prefetch all relevant
    # projects and configs and pass a dict of them to subroutines.  For
    # now, all issue must be in the same project.
    project_id = issues[0].project_id
    project = self.services.project.GetProject(mr.cnxn, project_id)
    config = self.services.config.GetProjectConfig(mr.cnxn, project_id)
    issues = [issue for issue in issues if not issue.is_spam]
    anon_perms = permissions.GetPermissions(None, set(), project)

    users_by_id = framework_views.MakeAllUserViews(
        mr.cnxn, self.services.user, [commenter_id])
    ids_in_issues = {}
    starrers = {}

    non_private_issues = []
    for issue, old_owner_id in zip(issues, old_owner_ids):
      # TODO(jrobbins): use issue_id consistently rather than local_id.
      starrers[issue.local_id] = self.services.issue_star.LookupItemStarrers(
          mr.cnxn, issue.issue_id)
      named_ids = set()  # users named in user-value fields that notify.
      for fd in config.field_defs:
        named_ids.update(notify_reasons.ComputeNamedUserIDsToNotify(
            issue.field_values, fd))
      direct, indirect = self.services.usergroup.ExpandAnyUserGroups(
          mr.cnxn, list(issue.cc_ids) + list(issue.derived_cc_ids) +
          [issue.owner_id, old_owner_id, issue.derived_owner_id] +
          list(named_ids))
      ids_in_issues[issue.local_id] = set(starrers[issue.local_id])
      ids_in_issues[issue.local_id].update(direct)
      ids_in_issues[issue.local_id].update(indirect)
      ids_in_issue_needing_views = (
          ids_in_issues[issue.local_id] |
          tracker_bizobj.UsersInvolvedInIssues([issue]))
      new_ids_in_issue = [user_id for user_id in ids_in_issue_needing_views
                          if user_id not in users_by_id]
      users_by_id.update(
          framework_views.MakeAllUserViews(
              mr.cnxn, self.services.user, new_ids_in_issue))

      anon_can_view = permissions.CanViewIssue(
          set(), anon_perms, project, issue)
      if anon_can_view:
        non_private_issues.append(issue)

    commenter_view = users_by_id[commenter_id]
    omit_addrs = {commenter_view.email}

    tasks = []
    if send_email:
      email_tasks = self._BulkEditEmailTasks(
          mr.cnxn, issues, old_owner_ids, omit_addrs, project,
          non_private_issues, users_by_id, ids_in_issues, starrers,
          commenter_view, hostport, comment_text, amendments, config)
      tasks = email_tasks

    notified = notify_helpers.AddAllEmailTasks(tasks)
    return {
        'params': params,
        'notified': notified,
        }
Example #12
0
def api_base_checks(request, requester, services, cnxn,
                    auth_client_ids, auth_emails):
  """Base checks for API users.

  Args:
    request: The HTTP request from Cloud Endpoints.
    requester: The user who sends the request.
    services: Services object.
    cnxn: connection to the SQL database.
    auth_client_ids: authorized client ids.
    auth_emails: authorized emails when client is anonymous.

  Returns:
    Client ID and client email.

  Raises:
    endpoints.UnauthorizedException: If the requester is anonymous.
    user_svc.NoSuchUserException: If the requester does not exist in Monorail.
    project_svc.NoSuchProjectException: If the project does not exist in
        Monorail.
    permissions.BannedUserException: If the requester is banned.
    permissions.PermissionException: If the requester does not have
        permisssion to view.
  """
  valid_user = False
  auth_err = ''
  client_id = None

  try:
    client_id = oauth.get_client_id(framework_constants.OAUTH_SCOPE)
    logging.info('Oauth client ID %s', client_id)
  except oauth.Error as ex:
    auth_err = 'oauth.Error: %s' % ex

  if not requester:
    try:
      requester = oauth.get_current_user(framework_constants.OAUTH_SCOPE)
      logging.info('Oauth requester %s', requester.email())
    except oauth.Error as ex:
      auth_err = 'oauth.Error: %s' % ex

  if client_id and requester:
    if client_id != 'anonymous':
      if client_id in auth_client_ids:
        valid_user = True
      else:
        auth_err = 'Client ID %s is not whitelisted' % client_id
    # Some service accounts may have anonymous client ID
    else:
      if requester.email() in auth_emails:
        valid_user = True
      else:
        auth_err = 'Client email %s is not whitelisted' % requester.email()

  if not valid_user:
    raise endpoints.UnauthorizedException('Auth error: %s' % auth_err)
  else:
    logging.info('API request from user %s:%s', client_id, requester.email())

  project_name = None
  if hasattr(request, 'projectId'):
    project_name = request.projectId
  issue_local_id = None
  if hasattr(request, 'issueId'):
    issue_local_id = request.issueId
  # This could raise user_svc.NoSuchUserException
  requester_id = services.user.LookupUserID(cnxn, requester.email())
  requester_pb = services.user.GetUser(cnxn, requester_id)
  requester_view = framework_views.UserView(requester_pb)
  if permissions.IsBanned(requester_pb, requester_view):
    raise permissions.BannedUserException(
        'The user %s has been banned from using Monorail' %
        requester.email())
  if project_name:
    project = services.project.GetProjectByName(
        cnxn, project_name)
    if not project:
      raise project_svc.NoSuchProjectException(
          'Project %s does not exist' % project_name)
    if project.state != project_pb2.ProjectState.LIVE:
      raise permissions.PermissionException(
          'API may not access project %s because it is not live'
          % project_name)
    requester_effective_ids = services.usergroup.LookupMemberships(
        cnxn, requester_id)
    requester_effective_ids.add(requester_id)
    if not permissions.UserCanViewProject(
        requester_pb, requester_effective_ids, project):
      raise permissions.PermissionException(
          'The user %s has no permission for project %s' %
          (requester.email(), project_name))
    if issue_local_id:
      # This may raise a NoSuchIssueException.
      issue = services.issue.GetIssueByLocalID(
          cnxn, project.project_id, issue_local_id)
      perms = permissions.GetPermissions(
          requester_pb, requester_effective_ids, project)
      config = services.config.GetProjectConfig(cnxn, project.project_id)
      granted_perms = tracker_bizobj.GetGrantedPerms(
          issue, requester_effective_ids, config)
      if not permissions.CanViewIssue(
          requester_effective_ids, perms, project, issue,
          granted_perms=granted_perms):
        raise permissions.PermissionException(
            'User is not allowed to view this issue %s:%d' %
            (project_name, issue_local_id))

  return client_id, requester.email()
Example #13
0
    def _MakeEmailTasks(self, cnxn, issue, project, config, comment,
                        starrer_ids, hostport, users_by_id, pings):
        """Return a list of dicts for tasks to notify people."""
        detail_url = framework_helpers.IssueCommentURL(
            hostport, project, issue.local_id, seq_num=comment.sequence)
        fields = sorted((field_def for (field_def, _date_value) in pings),
                        key=lambda fd: fd.field_name)
        email_data = {
            'issue': tracker_views.IssueView(issue, users_by_id, config),
            'summary': issue.summary,
            'ping_comment_content': comment.content,
            'detail_url': detail_url,
            'fields': fields,
        }

        # Generate three versions of email body with progressively more info.
        body_link_only = self.link_only_email_template.GetResponse({
            'detail_url':
            detail_url,
            'was_created':
            ezt.boolean(False)
        })
        body_for_non_members = self.email_template.GetResponse(email_data)
        framework_views.RevealAllEmails(users_by_id)
        body_for_members = self.email_template.GetResponse(email_data)
        logging.info('body for non-members is:\n%r' % body_for_non_members)
        logging.info('body for members is:\n%r' % body_for_members)

        contributor_could_view = permissions.CanViewIssue(
            set(), permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET, project,
            issue)

        group_reason_list = notify_reasons.ComputeGroupReasonList(
            cnxn,
            self.services,
            project,
            issue,
            config,
            users_by_id, [],
            contributor_could_view,
            starrer_ids=starrer_ids,
            commenter_in_project=True,
            include_subscribers=False,
            include_notify_all=False,
            starrer_pref_check_function=lambda u: u.notify_starred_ping)

        commenter_view = users_by_id[comment.user_id]
        email_tasks = notify_helpers.MakeBulletedEmailWorkItems(
            group_reason_list,
            issue,
            body_link_only,
            body_for_non_members,
            body_for_members,
            project,
            hostport,
            commenter_view,
            detail_url,
            seq_num=comment.sequence,
            subject_prefix='Follow up on issue ',
            compact_subject_prefix='Follow up ')

        return email_tasks