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
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
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
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
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')
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')
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
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
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, }
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
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, }
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()
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