| # Copyright 2016 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style |
| # license that can be found in the LICENSE file or at |
| # https://developers.google.com/open-source/licenses/bsd |
| |
| """Classes and functions to implement permission checking. |
| |
| The main data structure is a simple map from (user role, project status, |
| project_access_level) to specific perms. |
| |
| A perm is simply a string that indicates that the user has a given |
| permission. The servlets and templates can test whether the current |
| user has permission to see a UI element or perform an action by |
| testing for the presence of the corresponding perm in the user's |
| permission set. |
| |
| The user role is one of admin, owner, member, outsider user, or anon. |
| The project status is one of the project states defined in project_pb2, |
| or a special constant defined below. Likewise for access level. |
| """ |
| from __future__ import print_function |
| from __future__ import division |
| from __future__ import absolute_import |
| |
| import bisect |
| import collections |
| import logging |
| import time |
| |
| import ezt |
| |
| import settings |
| from framework import framework_bizobj |
| from framework import framework_constants |
| from proto import project_pb2 |
| from proto import site_pb2 |
| from proto import tracker_pb2 |
| from proto import usergroup_pb2 |
| from tracker import tracker_bizobj |
| |
| # Constants that define permissions. |
| # Note that perms with a leading "_" can never be granted |
| # to users who are not site admins. |
| VIEW = 'View' |
| EDIT_PROJECT = 'EditProject' |
| CREATE_PROJECT = 'CreateProject' |
| PUBLISH_PROJECT = '_PublishProject' # for making "doomed" projects LIVE |
| VIEW_DEBUG = '_ViewDebug' # on-page debugging info |
| EDIT_OTHER_USERS = '_EditOtherUsers' # can edit other user's prefs, ban, etc. |
| CUSTOMIZE_PROCESS = 'CustomizeProcess' # can use some enterprise features |
| VIEW_EXPIRED_PROJECT = '_ViewExpiredProject' # view long-deleted projects |
| # View the list of contributors even in hub-and-spoke projects. |
| VIEW_CONTRIBUTOR_LIST = 'ViewContributorList' |
| |
| # Quota |
| VIEW_QUOTA = 'ViewQuota' |
| EDIT_QUOTA = 'EditQuota' |
| |
| # Permissions for editing user groups |
| CREATE_GROUP = 'CreateGroup' |
| EDIT_GROUP = 'EditGroup' |
| DELETE_GROUP = 'DeleteGroup' |
| VIEW_GROUP = 'ViewGroup' |
| |
| # Perms for Source tools |
| # TODO(jrobbins): Monorail is just issue tracking with no version control, so |
| # phase out use of the term "Commit", sometime after Monorail's initial launch. |
| COMMIT = 'Commit' |
| |
| # Perms for issue tracking |
| CREATE_ISSUE = 'CreateIssue' |
| EDIT_ISSUE = 'EditIssue' |
| EDIT_ISSUE_OWNER = 'EditIssueOwner' |
| EDIT_ISSUE_SUMMARY = 'EditIssueSummary' |
| EDIT_ISSUE_STATUS = 'EditIssueStatus' |
| EDIT_ISSUE_CC = 'EditIssueCc' |
| EDIT_ISSUE_APPROVAL = 'EditIssueApproval' |
| DELETE_ISSUE = 'DeleteIssue' |
| # This allows certain API clients to attribute comments to other users. |
| # The permission is not offered in the UI, but it can be typed in as |
| # a custom permission name. The ID of the API client is also recorded. |
| IMPORT_COMMENT = 'ImportComment' |
| ADD_ISSUE_COMMENT = 'AddIssueComment' |
| VIEW_INBOUND_MESSAGES = 'ViewInboundMessages' |
| CREATE_HOTLIST = 'CreateHotlist' |
| # Note, there is no separate DELETE_ATTACHMENT perm. We |
| # allow a user to delete an attachment iff they could soft-delete |
| # the comment that holds the attachment. |
| |
| # Note: the "_" in the perm name makes it impossible for a |
| # project owner to grant it to anyone as an extra perm. |
| ADMINISTER_SITE = '_AdministerSite' |
| |
| # Permissions to soft-delete artifact comment |
| DELETE_ANY = 'DeleteAny' |
| DELETE_OWN = 'DeleteOwn' |
| |
| # Granting this allows owners to delegate some team management work. |
| EDIT_ANY_MEMBER_NOTES = 'EditAnyMemberNotes' |
| |
| # Permission to star/unstar any artifact. |
| SET_STAR = 'SetStar' |
| |
| # Permission to flag any artifact as spam. |
| FLAG_SPAM = 'FlagSpam' |
| VERDICT_SPAM = 'VerdictSpam' |
| MODERATE_SPAM = 'ModerateSpam' |
| |
| # Permissions for custom fields. |
| EDIT_FIELD_DEF = 'EditFieldDef' |
| EDIT_FIELD_DEF_VALUE = 'EditFieldDefValue' |
| |
| # Permissions for user hotlists. |
| ADMINISTER_HOTLIST = 'AdministerHotlist' |
| EDIT_HOTLIST = 'EditHotlist' |
| VIEW_HOTLIST = 'ViewHotlist' |
| HOTLIST_OWNER_PERMISSIONS = [ADMINISTER_HOTLIST, EDIT_HOTLIST] |
| HOTLIST_EDITOR_PERMISSIONS = [EDIT_HOTLIST] |
| |
| RESTRICTED_APPROVAL_STATUSES = [ |
| tracker_pb2.ApprovalStatus.NA, |
| tracker_pb2.ApprovalStatus.APPROVED, |
| tracker_pb2.ApprovalStatus.NOT_APPROVED] |
| |
| STANDARD_ADMIN_PERMISSIONS = [ |
| EDIT_PROJECT, CREATE_PROJECT, PUBLISH_PROJECT, VIEW_DEBUG, |
| EDIT_OTHER_USERS, CUSTOMIZE_PROCESS, |
| VIEW_QUOTA, EDIT_QUOTA, ADMINISTER_SITE, |
| EDIT_ANY_MEMBER_NOTES, VERDICT_SPAM, MODERATE_SPAM] |
| |
| STANDARD_ISSUE_PERMISSIONS = [ |
| VIEW, EDIT_ISSUE, ADD_ISSUE_COMMENT, DELETE_ISSUE, FLAG_SPAM] |
| |
| # Monorail has no source control, but keep COMMIT for backward compatability. |
| STANDARD_SOURCE_PERMISSIONS = [COMMIT] |
| |
| STANDARD_COMMENT_PERMISSIONS = [DELETE_OWN, DELETE_ANY] |
| |
| STANDARD_OTHER_PERMISSIONS = [CREATE_ISSUE, FLAG_SPAM, SET_STAR] |
| |
| STANDARD_PERMISSIONS = (STANDARD_ADMIN_PERMISSIONS + |
| STANDARD_ISSUE_PERMISSIONS + |
| STANDARD_SOURCE_PERMISSIONS + |
| STANDARD_COMMENT_PERMISSIONS + |
| STANDARD_OTHER_PERMISSIONS) |
| |
| # roles |
| SITE_ADMIN_ROLE = 'admin' |
| OWNER_ROLE = 'owner' |
| COMMITTER_ROLE = 'committer' |
| CONTRIBUTOR_ROLE = 'contributor' |
| USER_ROLE = 'user' |
| ANON_ROLE = 'anon' |
| |
| # Project state out-of-band values for keys |
| UNDEFINED_STATUS = 'undefined_status' |
| UNDEFINED_ACCESS = 'undefined_access' |
| WILDCARD_ACCESS = 'wildcard_access' |
| |
| |
| class PermissionSet(object): |
| """Class to represent the set of permissions available to the user.""" |
| |
| def __init__(self, perm_names, consider_restrictions=True): |
| """Create a PermissionSet with the given permissions. |
| |
| Args: |
| perm_names: a list of permission name strings. |
| consider_restrictions: if true, the user's permissions can be blocked |
| by restriction labels on an artifact. Project owners and site |
| admins do not consider restrictions so that they cannot |
| "lock themselves out" of editing an issue. |
| """ |
| self.perm_names = frozenset(p.lower() for p in perm_names) |
| self.consider_restrictions = consider_restrictions |
| |
| def __getattr__(self, perm_name): |
| """Easy permission testing in EZT. E.g., [if-any perms.format_drive].""" |
| return ezt.boolean(self.HasPerm(perm_name, None, None)) |
| |
| def CanUsePerm( |
| self, perm_name, effective_ids, project, restriction_labels, |
| granted_perms=None): |
| """Return True if the user can use the given permission. |
| |
| Args: |
| perm_name: string name of permission, e.g., 'EditIssue'. |
| effective_ids: set of int user IDs for the user (including any groups), |
| or an empty set if user is not signed in. |
| project: Project PB for the project being accessed, or None if not |
| in a project. |
| restriction_labels: list of strings that restrict permission usage. |
| granted_perms: optional list of lowercase 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. |
| |
| Restriction labels have 3 parts, e.g.: |
| 'Restrict-EditIssue-InnerCircle' blocks the use of just the |
| EditIssue permission, unless the user also has the InnerCircle |
| permission. This allows fine-grained restrictions on specific |
| actions, such as editing, commenting, or deleting. |
| |
| Restriction labels and permissions are case-insensitive. |
| |
| Returns: |
| True if the user can use the given permission, or False |
| if they cannot (either because they don't have that permission |
| or because it is blocked by a relevant restriction label). |
| """ |
| # TODO(jrobbins): room for performance improvement: avoid set creation and |
| # repeated string operations. |
| granted_perms = granted_perms or set() |
| perm_lower = perm_name.lower() |
| if perm_lower in granted_perms: |
| return True |
| |
| needed_perms = {perm_lower} |
| if self.consider_restrictions: |
| for label in restriction_labels: |
| label = label.lower() |
| # format: Restrict-Action-ToThisPerm |
| _kw, requested_perm, needed_perm = label.split('-', 2) |
| if requested_perm == perm_lower and needed_perm not in granted_perms: |
| needed_perms.add(needed_perm) |
| |
| if not effective_ids: |
| effective_ids = {framework_constants.NO_USER_SPECIFIED} |
| |
| # Get all extra perms for all effective ids. |
| # Id X might have perm A and Y might have B, if both A and B are needed |
| # True should be returned. |
| extra_perms = set() |
| for user_id in effective_ids: |
| extra_perms.update(p.lower() for p in GetExtraPerms(project, user_id)) |
| return all(self.HasPerm(perm, None, None, extra_perms) |
| for perm in needed_perms) |
| |
| def HasPerm(self, perm_name, user_id, project, extra_perms=None): |
| """Return True if the user has the given permission (ignoring user groups). |
| |
| Args: |
| perm_name: string name of permission, e.g., 'EditIssue'. |
| user_id: int user id of the user, or None if user is not signed in. |
| project: Project PB for the project being accessed, or None if not |
| in a project. |
| extra_perms: list of extra perms. If not given, GetExtraPerms will be |
| called to get them. |
| |
| Returns: |
| True if the user has the given perm. |
| """ |
| perm_name = perm_name.lower() |
| |
| # Return early if possible. |
| if perm_name in self.perm_names: |
| return True |
| |
| if extra_perms is None: |
| # TODO(jrobbins): room for performance improvement: pre-compute |
| # extra perms (maybe merge them into the perms object), avoid |
| # redundant call to lower(). |
| return any( |
| p.lower() == perm_name |
| for p in GetExtraPerms(project, user_id)) |
| |
| return perm_name in extra_perms |
| |
| def DebugString(self): |
| """Return a useful string to show when debugging.""" |
| return 'PermissionSet(%s)' % ', '.join(sorted(self.perm_names)) |
| |
| def __repr__(self): |
| return '%s(%r)' % (self.__class__.__name__, self.perm_names) |
| |
| |
| EMPTY_PERMISSIONSET = PermissionSet([]) |
| |
| READ_ONLY_PERMISSIONSET = PermissionSet([VIEW]) |
| |
| USER_PERMISSIONSET = PermissionSet([ |
| VIEW, FLAG_SPAM, SET_STAR, |
| CREATE_ISSUE, ADD_ISSUE_COMMENT, |
| DELETE_OWN]) |
| |
| CONTRIBUTOR_ACTIVE_PERMISSIONSET = PermissionSet( |
| [VIEW, |
| FLAG_SPAM, VERDICT_SPAM, SET_STAR, |
| CREATE_ISSUE, ADD_ISSUE_COMMENT, |
| DELETE_OWN]) |
| |
| CONTRIBUTOR_INACTIVE_PERMISSIONSET = PermissionSet( |
| [VIEW]) |
| |
| COMMITTER_ACTIVE_PERMISSIONSET = PermissionSet( |
| [VIEW, COMMIT, VIEW_CONTRIBUTOR_LIST, |
| FLAG_SPAM, VERDICT_SPAM, SET_STAR, VIEW_QUOTA, |
| CREATE_ISSUE, ADD_ISSUE_COMMENT, EDIT_ISSUE, VIEW_INBOUND_MESSAGES, |
| DELETE_OWN]) |
| |
| COMMITTER_INACTIVE_PERMISSIONSET = PermissionSet( |
| [VIEW, VIEW_CONTRIBUTOR_LIST, |
| VIEW_INBOUND_MESSAGES, VIEW_QUOTA]) |
| |
| OWNER_ACTIVE_PERMISSIONSET = PermissionSet( |
| [VIEW, VIEW_CONTRIBUTOR_LIST, EDIT_PROJECT, COMMIT, |
| FLAG_SPAM, VERDICT_SPAM, SET_STAR, VIEW_QUOTA, |
| CREATE_ISSUE, ADD_ISSUE_COMMENT, EDIT_ISSUE, DELETE_ISSUE, |
| VIEW_INBOUND_MESSAGES, |
| DELETE_ANY, EDIT_ANY_MEMBER_NOTES], |
| consider_restrictions=False) |
| |
| OWNER_INACTIVE_PERMISSIONSET = PermissionSet( |
| [VIEW, VIEW_CONTRIBUTOR_LIST, EDIT_PROJECT, |
| VIEW_INBOUND_MESSAGES, VIEW_QUOTA], |
| consider_restrictions=False) |
| |
| ADMIN_PERMISSIONSET = PermissionSet( |
| [VIEW, VIEW_CONTRIBUTOR_LIST, |
| CREATE_PROJECT, EDIT_PROJECT, PUBLISH_PROJECT, VIEW_DEBUG, |
| COMMIT, CUSTOMIZE_PROCESS, FLAG_SPAM, VERDICT_SPAM, SET_STAR, |
| ADMINISTER_SITE, VIEW_EXPIRED_PROJECT, EDIT_OTHER_USERS, |
| VIEW_QUOTA, EDIT_QUOTA, |
| CREATE_ISSUE, ADD_ISSUE_COMMENT, EDIT_ISSUE, DELETE_ISSUE, |
| EDIT_ISSUE_APPROVAL, |
| VIEW_INBOUND_MESSAGES, |
| DELETE_ANY, EDIT_ANY_MEMBER_NOTES, |
| CREATE_GROUP, EDIT_GROUP, DELETE_GROUP, VIEW_GROUP, |
| MODERATE_SPAM, CREATE_HOTLIST], |
| consider_restrictions=False) |
| |
| GROUP_IMPORT_BORG_PERMISSIONSET = PermissionSet( |
| [CREATE_GROUP, VIEW_GROUP, EDIT_GROUP]) |
| |
| # Permissions for project pages, e.g., the project summary page |
| _PERMISSIONS_TABLE = { |
| |
| # Project owners can view and edit artifacts in a LIVE project. |
| (OWNER_ROLE, project_pb2.ProjectState.LIVE, WILDCARD_ACCESS): |
| OWNER_ACTIVE_PERMISSIONSET, |
| |
| # Project owners can view, but not edit artifacts in ARCHIVED. |
| # Note: EDIT_PROJECT is not enough permission to change an ARCHIVED project |
| # back to LIVE if a delete_time was set. |
| (OWNER_ROLE, project_pb2.ProjectState.ARCHIVED, WILDCARD_ACCESS): |
| OWNER_INACTIVE_PERMISSIONSET, |
| |
| # Project members can view their own project, regardless of state. |
| (COMMITTER_ROLE, project_pb2.ProjectState.LIVE, WILDCARD_ACCESS): |
| COMMITTER_ACTIVE_PERMISSIONSET, |
| (COMMITTER_ROLE, project_pb2.ProjectState.ARCHIVED, WILDCARD_ACCESS): |
| COMMITTER_INACTIVE_PERMISSIONSET, |
| |
| # Project contributors can view their own project, regardless of state. |
| (CONTRIBUTOR_ROLE, project_pb2.ProjectState.LIVE, WILDCARD_ACCESS): |
| CONTRIBUTOR_ACTIVE_PERMISSIONSET, |
| (CONTRIBUTOR_ROLE, project_pb2.ProjectState.ARCHIVED, WILDCARD_ACCESS): |
| CONTRIBUTOR_INACTIVE_PERMISSIONSET, |
| |
| # Non-members users can read and comment in projects with access == ANYONE |
| (USER_ROLE, project_pb2.ProjectState.LIVE, |
| project_pb2.ProjectAccess.ANYONE): |
| USER_PERMISSIONSET, |
| |
| # Anonymous users can only read projects with access == ANYONE. |
| (ANON_ROLE, project_pb2.ProjectState.LIVE, |
| project_pb2.ProjectAccess.ANYONE): |
| READ_ONLY_PERMISSIONSET, |
| |
| # Permissions for site pages, e.g., creating a new project |
| (USER_ROLE, UNDEFINED_STATUS, UNDEFINED_ACCESS): |
| PermissionSet([CREATE_PROJECT, CREATE_GROUP, CREATE_HOTLIST]), |
| } |
| |
| def GetPermissions(user, effective_ids, project): |
| """Return a permission set appropriate for the user and project. |
| |
| Args: |
| user: The User PB for the signed-in user, or None for anon users. |
| effective_ids: set of int user IDs for the current user and all user |
| groups that they are a member of. This will be an empty set for |
| anonymous users. |
| project: either a Project protobuf, or None for a page whose scope is |
| wider than a single project. |
| |
| Returns: |
| a PermissionSet object for the current user and project (or for |
| site-wide operations if project is None). |
| |
| If an exact match for the user's role and project status is found, that is |
| returned. Otherwise, we look for permissions for the user's role that is |
| not specific to any project status, or not specific to any project access |
| level. If neither of those are defined, we give the user an empty |
| permission set. |
| """ |
| # Site admins get ADMIN_PERMISSIONSET regardless of groups or projects. |
| if user and user.is_site_admin: |
| return ADMIN_PERMISSIONSET |
| |
| # Grant the borg job permission to view/edit groups |
| if user and user.email == settings.borg_service_account: |
| return GROUP_IMPORT_BORG_PERMISSIONSET |
| |
| # Anon users don't need to accumulate anything. |
| if not effective_ids: |
| role, status, access = _GetPermissionKey(None, project) |
| return _LookupPermset(role, status, access) |
| |
| effective_perms = set() |
| consider_restrictions = True |
| |
| # Check for signed-in user with no roles in the current project. |
| if not project or not framework_bizobj.UserIsInProject( |
| project, effective_ids): |
| role, status, access = _GetPermissionKey(None, project) |
| return _LookupPermset(USER_ROLE, status, access) |
| |
| # Signed-in user gets the union of all their PermissionSets from the table. |
| for user_id in effective_ids: |
| role, status, access = _GetPermissionKey(user_id, project) |
| role_perms = _LookupPermset(role, status, access) |
| # Accumulate a union of all the user's permissions. |
| effective_perms.update(role_perms.perm_names) |
| # If any role allows the user to ignore restriction labels, then |
| # ignore them overall. |
| if not role_perms.consider_restrictions: |
| consider_restrictions = False |
| |
| return PermissionSet( |
| effective_perms, consider_restrictions=consider_restrictions) |
| |
| |
| 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 they are 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 always has 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 _LookupPermset(role, status, access): |
| """Lookup the appropriate PermissionSet in _PERMISSIONS_TABLE. |
| |
| Args: |
| role: a string indicating the user's role in the project. |
| status: a Project PB status value, or UNDEFINED_STATUS. |
| access: a Project PB access value, or UNDEFINED_ACCESS. |
| |
| Returns: |
| A PermissionSet that is appropriate for that kind of user in that |
| project context. |
| """ |
| if (role, status, access) in _PERMISSIONS_TABLE: |
| return _PERMISSIONS_TABLE[(role, status, access)] |
| elif (role, status, WILDCARD_ACCESS) in _PERMISSIONS_TABLE: |
| return _PERMISSIONS_TABLE[(role, status, WILDCARD_ACCESS)] |
| else: |
| return EMPTY_PERMISSIONSET |
| |
| |
| def _GetPermissionKey(user_id, project, expired_before=None): |
| """Return a permission lookup key appropriate for the user and project.""" |
| if user_id is None: |
| role = ANON_ROLE |
| elif project and IsExpired(project, expired_before=expired_before): |
| role = USER_ROLE # Do not honor roles in expired projects. |
| elif project and user_id in project.owner_ids: |
| role = OWNER_ROLE |
| elif project and user_id in project.committer_ids: |
| role = COMMITTER_ROLE |
| elif project and user_id in project.contributor_ids: |
| role = CONTRIBUTOR_ROLE |
| else: |
| role = USER_ROLE |
| |
| if project is None: |
| status = UNDEFINED_STATUS |
| else: |
| status = project.state |
| |
| if project is None: |
| access = UNDEFINED_ACCESS |
| else: |
| access = project.access |
| |
| return role, status, access |
| |
| |
| def GetExtraPerms(project, member_id): |
| """Return a list of extra perms for the user in the project. |
| |
| Args: |
| project: Project PB for the current project. |
| member_id: user id of a project owner, member, or contributor. |
| |
| Returns: |
| A list of strings for the extra perms granted to the |
| specified user in this project. The list will often be empty. |
| """ |
| |
| _, extra_perms = FindExtraPerms(project, member_id) |
| |
| if extra_perms: |
| return list(extra_perms.perms) |
| else: |
| return [] |
| |
| |
| def FindExtraPerms(project, member_id): |
| """Return a ExtraPerms PB for the given user in the project. |
| |
| Args: |
| project: Project PB for the current project, or None if the user is |
| not currently in a project. |
| member_id: user ID of a project owner, member, or contributor. |
| |
| Returns: |
| A pair (idx, extra_perms). |
| * If project is None or member_id is not part of the project, both are None. |
| * If member_id has no extra_perms, extra_perms is None, and idx points to |
| the position where it should go to keep the ExtraPerms sorted in project. |
| * Otherwise, idx is the position of member_id in the project's extra_perms, |
| and extra_perms is an ExtraPerms PB. |
| """ |
| class ExtraPermsView(object): |
| def __len__(self): |
| return len(project.extra_perms) |
| def __getitem__(self, idx): |
| return project.extra_perms[idx].member_id |
| |
| if not project: |
| # TODO(jrobbins): maybe define extra perms for site-wide operations. |
| return None, None |
| |
| # Users who have no current role cannot have any extra perms. Don't |
| # consider effective_ids (which includes user groups) for this check. |
| if not framework_bizobj.UserIsInProject(project, {member_id}): |
| return None, None |
| |
| extra_perms_view = ExtraPermsView() |
| # Find the index of the first extra_perms.member_id greater than or equal to |
| # member_id. |
| idx = bisect.bisect_left(extra_perms_view, member_id) |
| if idx >= len(project.extra_perms) or extra_perms_view[idx] > member_id: |
| return idx, None |
| return idx, project.extra_perms[idx] |
| |
| |
| def GetCustomPermissions(project): |
| """Return a sorted iterable of custom perms granted in a project.""" |
| custom_permissions = set() |
| for extra_perms in project.extra_perms: |
| for perm in extra_perms.perms: |
| if perm not in STANDARD_PERMISSIONS: |
| custom_permissions.add(perm) |
| |
| return sorted(custom_permissions) |
| |
| |
| def UserCanViewProject(user, effective_ids, project, expired_before=None): |
| """Return True if the user can view the given project. |
| |
| Args: |
| user: User protobuf for the user trying to view the project. |
| effective_ids: set of int user IDs of the user trying to view the project |
| (including any groups), or an empty set for anonymous users. |
| project: the Project protobuf to check. |
| expired_before: option time value for testing. |
| |
| Returns: |
| True if the user should be allowed to view the project. |
| """ |
| perms = GetPermissions(user, effective_ids, project) |
| |
| if IsExpired(project, expired_before=expired_before): |
| needed_perm = VIEW_EXPIRED_PROJECT |
| else: |
| needed_perm = VIEW |
| |
| return perms.CanUsePerm(needed_perm, effective_ids, project, []) |
| |
| |
| def IsExpired(project, expired_before=None): |
| """Return True if a project deletion has been pending long enough already. |
| |
| Args: |
| project: The project being viewed. |
| expired_before: If supplied, this method will return True only if the |
| project expired before the given time. |
| |
| Returns: |
| True if the project is eligible for reaping. |
| """ |
| if project.state != project_pb2.ProjectState.ARCHIVED: |
| return False |
| |
| if expired_before is None: |
| expired_before = int(time.time()) |
| |
| return project.delete_time and project.delete_time < expired_before |
| |
| |
| def CanDeleteComment(comment, commenter, user_id, perms): |
| """Returns true if the user can (un)delete the given comment. |
| |
| UpdateIssuePermissions must have been called first. |
| |
| Args: |
| comment: An IssueComment PB object. |
| commenter: An User PB object with the user who created the comment. |
| user_id: The ID of the user whose permission we want to check. |
| perms: The PermissionSet with the issue permissions. |
| |
| Returns: |
| True if the user can (un)delete the comment. |
| """ |
| # User is not logged in or has no permissions. |
| if not user_id or not perms: |
| return False |
| |
| # Nobody can (un)delete comments by banned users or spam comments, which |
| # should be un-flagged instead. |
| if commenter.banned or comment.is_spam: |
| return False |
| |
| # Site admin or project owners can delete any comment. |
| permit_delete_any = perms.HasPerm(DELETE_ANY, None, None, []) |
| if permit_delete_any: |
| return True |
| |
| # Users cannot undelete unless they deleted. |
| if comment.deleted_by and comment.deleted_by != user_id: |
| return False |
| |
| # Users can delete their own items. |
| permit_delete_own = perms.HasPerm(DELETE_OWN, None, None, []) |
| if permit_delete_own and comment.user_id == user_id: |
| return True |
| |
| return False |
| |
| |
| def CanFlagComment(comment, commenter, comment_reporters, user_id, perms): |
| """Returns true if the user can flag the given comment. |
| |
| UpdateIssuePermissions must have been called first. |
| Assumes that the user has permission to view the issue. |
| |
| Args: |
| comment: An IssueComment PB object. |
| commenter: An User PB object with the user who created the comment. |
| comment_reporters: A collection of user IDs who flagged the comment as spam. |
| user_id: The ID of the user for whom we're checking permissions. |
| perms: The PermissionSet with the issue permissions. |
| |
| Returns: |
| A tuple (can_flag, is_flagged). |
| can_flag is True if the user can flag the comment. and is_flagged is True |
| if the user sees the comment marked as spam. |
| """ |
| # Nobody can flag comments by banned users. |
| if commenter.banned: |
| return False, comment.is_spam |
| |
| # If a comment was deleted for a reason other than being spam, nobody can |
| # flag or un-flag it. |
| if comment.deleted_by and not comment.is_spam: |
| return False, comment.is_spam |
| |
| # A user with the VerdictSpam permission sees whether the comment is flagged |
| # as spam or not, and can mark it as flagged or un-flagged. |
| # If the comment is flagged as spam, all users see it as flagged, but only |
| # those with the VerdictSpam can un-flag it. |
| permit_verdict_spam = perms.HasPerm(VERDICT_SPAM, None, None, []) |
| if permit_verdict_spam or comment.is_spam: |
| return permit_verdict_spam, comment.is_spam |
| |
| # Otherwise, the comment is not marked as flagged and the user doesn't have |
| # the VerdictSpam permission. |
| # They are able to report a comment as spam if they have the FlagSpam |
| # permission, and they see the comment as flagged if the have previously |
| # reported it as spam. |
| permit_flag_spam = perms.HasPerm(FLAG_SPAM, None, None, []) |
| return permit_flag_spam, user_id in comment_reporters |
| |
| |
| def CanViewComment(comment, commenter, user_id, perms): |
| """Returns true if the user can view the given comment. |
| |
| UpdateIssuePermissions must have been called first. |
| Assumes that the user has permission to view the issue. |
| |
| Args: |
| comment: An IssueComment PB object. |
| commenter: An User PB object with the user who created the comment. |
| user_id: The ID of the user whose permission we want to check. |
| perms: The PermissionSet with the issue permissions. |
| |
| Returns: |
| True if the user can view the comment. |
| """ |
| # Nobody can view comments by banned users. |
| if commenter.banned: |
| return False |
| |
| # Only users with the permission to un-flag comments can view flagged |
| # comments. |
| if comment.is_spam: |
| # If the comment is marked as spam, whether the user can un-flag the comment |
| # or not doesn't depend on who reported it as spam. |
| can_flag, _ = CanFlagComment(comment, commenter, [], user_id, perms) |
| return can_flag |
| |
| # Only users with the permission to un-delete comments can view deleted |
| # comments. |
| if comment.deleted_by: |
| return CanDeleteComment(comment, commenter, user_id, perms) |
| |
| return True |
| |
| |
| def CanViewInboundMessage(comment, user_id, perms): |
| """Returns true if the user can view the given comment's inbound message. |
| |
| UpdateIssuePermissions must have been called first. |
| Assumes that the user has permission to view the comment. |
| |
| Args: |
| comment: An IssueComment PB object. |
| commenter: An User PB object with the user who created the comment. |
| user_id: The ID of the user whose permission we want to check. |
| perms: The PermissionSet with the issue permissions. |
| |
| Returns: |
| True if the user can view the comment's inbound message. |
| """ |
| return (perms.HasPerm(VIEW_INBOUND_MESSAGES, None, None, []) |
| or comment.user_id == user_id) |
| |
| |
| def CanView(effective_ids, perms, project, restrictions, granted_perms=None): |
| """Checks if user has permission to view an issue.""" |
| return perms.CanUsePerm( |
| VIEW, effective_ids, project, restrictions, granted_perms=granted_perms) |
| |
| |
| def CanCreateProject(perms): |
| """Return True if the given user may create a project. |
| |
| Args: |
| perms: Permissionset for the current user. |
| |
| Returns: |
| True if the user should be allowed to create a project. |
| """ |
| # "ANYONE" means anyone who has the needed perm. |
| if (settings.project_creation_restriction == |
| site_pb2.UserTypeRestriction.ANYONE): |
| return perms.HasPerm(CREATE_PROJECT, None, None) |
| |
| if (settings.project_creation_restriction == |
| site_pb2.UserTypeRestriction.ADMIN_ONLY): |
| return perms.HasPerm(ADMINISTER_SITE, None, None) |
| |
| return False |
| |
| |
| def CanCreateGroup(perms): |
| """Return True if the given user may create a user group. |
| |
| Args: |
| perms: Permissionset for the current user. |
| |
| Returns: |
| True if the user should be allowed to create a group. |
| """ |
| # "ANYONE" means anyone who has the needed perm. |
| if (settings.group_creation_restriction == |
| site_pb2.UserTypeRestriction.ANYONE): |
| return perms.HasPerm(CREATE_GROUP, None, None) |
| |
| if (settings.group_creation_restriction == |
| site_pb2.UserTypeRestriction.ADMIN_ONLY): |
| return perms.HasPerm(ADMINISTER_SITE, None, None) |
| |
| return False |
| |
| |
| def CanEditGroup(perms, effective_ids, group_owner_ids): |
| """Return True if the given user may edit a user group. |
| |
| Args: |
| perms: Permissionset for the current user. |
| effective_ids: set of user IDs for the logged in user. |
| group_owner_ids: set of user IDs of the user group owners. |
| |
| Returns: |
| True if the user should be allowed to edit the group. |
| """ |
| return (perms.HasPerm(EDIT_GROUP, None, None) or |
| not effective_ids.isdisjoint(group_owner_ids)) |
| |
| |
| def CanViewGroupMembers(perms, effective_ids, group_settings, member_ids, |
| owner_ids, user_project_ids): |
| """Return True if the given user may view a user group's members. |
| |
| Args: |
| perms: Permissionset for the current user. |
| effective_ids: set of user IDs for the logged in user. |
| group_settings: PB of UserGroupSettings. |
| member_ids: A list of member ids of this user group. |
| owner_ids: A list of owner ids of this user group. |
| user_project_ids: A list of project ids which the user has a role. |
| |
| Returns: |
| True if the user should be allowed to view the group's members. |
| """ |
| if perms.HasPerm(VIEW_GROUP, None, None): |
| return True |
| # The user could view this group with membership of some projects which are |
| # friends of the group. |
| if (group_settings.friend_projects and user_project_ids |
| and (set(group_settings.friend_projects) & set(user_project_ids))): |
| return True |
| visibility = group_settings.who_can_view_members |
| if visibility == usergroup_pb2.MemberVisibility.OWNERS: |
| return not effective_ids.isdisjoint(owner_ids) |
| elif visibility == usergroup_pb2.MemberVisibility.MEMBERS: |
| return (not effective_ids.isdisjoint(member_ids) or |
| not effective_ids.isdisjoint(owner_ids)) |
| else: |
| return True |
| |
| |
| def IsBanned(user, user_view): |
| """Return True if this user is banned from using our site.""" |
| if user is None: |
| return False # Anyone is welcome to browse |
| |
| if user.banned: |
| return True # We checked the "Banned" checkbox for this user. |
| |
| if user_view: |
| if user_view.domain in settings.banned_user_domains: |
| return True # Some spammers create many accounts with the same domain. |
| |
| if '+' in (user.email or ''): |
| # Spammers can make plus-addr Google accounts in unexpected domains. |
| return True |
| |
| return False |
| |
| |
| def CanBan(mr, services): |
| """Return True if the user is allowed to ban other users, site-wide.""" |
| if mr.perms.HasPerm(ADMINISTER_SITE, None, None): |
| return True |
| |
| owned, _, _ = services.project.GetUserRolesInAllProjects(mr.cnxn, |
| mr.auth.effective_ids) |
| return len(owned) > 0 |
| |
| |
| def CanExpungeUsers(mr): |
| """Return True is the user is allowed to delete user accounts.""" |
| return mr.perms.HasPerm(ADMINISTER_SITE, None, None) |
| |
| |
| def CanViewContributorList(mr, project): |
| """Return True if we should display the list project contributors. |
| |
| This is used on the project summary page, when deciding to offer the |
| project People page link, and when generating autocomplete options |
| that include project members. |
| |
| Args: |
| mr: commonly used info parsed from the request. |
| project: the Project we're interested in. |
| |
| Returns: |
| True if we should display the project contributor list. |
| """ |
| if not project: |
| return False # We are not even in a project context. |
| |
| if not project.only_owners_see_contributors: |
| return True # Contributor list is not resticted. |
| |
| # If it is hub-and-spoke, check for the perm that allows the user to |
| # view it anyway. |
| return mr.perms.HasPerm( |
| VIEW_CONTRIBUTOR_LIST, mr.auth.user_id, project) |
| |
| |
| def ShouldCheckForAbandonment(mr): |
| """Return True if user should be warned before changing/deleting their role. |
| |
| Args: |
| mr: common info parsed from the user's request. |
| |
| Returns: |
| True if user should be warned before changing/deleting their role. |
| """ |
| # Note: No need to warn admins because they won't lose access anyway. |
| if mr.perms.CanUsePerm( |
| ADMINISTER_SITE, mr.auth.effective_ids, mr.project, []): |
| return False |
| |
| return mr.perms.CanUsePerm( |
| EDIT_PROJECT, mr.auth.effective_ids, mr.project, []) |
| |
| |
| # For speed, we remember labels that we have already classified as being |
| # restriction labels or not being restriction labels. These sets are for |
| # restrictions in general, not for any particular perm. |
| _KNOWN_RESTRICTION_LABELS = set() |
| _KNOWN_NON_RESTRICTION_LABELS = set() |
| |
| |
| def IsRestrictLabel(label, perm=''): |
| """Returns True if a given label is a restriction label. |
| |
| Args: |
| label: string for the label to examine. |
| perm: a permission that can be restricted (e.g. 'View' or 'Edit'). |
| Defaults to '' to mean 'any'. |
| |
| Returns: |
| True if a given label is a restriction label (of the specified perm) |
| """ |
| if label in _KNOWN_NON_RESTRICTION_LABELS: |
| return False |
| if not perm and label in _KNOWN_RESTRICTION_LABELS: |
| return True |
| |
| prefix = ('restrict-%s-' % perm.lower()) if perm else 'restrict-' |
| is_restrict = label.lower().startswith(prefix) and label.count('-') >= 2 |
| |
| if is_restrict: |
| _KNOWN_RESTRICTION_LABELS.add(label) |
| elif not perm: |
| _KNOWN_NON_RESTRICTION_LABELS.add(label) |
| |
| return is_restrict |
| |
| |
| def HasRestrictions(issue, perm=''): |
| """Return True if the issue has any restrictions (on the specified perm).""" |
| return ( |
| any(IsRestrictLabel(lab, perm=perm) for lab in issue.labels) or |
| any(IsRestrictLabel(lab, perm=perm) for lab in issue.derived_labels)) |
| |
| |
| def GetRestrictions(issue, perm=''): |
| """Return a list of restriction labels on the given issue.""" |
| if not issue: |
| return [] |
| |
| return [lab.lower() for lab in tracker_bizobj.GetLabels(issue) |
| if IsRestrictLabel(lab, perm=perm)] |
| |
| |
| def CanViewIssue( |
| effective_ids, perms, project, issue, allow_viewing_deleted=False, |
| granted_perms=None): |
| """Checks if user has permission to view an artifact. |
| |
| Args: |
| effective_ids: set of user IDs for the logged in user and any user |
| group memberships. Should be an empty set for anon users. |
| perms: PermissionSet for the user. |
| project: Project PB for the project that contains this issue. |
| issue: Issue PB for the issue being viewed. |
| allow_viewing_deleted: True if the user should be allowed to view |
| deleted artifacts. |
| 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. |
| |
| Returns: |
| True iff the user can view the specified issue. |
| """ |
| if issue.deleted and not allow_viewing_deleted: |
| return False |
| |
| perms = UpdateIssuePermissions( |
| perms, project, issue, effective_ids, granted_perms=granted_perms) |
| return perms.HasPerm(VIEW, None, None) |
| |
| |
| def CanEditIssue(effective_ids, perms, project, issue, granted_perms=None): |
| """Return True if a user can edit an issue. |
| |
| Args: |
| effective_ids: set of user IDs for the logged in user and any user |
| group memberships. Should be an empty set for anon users. |
| perms: PermissionSet for the user. |
| project: Project PB for the project that contains this issue. |
| issue: Issue PB for the issue being viewed. |
| 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. |
| |
| Returns: |
| True iff the user can edit the specified issue. |
| """ |
| perms = UpdateIssuePermissions( |
| perms, project, issue, effective_ids, granted_perms=granted_perms) |
| return perms.HasPerm(EDIT_ISSUE, None, None) |
| |
| |
| def CanCommentIssue(effective_ids, perms, project, issue, granted_perms=None): |
| """Return True if a user can comment on an issue.""" |
| |
| return perms.CanUsePerm( |
| ADD_ISSUE_COMMENT, effective_ids, project, |
| GetRestrictions(issue), granted_perms=granted_perms) |
| |
| |
| def CanUpdateApprovalStatus( |
| effective_ids, perms, project, approver_ids, new_status): |
| """Return True if a user can change the approval status to the new status.""" |
| if not effective_ids.isdisjoint(approver_ids): |
| return True # Approval approvers can always change the approval status |
| |
| if new_status not in RESTRICTED_APPROVAL_STATUSES: |
| return True |
| |
| return perms.CanUsePerm(EDIT_ISSUE_APPROVAL, effective_ids, project, []) |
| |
| |
| def CanUpdateApprovers(effective_ids, perms, project, current_approver_ids): |
| """Return True if a user can edit the list of approvers for an approval.""" |
| if not effective_ids.isdisjoint(current_approver_ids): |
| return True |
| |
| return perms.CanUsePerm(EDIT_ISSUE_APPROVAL, effective_ids, project, []) |
| |
| |
| def CanViewComponentDef(effective_ids, perms, project, component_def): |
| """Return True if a user can view the given component definition.""" |
| if not effective_ids.isdisjoint(component_def.admin_ids): |
| return True # Component admins can view that component. |
| |
| # TODO(jrobbins): check restrictions on the component definition. |
| return perms.CanUsePerm(VIEW, effective_ids, project, []) |
| |
| |
| def CanEditComponentDef(effective_ids, perms, project, component_def, config): |
| """Return True if a user can edit the given component definition.""" |
| if not effective_ids.isdisjoint(component_def.admin_ids): |
| return True # Component admins can edit that component. |
| |
| # Check to see if user is admin of any parent component. |
| parent_components = tracker_bizobj.FindAncestorComponents( |
| config, component_def) |
| for parent in parent_components: |
| if not effective_ids.isdisjoint(parent.admin_ids): |
| return True |
| |
| return perms.CanUsePerm(EDIT_PROJECT, effective_ids, project, []) |
| |
| |
| def CanViewFieldDef(effective_ids, perms, project, field_def): |
| """Return True if a user can view the given field definition.""" |
| if not effective_ids.isdisjoint(field_def.admin_ids): |
| return True # Field admins can view that field. |
| |
| # TODO(jrobbins): check restrictions on the field definition. |
| return perms.CanUsePerm(VIEW, effective_ids, project, []) |
| |
| |
| def CanEditFieldDef(effective_ids, perms, project, field_def): |
| """Return True if a user can edit the given field definition.""" |
| if not effective_ids.isdisjoint(field_def.admin_ids): |
| return True # Field admins can edit that field. |
| |
| return perms.CanUsePerm(EDIT_PROJECT, effective_ids, project, []) |
| |
| |
| def CanEditValueForFieldDef(effective_ids, perms, project, field_def): |
| """Return True if a user can edit the given field definition value. |
| This method does not check that a user can edit the project issues.""" |
| if not effective_ids: |
| return False |
| if not field_def.is_restricted_field: |
| return True |
| if not effective_ids.isdisjoint(field_def.editor_ids): |
| return True |
| return CanEditFieldDef(effective_ids, perms, project, field_def) |
| |
| |
| def CanViewTemplate(effective_ids, perms, project, template): |
| """Return True if a user can view the given issue template.""" |
| if not effective_ids.isdisjoint(template.admin_ids): |
| return True # template admins can view that template. |
| |
| # Members-only templates are only shown to members, other templates are |
| # shown to any user that is generally allowed to view project content. |
| if template.members_only: |
| return framework_bizobj.UserIsInProject(project, effective_ids) |
| else: |
| return perms.CanUsePerm(VIEW, effective_ids, project, []) |
| |
| |
| def CanEditTemplate(effective_ids, perms, project, template): |
| """Return True if a user can edit the given field definition.""" |
| if not effective_ids.isdisjoint(template.admin_ids): |
| return True # Template admins can edit that template. |
| |
| return perms.CanUsePerm(EDIT_PROJECT, effective_ids, project, []) |
| |
| |
| def CanViewHotlist(effective_ids, perms, hotlist): |
| """Return True if a user can view the given hotlist.""" |
| if not hotlist.is_private or perms.HasPerm(ADMINISTER_SITE, None, None): |
| return True |
| |
| return any([user_id in (hotlist.owner_ids + hotlist.editor_ids) |
| for user_id in effective_ids]) |
| |
| |
| def CanEditHotlist(effective_ids, perms, hotlist): |
| """Return True if a user is editor(add/remove issues and change rankings).""" |
| return perms.HasPerm(ADMINISTER_SITE, None, None) or any( |
| [user_id in (hotlist.owner_ids + hotlist.editor_ids) |
| for user_id in effective_ids]) |
| |
| |
| def CanAdministerHotlist(effective_ids, perms, hotlist): |
| """Return True if user is owner(add/remove members, edit/delete hotlist).""" |
| return perms.HasPerm(ADMINISTER_SITE, None, None) or any( |
| [user_id in hotlist.owner_ids for user_id in effective_ids]) |
| |
| |
| def CanCreateHotlist(perms): |
| """Return True if the given user may create a hotlist. |
| |
| Args: |
| perms: Permissionset for the current user. |
| |
| Returns: |
| True if the user should be allowed to create a hotlist. |
| """ |
| if (settings.hotlist_creation_restriction == |
| site_pb2.UserTypeRestriction.ANYONE): |
| return perms.HasPerm(CREATE_HOTLIST, None, None) |
| |
| if (settings.hotlist_creation_restriction == |
| site_pb2.UserTypeRestriction.ADMIN_ONLY): |
| return perms.HasPerm(ADMINISTER_SITE, None, None) |
| |
| |
| class Error(Exception): |
| """Base class for errors from this module.""" |
| |
| |
| class PermissionException(Error): |
| """The user is not authorized to make the current request.""" |
| |
| |
| class BannedUserException(Error): |
| """The user has been banned from using our service.""" |