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 PaginateComments(mr, issue, issuecomment_list, config): """Filter and paginate the IssueComment PBs for the given issue. Unlike most pagination, this one starts at the end of the whole list so it shows only the most recent comments. The user can use the "Older" and "Newer" links to page through older comments. Args: mr: common info parsed from the HTTP request. issue: Issue PB for the issue being viewed. issuecomment_list: list of IssueComment PBs for the viewed issue, the zeroth item in this list is the initial issue description. config: ProjectIssueConfig for the project that contains this issue. Returns: A tuple (descriptions, visible_comments, pagination), where descriptions is a list of IssueComment PBs for the issue description history, visible_comments is a list of IssueComment PBs for the comments that should be displayed on the current pagination page, and pagination is a VirtualPagination object that keeps track of the Older and Newer links. """ if not issuecomment_list: return [], [], None # TODO(lukasperaza): update first comments' rows to is_description=TRUE # so [issuecomment_list[0]] can be removed descriptions = ( [issuecomment_list[0]] + [comment for comment in issuecomment_list[1:] if comment.is_description]) comments = issuecomment_list[1:] allowed_comments = [] restrictions = permissions.GetRestrictions(issue) granted_perms = tracker_bizobj.GetGrantedPerms( issue, mr.auth.effective_ids, config) for c in comments: can_delete = permissions.CanDelete( mr.auth.user_id, mr.auth.effective_ids, mr.perms, c.deleted_by, c.user_id, mr.project, restrictions, granted_perms=granted_perms) if can_delete or not c.deleted_by: allowed_comments.append(c) pagination_url = '%s?id=%d' % (urls.ISSUE_DETAIL, issue.local_id) pagination = paginate.VirtualPagination( mr, len(allowed_comments), framework_constants.DEFAULT_COMMENTS_PER_PAGE, list_page_url=pagination_url, count_up=False, start_param='cstart', num_param='cnum', max_num=settings.max_comments_per_page) if pagination.last == 1 and pagination.start == len(allowed_comments): pagination.visible = ezt.boolean(False) visible_comments = allowed_comments[ pagination.last - 1:pagination.start] return descriptions, visible_comments, pagination
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 IsMergeAllowed(merge_into_issue, mr, services): """Check to see if user has permission to merge with specified issue.""" merge_into_project = services.project.GetProjectByName( mr.cnxn, merge_into_issue.project_name) merge_into_config = services.config.GetProjectConfig( mr.cnxn, merge_into_project.project_id) merge_granted_perms = tracker_bizobj.GetGrantedPerms( merge_into_issue, mr.auth.effective_ids, merge_into_config) merge_view_allowed = mr.perms.CanUsePerm( permissions.VIEW, mr.auth.effective_ids, merge_into_project, permissions.GetRestrictions(merge_into_issue), granted_perms=merge_granted_perms) merge_edit_allowed = mr.perms.CanUsePerm( permissions.EDIT_ISSUE, mr.auth.effective_ids, merge_into_project, permissions.GetRestrictions(merge_into_issue), granted_perms=merge_granted_perms) return merge_view_allowed and merge_edit_allowed
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, project_dict.values()) denied_project_ids = { pid for pid, p in project_dict.iteritems() 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.CanViewRestrictedIssueInVisibleProject( effective_ids, perms, project, issue, granted_perms=granted_perms) if may_view: results.append(issue) return results
def __init__(self, request, services, cnxn=None): requester_object = (endpoints.get_current_user() or oauth.get_current_user( framework_constants.OAUTH_SCOPE)) requester = requester_object.email().lower() super(MonorailApiRequest, self).__init__(services, requester=requester, cnxn=cnxn) self.me_user_id = self.auth.user_id self.viewed_username = None self.viewed_user_auth = None self.issue = None self.granted_perms = set() # query parameters self.params = { 'can': 1, 'start': 0, 'num': tracker_constants.DEFAULT_RESULTS_PER_PAGE, 'q': '', 'sort': '', 'groupby': '', 'projects': [], 'hotlists': [] } self.use_cached_searches = True self.mode = None if hasattr(request, 'projectId'): self.project_name = request.projectId with work_env.WorkEnv(self, services) as we: self.project = we.GetProjectByName(self.project_name) self.params['projects'].append(self.project_name) self.config = we.GetProjectConfig(self.project_id) if hasattr(request, 'additionalProject'): self.params['projects'].extend(request.additionalProject) self.params['projects'] = list(set( self.params['projects'])) self.LookupLoggedInUserPerms(self.project) if hasattr(request, 'projectId'): with work_env.WorkEnv(self, services) as we: if hasattr(request, 'issueId'): self.issue = we.GetIssueByLocalID(self.project_id, request.issueId, use_cache=False) self.granted_perms = tracker_bizobj.GetGrantedPerms( self.issue, self.auth.effective_ids, self.config) if hasattr(request, 'userId'): self.viewed_username = request.userId.lower() if self.viewed_username == 'me': self.viewed_username = requester self.viewed_user_auth = authdata.AuthData.FromEmail( self.cnxn, self.viewed_username, services) elif hasattr(request, 'groupName'): self.viewed_username = request.groupName.lower() try: self.viewed_user_auth = authdata.AuthData.FromEmail( self.cnxn, self.viewed_username, services) except exceptions.NoSuchUserException: self.viewed_user_auth = None # Build q. if hasattr(request, 'q') and request.q: self.params['q'] = request.q if hasattr(request, 'publishedMax') and request.publishedMax: self.params['q'] += ' opened<=%d' % request.publishedMax if hasattr(request, 'publishedMin') and request.publishedMin: self.params['q'] += ' opened>=%d' % request.publishedMin if hasattr(request, 'updatedMax') and request.updatedMax: self.params['q'] += ' modified<=%d' % request.updatedMax if hasattr(request, 'updatedMin') and request.updatedMin: self.params['q'] += ' modified>=%d' % request.updatedMin if hasattr(request, 'owner') and request.owner: self.params['q'] += ' owner:%s' % request.owner if hasattr(request, 'status') and request.status: self.params['q'] += ' status:%s' % request.status if hasattr(request, 'label') and request.label: self.params['q'] += ' label:%s' % request.label if hasattr(request, 'can') and request.can: if request.can == api_pb2_v1.CannedQuery.all: self.params['can'] = 1 elif request.can == api_pb2_v1.CannedQuery.new: self.params['can'] = 6 elif request.can == api_pb2_v1.CannedQuery.open: self.params['can'] = 2 elif request.can == api_pb2_v1.CannedQuery.owned: self.params['can'] = 3 elif request.can == api_pb2_v1.CannedQuery.reported: self.params['can'] = 4 elif request.can == api_pb2_v1.CannedQuery.starred: self.params['can'] = 5 elif request.can == api_pb2_v1.CannedQuery.to_verify: self.params['can'] = 7 else: # Endpoints should have caught this. raise exceptions.InputException( 'Canned query %s is not supported.', request.can) if hasattr(request, 'startIndex') and request.startIndex: self.params['start'] = request.startIndex if hasattr(request, 'maxResults') and request.maxResults: self.params['num'] = request.maxResults if hasattr(request, 'sort') and request.sort: self.params['sort'] = request.sort self.query_project_names = self.GetParam('projects') self.group_by_spec = self.GetParam('groupby') self.group_by_spec = ' '.join( ParseColSpec(self.group_by_spec, ignore=tracker_constants.NOT_USED_IN_GRID_AXES)) self.sort_spec = self.GetParam('sort') self.sort_spec = ' '.join(ParseColSpec(self.sort_spec)) self.query = self.GetParam('q') self.can = self.GetParam('can') self.start = self.GetParam('start') self.num = self.GetParam('num')
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 UpdateIssuePermissions( perms, project, issue, effective_ids, granted_perms=None, config=None): """Update the PermissionSet for an specific issue. Take into account granted permissions and label restrictions to filter the permissions, and updates the VIEW and EDIT_ISSUE permissions depending on the role of the user in the issue (i.e. owner, reporter, cc or approver). Args: perms: The PermissionSet to update. project: The Project PB for the issue project. issue: The Issue PB. effective_ids: Set of int user IDs for the current user and all user groups that s/he is a member of. This will be an empty set for anonymous users. granted_perms: optional list of strings of permissions that the user is granted only within the scope of one issue, e.g., by being named in a user-type custom field that grants permissions. config: optional ProjectIssueConfig PB where granted perms should be extracted from, if granted_perms is not given. """ if config: granted_perms = tracker_bizobj.GetGrantedPerms( issue, effective_ids, config) elif granted_perms is None: granted_perms = [] # If the user has no permission to view the project, it has no permissions on # this issue. if not perms.HasPerm(VIEW, None, None): return EMPTY_PERMISSIONSET # Compute the restrictions for the given issue and store them in a dictionary # of {perm: set(needed_perms)}. restrictions = collections.defaultdict(set) if perms.consider_restrictions: for label in GetRestrictions(issue): label = label.lower() # format: Restrict-Action-ToThisPerm _, requested_perm, needed_perm = label.split('-', 2) restrictions[requested_perm.lower()].add(needed_perm.lower()) # Store the user permissions, and the extra permissions of all effective IDs # in the given project. all_perms = set(perms.perm_names) for effective_id in effective_ids: all_perms.update(p.lower() for p in GetExtraPerms(project, effective_id)) # And filter them applying the restriction labels. filtered_perms = set() for perm_name in all_perms: perm_name = perm_name.lower() restricted = any( restriction not in all_perms and restriction not in granted_perms for restriction in restrictions.get(perm_name, [])) if not restricted: filtered_perms.add(perm_name) # Add any granted permissions. filtered_perms.update(granted_perms) # The VIEW perm might have been removed due to restrictions, but the issue # owner, reporter, cc and approvers can always be an issue. allowed_ids = set( tracker_bizobj.GetCcIds(issue) + tracker_bizobj.GetApproverIds(issue) + [issue.reporter_id, tracker_bizobj.GetOwnerId(issue)]) if effective_ids and not allowed_ids.isdisjoint(effective_ids): filtered_perms.add(VIEW.lower()) # If the issue is deleted, only the VIEW and DELETE_ISSUE permissions are # relevant. if issue.deleted: if VIEW.lower() not in filtered_perms: return EMPTY_PERMISSIONSET if DELETE_ISSUE.lower() in filtered_perms: return PermissionSet([VIEW, DELETE_ISSUE], perms.consider_restrictions) return PermissionSet([VIEW], perms.consider_restrictions) # The EDIT_ISSUE permission might have been removed due to restrictions, but # the owner has always permission to edit it. if effective_ids and tracker_bizobj.GetOwnerId(issue) in effective_ids: filtered_perms.add(EDIT_ISSUE.lower()) return PermissionSet(filtered_perms, perms.consider_restrictions)
def __init__(self, request, services): requester = (endpoints.get_current_user() or oauth.get_current_user( framework_constants.OAUTH_SCOPE)) requester_email = requester.email().lower() self.cnxn = sql.MonorailConnection() self.auth = AuthData.FromEmail(self.cnxn, requester_email, services) self.me_user_id = self.auth.user_id self.viewed_username = None self.viewed_user_auth = None self.project_name = None self.project = None self.issue = None self.config = None self.granted_perms = set() # query parameters self.params = { 'can': 1, 'start': 0, 'num': 100, 'q': '', 'sort': '', 'groupby': '', 'projects': [], 'hotlists': [] } self.use_cached_searches = True self.warnings = [] self.errors = template_helpers.EZTError() self.mode = None if hasattr(request, 'projectId'): self.project_name = request.projectId self.project = services.project.GetProjectByName( self.cnxn, self.project_name) self.params['projects'].append(self.project_name) self.config = services.config.GetProjectConfig( self.cnxn, self.project_id) if hasattr(request, 'additionalProject'): self.params['projects'].extend(request.additionalProject) self.params['projects'] = list(set(self.params['projects'])) if hasattr(request, 'issueId'): self.issue = services.issue.GetIssueByLocalID( self.cnxn, self.project_id, request.issueId) self.granted_perms = tracker_bizobj.GetGrantedPerms( self.issue, self.auth.effective_ids, self.config) if hasattr(request, 'userId'): self.viewed_username = request.userId.lower() if self.viewed_username == 'me': self.viewed_username = requester_email self.viewed_user_auth = AuthData.FromEmail(self.cnxn, self.viewed_username, services) elif hasattr(request, 'groupName'): self.viewed_username = request.groupName.lower() try: self.viewed_user_auth = AuthData.FromEmail( self.cnxn, self.viewed_username, services) except user_svc.NoSuchUserException: self.viewed_user_auth = None self.perms = permissions.GetPermissions(self.auth.user_pb, self.auth.effective_ids, self.project) # Build q. if hasattr(request, 'q') and request.q: self.params['q'] = request.q if hasattr(request, 'publishedMax') and request.publishedMax: self.params['q'] += ' opened<=%d' % request.publishedMax if hasattr(request, 'publishedMin') and request.publishedMin: self.params['q'] += ' opened>=%d' % request.publishedMin if hasattr(request, 'updatedMax') and request.updatedMax: self.params['q'] += ' modified<=%d' % request.updatedMax if hasattr(request, 'updatedMin') and request.updatedMin: self.params['q'] += ' modified>=%d' % request.updatedMin if hasattr(request, 'owner') and request.owner: self.params['q'] += ' owner:%s' % request.owner if hasattr(request, 'status') and request.status: self.params['q'] += ' status:%s' % request.status if hasattr(request, 'label') and request.label: self.params['q'] += ' label:%s' % request.label if hasattr(request, 'can') and request.can: if request.can == api_pb2_v1.CannedQuery.all: self.params['can'] = 1 elif request.can == api_pb2_v1.CannedQuery.new: self.params['can'] = 6 elif request.can == api_pb2_v1.CannedQuery.open: self.params['can'] = 2 elif request.can == api_pb2_v1.CannedQuery.owned: self.params['can'] = 3 elif request.can == api_pb2_v1.CannedQuery.reported: self.params['can'] = 4 elif request.can == api_pb2_v1.CannedQuery.starred: self.params['can'] = 5 elif request.can == api_pb2_v1.CannedQuery.to_verify: self.params['can'] = 7 else: # Endpoints should have caught this. raise InputException('Canned query %s is not supported.', request.can) if hasattr(request, 'startIndex') and request.startIndex: self.params['start'] = request.startIndex if hasattr(request, 'maxResults') and request.maxResults: self.params['num'] = request.maxResults if hasattr(request, 'sort') and request.sort: self.params['sort'] = request.sort self.query_project_names = self.GetParam('projects') self.group_by_spec = self.GetParam('groupby') self.sort_spec = self.GetParam('sort') self.query = self.GetParam('q') self.can = self.GetParam('can') self.start = self.GetParam('start') self.num = self.GetParam('num')
def convert_issue(cls, issue, mar, services): """Convert Monorail Issue PB to API IssuesGetInsertResponse.""" config = services.config.GetProjectConfig(mar.cnxn, issue.project_id) granted_perms = tracker_bizobj.GetGrantedPerms(issue, mar.auth.effective_ids, config) issue_project = services.project.GetProject(mar.cnxn, issue.project_id) component_list = [] for cd in config.component_defs: cid = cd.component_id if cid in issue.component_ids: component_list.append(cd.path) cc_list = [convert_person(p, mar.cnxn, services) for p in issue.cc_ids] cc_list = [p for p in cc_list if p is not None] field_values_list = [] fds_by_id = {fd.field_id: fd for fd in config.field_defs} phases_by_id = {phase.phase_id: phase for phase in issue.phases} for fv in issue.field_values: fd = fds_by_id.get(fv.field_id) if not fd: logging.warning('Custom field %d of project %s does not exist', fv.field_id, issue_project.project_name) continue val = None if fv.user_id: val = _get_user_email(services.user, mar.cnxn, fv.user_id) else: val = tracker_bizobj.GetFieldValue(fv, {}) if not isinstance(val, string_types): val = str(val) new_fv = api_pb2_v1.FieldValue(fieldName=fd.field_name, fieldValue=val, derived=fv.derived) if fd.approval_id: # Attach parent approval name approval_fd = fds_by_id.get(fd.approval_id) if not approval_fd: logging.warning( 'Parent approval field %d of field %s does not exist', fd.approval_id, fd.field_name) else: new_fv.approvalName = approval_fd.field_name elif fv.phase_id: # Attach phase name phase = phases_by_id.get(fv.phase_id) if not phase: logging.warning('Phase %d for field %s does not exist', fv.phase_id, fd.field_name) else: new_fv.phaseName = phase.name field_values_list.append(new_fv) approval_values_list = convert_approvals(mar.cnxn, issue.approval_values, services, config, issue.phases) phases_list = convert_phases(issue.phases) with work_env.WorkEnv(mar, services) as we: starred = we.IsIssueStarred(issue) resp = cls( kind='monorail#issue', id=issue.local_id, title=issue.summary, summary=issue.summary, projectId=issue_project.project_name, stars=issue.star_count, starred=starred, status=issue.status, state=(api_pb2_v1.IssueState.open if tracker_helpers.MeansOpenInProject( tracker_bizobj.GetStatus(issue), config) else api_pb2_v1.IssueState.closed), labels=issue.labels, components=component_list, author=convert_person(issue.reporter_id, mar.cnxn, services), owner=convert_person(issue.owner_id, mar.cnxn, services), cc=cc_list, updated=datetime.datetime.fromtimestamp(issue.modified_timestamp), published=datetime.datetime.fromtimestamp(issue.opened_timestamp), blockedOn=convert_issue_ids(issue.blocked_on_iids, mar, services), blocking=convert_issue_ids(issue.blocking_iids, mar, services), canComment=permissions.CanCommentIssue(mar.auth.effective_ids, mar.perms, issue_project, issue, granted_perms=granted_perms), canEdit=permissions.CanEditIssue(mar.auth.effective_ids, mar.perms, issue_project, issue, granted_perms=granted_perms), fieldValues=field_values_list, approvalValues=approval_values_list, phases=phases_list) if issue.closed_timestamp > 0: resp.closed = datetime.datetime.fromtimestamp(issue.closed_timestamp) if issue.merged_into: resp.mergedInto = convert_issue_ids([issue.merged_into], mar, services)[0] if issue.owner_modified_timestamp: resp.owner_modified = datetime.datetime.fromtimestamp( issue.owner_modified_timestamp) if issue.status_modified_timestamp: resp.status_modified = datetime.datetime.fromtimestamp( issue.status_modified_timestamp) if issue.component_modified_timestamp: resp.component_modified = datetime.datetime.fromtimestamp( issue.component_modified_timestamp) return resp
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 convert_issue(cls, issue, mar, services): """Convert Monorail Issue PB to API IssuesGetInsertResponse.""" config = services.config.GetProjectConfig(mar.cnxn, issue.project_id) granted_perms = tracker_bizobj.GetGrantedPerms( issue, mar.auth.effective_ids, config) issue_project = services.project.GetProject(mar.cnxn, issue.project_id) component_list = [] for cd in config.component_defs: cid = cd.component_id if cid in issue.component_ids: component_list.append(cd.path) cc_list = [convert_person(p, mar.cnxn, services) for p in issue.cc_ids] cc_list = [p for p in cc_list if p is not None] field_values_list = [] field_id_dict = { fd.field_id: fd.field_name for fd in config.field_defs} for fv in issue.field_values: field_name = field_id_dict.get(fv.field_id) if not field_name: logging.warning('Custom field %d of project %s does not exist', fv.field_id, issue_project.project_name) continue val = None if fv.user_id: val = _get_user_email( services.user, mar.cnxn, fv.user_id) elif fv.str_value: val = fv.str_value elif fv.int_value: val = str(fv.int_value) new_fv = api_pb2_v1.FieldValue( fieldName=field_name, fieldValue=val, derived=fv.derived) field_values_list.append(new_fv) resp = cls( kind='monorail#issue', id=issue.local_id, title=issue.summary, summary=issue.summary, projectId=issue_project.project_name, stars=issue.star_count, starred=services.issue_star.IsItemStarredBy( mar.cnxn, issue.issue_id, mar.auth.user_id), status=issue.status, state=(api_pb2_v1.IssueState.open if tracker_helpers.MeansOpenInProject( tracker_bizobj.GetStatus(issue), config) else api_pb2_v1.IssueState.closed), labels=issue.labels, components=component_list, author=convert_person(issue.reporter_id, mar.cnxn, services), owner=convert_person(issue.owner_id, mar.cnxn, services), cc=cc_list, updated=datetime.datetime.fromtimestamp(issue.modified_timestamp), published=datetime.datetime.fromtimestamp(issue.opened_timestamp), blockedOn=convert_issue_ids(issue.blocked_on_iids, mar, services), blocking=convert_issue_ids(issue.blocking_iids, mar, services), canComment=permissions.CanCommentIssue( mar.auth.effective_ids, mar.perms, issue_project, issue, granted_perms=granted_perms), canEdit=permissions.CanEditIssue( mar.auth.effective_ids, mar.perms, issue_project, issue, granted_perms=granted_perms), fieldValues=field_values_list) if issue.closed_timestamp > 0: resp.closed = datetime.datetime.fromtimestamp(issue.closed_timestamp) if issue.merged_into: resp.mergedInto=convert_issue_ids([issue.merged_into], mar, services)[0] if issue.owner_modified_timestamp: resp.owner_modified = datetime.datetime.fromtimestamp( issue.owner_modified_timestamp) if issue.status_modified_timestamp: resp.status_modified = datetime.datetime.fromtimestamp( issue.status_modified_timestamp) if issue.component_modified_timestamp: resp.component_modified = datetime.datetime.fromtimestamp( issue.component_modified_timestamp) return resp
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 GatherPageData(self, mr): """Parse the attachment ID from the request and serve its content. Args: mr: commonly used info parsed from the request. Returns: Dict of values used by EZT for rendering almost the page. """ with mr.profiler.Phase('get issue, comment, and attachment'): try: attachment, issue = tracker_helpers.GetAttachmentIfAllowed( mr, self.services) except exceptions.NoSuchIssueException: webapp2.abort(404, 'issue not found') except exceptions.NoSuchAttachmentException: webapp2.abort(404, 'attachment not found') except exceptions.NoSuchCommentException: webapp2.abort(404, 'comment not found') content = [] if attachment.gcs_object_id: bucket_name = app_identity.get_default_gcs_bucket_name() full_path = '/' + bucket_name + attachment.gcs_object_id logging.info("reading gcs: %s" % full_path) with cloudstorage.open(full_path, 'r') as f: content = f.read() filesize = len(content) # This servlet only displays safe textual attachments. The user should # not have been given a link to this servlet for any other kind. if not attachment_helpers.IsViewableText(attachment.mimetype, filesize): self.abort(400, 'not a text file') u_text, is_binary, too_large = filecontent.DecodeFileContents(content) lines = prettify.PrepareSourceLinesForHighlighting( u_text.encode('utf8')) config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id) granted_perms = tracker_bizobj.GetGrantedPerms(issue, mr.auth.effective_ids, config) page_perms = self.MakePagePerms(mr, issue, permissions.DELETE_ISSUE, permissions.CREATE_ISSUE, granted_perms=granted_perms) page_data = { 'issue_tab_mode': 'issueDetail', 'local_id': issue.local_id, 'filename': attachment.filename, 'filesize': template_helpers.BytesKbOrMb(filesize), 'file_lines': lines, 'is_binary': ezt.boolean(is_binary), 'too_large': ezt.boolean(too_large), 'code_reviews': None, 'page_perms': page_perms, } if is_binary or too_large: page_data['should_prettify'] = ezt.boolean(False) else: page_data.update( prettify.BuildPrettifyData(len(lines), attachment.filename)) return page_data