| # Copyright 2016 The Chromium Authors. All rights reserved. |
| # Use of this source code is govered by a BSD-style |
| # license that can be found in the LICENSE file or at |
| # https://developers.google.com/open-source/licenses/bsd |
| |
| """JSON feed for issue autocomplete options.""" |
| |
| import logging |
| from third_party import ezt |
| |
| from framework import framework_helpers |
| from framework import framework_views |
| from framework import jsonfeed |
| from framework import monorailrequest |
| from framework import permissions |
| from project import project_helpers |
| from tracker import tracker_helpers |
| from tracker import tracker_views |
| |
| |
| # Here are some restriction labels to help people do the most common things |
| # that they might want to do with restrictions. |
| _FREQUENT_ISSUE_RESTRICTIONS = [ |
| (permissions.VIEW, permissions.EDIT_ISSUE, |
| 'Only users who can edit the issue may access it'), |
| (permissions.ADD_ISSUE_COMMENT, permissions.EDIT_ISSUE, |
| 'Only users who can edit the issue may add comments'), |
| ] |
| |
| |
| # These issue restrictions should be offered as examples whenever the project |
| # does not have any custom permissions in use already. |
| _EXAMPLE_ISSUE_RESTRICTIONS = [ |
| (permissions.VIEW, 'CoreTeam', |
| 'Custom permission CoreTeam is needed to access'), |
| ] |
| |
| |
| class IssueOptionsJSON(jsonfeed.JsonFeed): |
| """JSON data describing all issue statuses, labels, and members.""" |
| |
| def HandleRequest(self, mr): |
| """Provide the UI with info used in auto-completion. |
| |
| Args: |
| mr: common information parsed from the HTTP request. |
| |
| Returns: |
| Results dictionary in JSON format |
| """ |
| # Issue options data can be cached separately in each user's browser. When |
| # the project changes, a new cached_content_timestamp is set and it will |
| # cause new requests to use a new URL. |
| self.SetCacheHeaders(self.response) |
| |
| member_data = project_helpers.BuildProjectMembers( |
| mr.cnxn, mr.project, self.services.user) |
| owner_views = member_data['owners'] |
| committer_views = member_data['committers'] |
| contributor_views = member_data['contributors'] |
| |
| config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id) |
| |
| open_statuses = [] |
| closed_statuses = [] |
| for wks in config.well_known_statuses: |
| if not wks.deprecated: |
| item = dict(name=wks.status, doc=wks.status_docstring) |
| if wks.means_open: |
| open_statuses.append(item) |
| else: |
| closed_statuses.append(item) |
| |
| # TODO(jrobbins): restrictions on component definitions? |
| components = [{'name': cd.path, 'doc': cd.docstring} |
| for cd in config.component_defs if not cd.deprecated] |
| |
| labels = [] |
| field_names = [ |
| fd.field_name for fd in config.field_defs if not fd.is_deleted] |
| non_masked_labels = tracker_helpers.LabelsNotMaskedByFields( |
| config, field_names) |
| for wkl in non_masked_labels: |
| if not wkl.commented: |
| item = dict(name=wkl.name, doc=wkl.docstring) |
| labels.append(item) |
| |
| # TODO(jrobbins): omit fields that they don't have permission to view. |
| field_def_views = [ |
| tracker_views.FieldDefView(fd, config) |
| for fd in config.field_defs |
| if not fd.is_deleted] |
| fields = [ |
| dict(field_name=fdv.field_name, field_type=fdv.field_type, |
| field_id=fdv.field_id, needs_perm=fdv.needs_perm, |
| is_required=fdv.is_required, is_multivalued=fdv.is_multivalued, |
| choices=[dict(name=c.name, doc=c.docstring) for c in fdv.choices], |
| docstring=fdv.docstring) |
| for fdv in field_def_views] |
| |
| frequent_restrictions = _FREQUENT_ISSUE_RESTRICTIONS[:] |
| custom_permissions = permissions.GetCustomPermissions(mr.project) |
| if not custom_permissions: |
| frequent_restrictions.extend( |
| _EXAMPLE_ISSUE_RESTRICTIONS) |
| |
| labels.extend(_BuildRestrictionChoices( |
| mr.project, frequent_restrictions, |
| permissions.STANDARD_ISSUE_PERMISSIONS)) |
| |
| group_ids = self.services.usergroup.DetermineWhichUserIDsAreGroups( |
| mr.cnxn, [mem.user_id for mem in member_data['all_members']]) |
| logging.info('group_ids is %r', group_ids) |
| |
| # TODO(jrobbins): Normally, users will be allowed view the members |
| # of any user group if the project From: email address is listed |
| # as a group member, as well as any group that they are personally |
| # members of. |
| member_ids, owner_ids = self.services.usergroup.LookupVisibleMembers( |
| mr.cnxn, group_ids, mr.perms, mr.auth.effective_ids, self.services) |
| indirect_ids = set() |
| for gid in group_ids: |
| indirect_ids.update(member_ids.get(gid, [])) |
| indirect_ids.update(owner_ids.get(gid, [])) |
| indirect_user_ids = list(indirect_ids) |
| indirect_member_views = framework_views.MakeAllUserViews( |
| mr.cnxn, self.services.user, indirect_user_ids).values() |
| |
| visible_member_views = _FilterMemberData( |
| mr, owner_views, committer_views, contributor_views, |
| indirect_member_views) |
| # Filter out servbice accounts |
| visible_member_views = [m for m in visible_member_views |
| if not framework_helpers.IsServiceAccount(m.email)] |
| visible_member_email_list = list({ |
| uv.email for uv in visible_member_views}) |
| user_indexes = {email: idx |
| for idx, email in enumerate(visible_member_email_list)} |
| visible_members_dict = {} |
| for uv in visible_member_views: |
| visible_members_dict[uv.email] = uv.user_id |
| group_ids = self.services.usergroup.DetermineWhichUserIDsAreGroups( |
| mr.cnxn, visible_members_dict.values()) |
| |
| for field_dict in fields: |
| needed_perm = field_dict['needs_perm'] |
| if needed_perm: |
| qualified_user_indexes = [] |
| for uv in visible_member_views: |
| # TODO(jrobbins): Similar code occurs in field_helpers.py. |
| user = self.services.user.GetUser(mr.cnxn, uv.user_id) |
| auth = monorailrequest.AuthData.FromUserID( |
| mr.cnxn, uv.user_id, self.services) |
| user_perms = permissions.GetPermissions( |
| user, auth.effective_ids, mr.project) |
| has_perm = user_perms.CanUsePerm( |
| needed_perm, auth.effective_ids, mr.project, []) |
| if has_perm: |
| qualified_user_indexes.append(user_indexes[uv.email]) |
| |
| field_dict['user_indexes'] = sorted(set(qualified_user_indexes)) |
| |
| excl_prefixes = [prefix.lower() for prefix in |
| config.exclusive_label_prefixes] |
| members_def_list = [dict(name=email, doc='') |
| for email in visible_member_email_list] |
| members_def_list = sorted( |
| members_def_list, key=lambda md: md['name']) |
| for md in members_def_list: |
| md_id = visible_members_dict[md['name']] |
| if md_id in group_ids: |
| md['is_group'] = True |
| |
| return { |
| 'open': open_statuses, |
| 'closed': closed_statuses, |
| 'statuses_offer_merge': config.statuses_offer_merge, |
| 'components': components, |
| 'labels': labels, |
| 'fields': fields, |
| 'excl_prefixes': excl_prefixes, |
| 'strict': ezt.boolean(config.restrict_to_known), |
| 'members': members_def_list, |
| 'custom_permissions': custom_permissions, |
| } |
| |
| |
| def _FilterMemberData( |
| mr, owner_views, committer_views, contributor_views, |
| indirect_member_views): |
| """Return a filtered list of members that the user can view. |
| |
| In most projects, everyone can view the entire member list. But, |
| some projects are configured to only allow project owners to see |
| all members. In those projects, committers and contributors do not |
| see any contributors. Regardless of how the project is configured |
| or the role that the user plays in the current project, we include |
| any indirect members through user groups that the user has access |
| to view. |
| |
| Args: |
| mr: Commonly used info parsed from the HTTP request. |
| owner_views: list of UserViews for project owners. |
| committer_views: list of UserViews for project committers. |
| contributor_views: list of UserViews for project contributors. |
| indirect_member_views: list of UserViews for users who have |
| an indirect role in the project via a user group, and that the |
| logged in user is allowed to see. |
| |
| Returns: |
| A list of owners, committer and visible indirect members if the user is not |
| signed in. If the project is set to display contributors to non-owners or |
| the signed in user has necessary permissions then additionally a list of |
| contributors. |
| """ |
| visible_members = [] |
| |
| # Everyone can view owners and committers |
| visible_members.extend(owner_views) |
| visible_members.extend(committer_views) |
| |
| # The list of indirect members is already limited to ones that the user |
| # is allowed to see according to user group settings. |
| visible_members.extend(indirect_member_views) |
| |
| # If the user is allowed to view the list of contributors, add those too. |
| if permissions.CanViewContributorList(mr): |
| visible_members.extend(contributor_views) |
| |
| return visible_members |
| |
| |
| def _BuildRestrictionChoices(project, freq_restrictions, actions): |
| """Return a list of autocompletion choices for restriction labels. |
| |
| Args: |
| project: Project PB for the current project. |
| freq_restrictions: list of (action, perm, doc) tuples for restrictions |
| that are frequently used. |
| actions: list of strings for actions that are relevant to the current |
| artifact. |
| |
| Returns: |
| A list of dictionaries [{'name': 'perm name', 'doc': 'docstring'}, ...] |
| suitable for use in a JSON feed to our JS autocompletion functions. |
| """ |
| custom_permissions = permissions.GetCustomPermissions(project) |
| choices = [] |
| |
| for action, perm, doc in freq_restrictions: |
| choices.append({ |
| 'name': 'Restrict-%s-%s' % (action, perm), |
| 'doc': doc, |
| }) |
| |
| for action in actions: |
| for perm in custom_permissions: |
| choices.append({ |
| 'name': 'Restrict-%s-%s' % (action, perm), |
| 'doc': 'Permission %s needed to use %s' % (perm, action), |
| }) |
| |
| return choices |