Index: appengine/monorail/framework/permissions.py |
diff --git a/appengine/monorail/framework/permissions.py b/appengine/monorail/framework/permissions.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..e5b8404be8364b652acd4d2eead146469f96aba7 |
--- /dev/null |
+++ b/appengine/monorail/framework/permissions.py |
@@ -0,0 +1,959 @@ |
+# 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 |
+ |
+"""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. |
+""" |
+ |
+import logging |
+import time |
+ |
+from third_party 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 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' |
+DELETE_ISSUE = 'DeleteIssue' |
+ADD_ISSUE_COMMENT = 'AddIssueComment' |
+VIEW_INBOUND_MESSAGES = 'ViewInboundMessages' |
+# 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' |
+ |
+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} |
+ # Id X might have perm A and Y might have B, if both A and B are needed |
+ # True should be returned. |
+ for perm in needed_perms: |
+ if not any( |
+ self.HasPerm(perm, user_id, project) for user_id in effective_ids): |
+ return False |
+ |
+ return True |
+ |
+ def HasPerm(self, perm_name, user_id, project): |
+ """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. |
+ |
+ Returns: |
+ True if the user has the given perm. |
+ """ |
+ # TODO(jrobbins): room for performance improvement: pre-compute |
+ # extra perms (maybe merge them into the perms object), avoid |
+ # redundant call to lower(). |
+ extra_perms = [p.lower() for p in GetExtraPerms(project, user_id)] |
+ perm_name = perm_name.lower() |
+ return perm_name in self.perm_names or 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, 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, 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, |
+ VIEW_INBOUND_MESSAGES, |
+ DELETE_ANY, EDIT_ANY_MEMBER_NOTES, |
+ CREATE_GROUP, EDIT_GROUP, DELETE_GROUP, VIEW_GROUP, |
+ MODERATE_SPAM], |
+ 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]), |
+ } |
+ |
+ |
+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 s/he is 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 his/her 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 _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 |
+ |
+ # TODO(jrobbins): re-implement same_org |
+ |
+ 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: |
+ An ExtraPerms PB, or None. |
+ """ |
+ if not project: |
+ # TODO(jrobbins): maybe define extra perms for site-wide operations. |
+ return 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 |
+ |
+ for extra_perms in project.extra_perms: |
+ if extra_perms.member_id == member_id: |
+ return extra_perms |
+ |
+ return None |
+ |
+ |
+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 CanDelete(logged_in_user_id, effective_ids, perms, deleted_by_user_id, |
+ creator_user_id, project, restrictions, granted_perms=None): |
+ """Returns true if user has delete permission. |
+ |
+ Args: |
+ logged_in_user_id: int user id of the logged in user. |
+ effective_ids: set of int user IDs for the user (including any groups), |
+ or an empty set if user is not signed in. |
+ perms: instance of PermissionSet describing the current user's permissions. |
+ deleted_by_user_id: int user ID of the user having previously deleted this |
+ comment, or None, if the comment has never been deleted. |
+ creator_user_id: int user ID of the user having created this comment. |
+ project: Project PB for the project being accessed, or None if not |
+ in a project. |
+ restrictions: list of strings that restrict permission usage. |
+ 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 if the logged in user has delete permissions. |
+ """ |
+ |
+ # User is not logged in or has no permissions. |
+ if not logged_in_user_id or not perms: |
+ return False |
+ |
+ # Site admin or project owners can delete any comment. |
+ permit_delete_any = perms.CanUsePerm( |
+ DELETE_ANY, effective_ids, project, restrictions, |
+ granted_perms=granted_perms) |
+ if permit_delete_any: |
+ return True |
+ |
+ # Users cannot undelete unless they deleted. |
+ if deleted_by_user_id and deleted_by_user_id != logged_in_user_id: |
+ return False |
+ |
+ # Users can delete their own items. |
+ permit_delete_own = perms.CanUsePerm( |
+ DELETE_OWN, effective_ids, project, restrictions) |
+ if permit_delete_own and creator_user_id == logged_in_user_id: |
+ return True |
+ |
+ return False |
+ |
+ |
+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 CanViewGroup(perms, effective_ids, group_settings, member_ids, owner_ids, |
+ user_project_ids): |
+ """Return True if the given user may view a user group. |
+ |
+ 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. |
+ """ |
+ 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. |
+ |
+ return False |
+ |
+ |
+def CanViewContributorList(mr): |
+ """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. |
+ |
+ Returns: |
+ True if we should display the project contributor list. |
+ """ |
+ if not mr.project: |
+ return False # We are not even in a project context. |
+ |
+ if not mr.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, mr.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): |
+ """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)] |
+ |
+ |
+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: |
+ # No one can view a deleted issue. If the user can undelete, that |
+ # goes through the custom 404 page. |
+ return False |
+ |
+ # Check to see if the user can view anything in the project. |
+ if not perms.CanUsePerm(VIEW, effective_ids, project, []): |
+ return False |
+ |
+ if not HasRestrictions(issue): |
+ return True |
+ |
+ return CanViewRestrictedIssueInVisibleProject( |
+ effective_ids, perms, project, issue, granted_perms=granted_perms) |
+ |
+ |
+def CanViewRestrictedIssueInVisibleProject( |
+ effective_ids, perms, project, issue, granted_perms=None): |
+ """Return True if the user can view this issue. Assumes project is OK.""" |
+ # The reporter, owner, and CC'd users can always see the issue. |
+ # In effect, these fields override artifact restriction labels. |
+ if effective_ids: |
+ if (issue.reporter_id in effective_ids or |
+ tracker_bizobj.GetOwnerId(issue) in effective_ids or |
+ not effective_ids.isdisjoint(tracker_bizobj.GetCcIds(issue))): |
+ return True |
+ |
+ # Otherwise, apply the usual permission checking. |
+ return CanView( |
+ effective_ids, perms, project, GetRestrictions(issue), |
+ granted_perms=granted_perms) |
+ |
+ |
+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. |
+ """ |
+ # TODO(jrobbins): We need to actually grant View+EditIssue in most cases. |
+ # So, always grant View whenever there is any granted perm. |
+ if not CanViewIssue( |
+ effective_ids, perms, project, issue, granted_perms=granted_perms): |
+ return False |
+ |
+ # The issue owner can always edit the issue. |
+ if effective_ids: |
+ if tracker_bizobj.GetOwnerId(issue) in effective_ids: |
+ return True |
+ |
+ # Otherwise, apply the usual permission checking. |
+ return perms.CanUsePerm( |
+ EDIT_ISSUE, effective_ids, project, GetRestrictions(issue), |
+ granted_perms=granted_perms) |
+ |
+ |
+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 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 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, []) |
+ |
+ |
+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.""" |