Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 1 | # Copyright 2016 The Chromium Authors |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 4 | |
| 5 | """Classes and functions to implement permission checking. |
| 6 | |
| 7 | The main data structure is a simple map from (user role, project status, |
| 8 | project_access_level) to specific perms. |
| 9 | |
| 10 | A perm is simply a string that indicates that the user has a given |
| 11 | permission. The servlets and templates can test whether the current |
| 12 | user has permission to see a UI element or perform an action by |
| 13 | testing for the presence of the corresponding perm in the user's |
| 14 | permission set. |
| 15 | |
| 16 | The user role is one of admin, owner, member, outsider user, or anon. |
| 17 | The project status is one of the project states defined in project_pb2, |
| 18 | or a special constant defined below. Likewise for access level. |
| 19 | """ |
| 20 | from __future__ import print_function |
| 21 | from __future__ import division |
| 22 | from __future__ import absolute_import |
| 23 | |
| 24 | import bisect |
| 25 | import collections |
| 26 | import logging |
| 27 | import time |
| 28 | |
| 29 | import ezt |
| 30 | |
| 31 | import settings |
| 32 | from framework import framework_bizobj |
| 33 | from framework import framework_constants |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 34 | from mrproto import project_pb2 |
| 35 | from mrproto import site_pb2 |
| 36 | from mrproto import tracker_pb2 |
| 37 | from mrproto import usergroup_pb2 |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 38 | from tracker import tracker_bizobj |
| 39 | |
| 40 | # Constants that define permissions. |
| 41 | # Note that perms with a leading "_" can never be granted |
| 42 | # to users who are not site admins. |
| 43 | VIEW = 'View' |
| 44 | EDIT_PROJECT = 'EditProject' |
| 45 | CREATE_PROJECT = 'CreateProject' |
| 46 | PUBLISH_PROJECT = '_PublishProject' # for making "doomed" projects LIVE |
| 47 | VIEW_DEBUG = '_ViewDebug' # on-page debugging info |
| 48 | EDIT_OTHER_USERS = '_EditOtherUsers' # can edit other user's prefs, ban, etc. |
| 49 | CUSTOMIZE_PROCESS = 'CustomizeProcess' # can use some enterprise features |
| 50 | VIEW_EXPIRED_PROJECT = '_ViewExpiredProject' # view long-deleted projects |
| 51 | # View the list of contributors even in hub-and-spoke projects. |
| 52 | VIEW_CONTRIBUTOR_LIST = 'ViewContributorList' |
| 53 | |
| 54 | # Quota |
| 55 | VIEW_QUOTA = 'ViewQuota' |
| 56 | EDIT_QUOTA = 'EditQuota' |
| 57 | |
| 58 | # Permissions for editing user groups |
| 59 | CREATE_GROUP = 'CreateGroup' |
| 60 | EDIT_GROUP = 'EditGroup' |
| 61 | DELETE_GROUP = 'DeleteGroup' |
| 62 | VIEW_GROUP = 'ViewGroup' |
| 63 | |
| 64 | # Perms for Source tools |
| 65 | # TODO(jrobbins): Monorail is just issue tracking with no version control, so |
| 66 | # phase out use of the term "Commit", sometime after Monorail's initial launch. |
| 67 | COMMIT = 'Commit' |
| 68 | |
| 69 | # Perms for issue tracking |
| 70 | CREATE_ISSUE = 'CreateIssue' |
| 71 | EDIT_ISSUE = 'EditIssue' |
| 72 | EDIT_ISSUE_OWNER = 'EditIssueOwner' |
| 73 | EDIT_ISSUE_SUMMARY = 'EditIssueSummary' |
| 74 | EDIT_ISSUE_STATUS = 'EditIssueStatus' |
| 75 | EDIT_ISSUE_CC = 'EditIssueCc' |
| 76 | EDIT_ISSUE_APPROVAL = 'EditIssueApproval' |
| 77 | DELETE_ISSUE = 'DeleteIssue' |
| 78 | # This allows certain API clients to attribute comments to other users. |
| 79 | # The permission is not offered in the UI, but it can be typed in as |
| 80 | # a custom permission name. The ID of the API client is also recorded. |
| 81 | IMPORT_COMMENT = 'ImportComment' |
| 82 | ADD_ISSUE_COMMENT = 'AddIssueComment' |
| 83 | VIEW_INBOUND_MESSAGES = 'ViewInboundMessages' |
| 84 | CREATE_HOTLIST = 'CreateHotlist' |
| 85 | # Note, there is no separate DELETE_ATTACHMENT perm. We |
| 86 | # allow a user to delete an attachment iff they could soft-delete |
| 87 | # the comment that holds the attachment. |
| 88 | |
| 89 | # Note: the "_" in the perm name makes it impossible for a |
| 90 | # project owner to grant it to anyone as an extra perm. |
| 91 | ADMINISTER_SITE = '_AdministerSite' |
| 92 | |
| 93 | # Permissions to soft-delete artifact comment |
| 94 | DELETE_ANY = 'DeleteAny' |
| 95 | DELETE_OWN = 'DeleteOwn' |
| 96 | |
| 97 | # Granting this allows owners to delegate some team management work. |
| 98 | EDIT_ANY_MEMBER_NOTES = 'EditAnyMemberNotes' |
| 99 | |
| 100 | # Permission to star/unstar any artifact. |
| 101 | SET_STAR = 'SetStar' |
| 102 | |
| 103 | # Permission to flag any artifact as spam. |
| 104 | FLAG_SPAM = 'FlagSpam' |
| 105 | VERDICT_SPAM = 'VerdictSpam' |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 106 | |
| 107 | # Permissions for custom fields. |
| 108 | EDIT_FIELD_DEF = 'EditFieldDef' |
| 109 | EDIT_FIELD_DEF_VALUE = 'EditFieldDefValue' |
| 110 | |
| 111 | # Permissions for user hotlists. |
| 112 | ADMINISTER_HOTLIST = 'AdministerHotlist' |
| 113 | EDIT_HOTLIST = 'EditHotlist' |
| 114 | VIEW_HOTLIST = 'ViewHotlist' |
| 115 | HOTLIST_OWNER_PERMISSIONS = [ADMINISTER_HOTLIST, EDIT_HOTLIST] |
| 116 | HOTLIST_EDITOR_PERMISSIONS = [EDIT_HOTLIST] |
| 117 | |
| 118 | RESTRICTED_APPROVAL_STATUSES = [ |
| 119 | tracker_pb2.ApprovalStatus.NA, |
| 120 | tracker_pb2.ApprovalStatus.APPROVED, |
| 121 | tracker_pb2.ApprovalStatus.NOT_APPROVED] |
| 122 | |
| 123 | STANDARD_ADMIN_PERMISSIONS = [ |
Adrià Vilanova Martínez | de94280 | 2022-07-15 14:06:55 +0200 | [diff] [blame] | 124 | EDIT_PROJECT, CREATE_PROJECT, PUBLISH_PROJECT, VIEW_DEBUG, EDIT_OTHER_USERS, |
| 125 | CUSTOMIZE_PROCESS, VIEW_QUOTA, EDIT_QUOTA, ADMINISTER_SITE, |
| 126 | EDIT_ANY_MEMBER_NOTES, VERDICT_SPAM |
| 127 | ] |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 128 | |
| 129 | STANDARD_ISSUE_PERMISSIONS = [ |
| 130 | VIEW, EDIT_ISSUE, ADD_ISSUE_COMMENT, DELETE_ISSUE, FLAG_SPAM] |
| 131 | |
| 132 | # Monorail has no source control, but keep COMMIT for backward compatability. |
| 133 | STANDARD_SOURCE_PERMISSIONS = [COMMIT] |
| 134 | |
| 135 | STANDARD_COMMENT_PERMISSIONS = [DELETE_OWN, DELETE_ANY] |
| 136 | |
| 137 | STANDARD_OTHER_PERMISSIONS = [CREATE_ISSUE, FLAG_SPAM, SET_STAR] |
| 138 | |
| 139 | STANDARD_PERMISSIONS = (STANDARD_ADMIN_PERMISSIONS + |
| 140 | STANDARD_ISSUE_PERMISSIONS + |
| 141 | STANDARD_SOURCE_PERMISSIONS + |
| 142 | STANDARD_COMMENT_PERMISSIONS + |
| 143 | STANDARD_OTHER_PERMISSIONS) |
| 144 | |
| 145 | # roles |
| 146 | SITE_ADMIN_ROLE = 'admin' |
| 147 | OWNER_ROLE = 'owner' |
| 148 | COMMITTER_ROLE = 'committer' |
| 149 | CONTRIBUTOR_ROLE = 'contributor' |
| 150 | USER_ROLE = 'user' |
| 151 | ANON_ROLE = 'anon' |
| 152 | |
| 153 | # Project state out-of-band values for keys |
| 154 | UNDEFINED_STATUS = 'undefined_status' |
| 155 | UNDEFINED_ACCESS = 'undefined_access' |
| 156 | WILDCARD_ACCESS = 'wildcard_access' |
| 157 | |
| 158 | |
| 159 | class PermissionSet(object): |
| 160 | """Class to represent the set of permissions available to the user.""" |
| 161 | |
| 162 | def __init__(self, perm_names, consider_restrictions=True): |
| 163 | """Create a PermissionSet with the given permissions. |
| 164 | |
| 165 | Args: |
| 166 | perm_names: a list of permission name strings. |
| 167 | consider_restrictions: if true, the user's permissions can be blocked |
| 168 | by restriction labels on an artifact. Project owners and site |
| 169 | admins do not consider restrictions so that they cannot |
| 170 | "lock themselves out" of editing an issue. |
| 171 | """ |
| 172 | self.perm_names = frozenset(p.lower() for p in perm_names) |
| 173 | self.consider_restrictions = consider_restrictions |
| 174 | |
| 175 | def __getattr__(self, perm_name): |
| 176 | """Easy permission testing in EZT. E.g., [if-any perms.format_drive].""" |
| 177 | return ezt.boolean(self.HasPerm(perm_name, None, None)) |
| 178 | |
| 179 | def CanUsePerm( |
| 180 | self, perm_name, effective_ids, project, restriction_labels, |
| 181 | granted_perms=None): |
| 182 | """Return True if the user can use the given permission. |
| 183 | |
| 184 | Args: |
| 185 | perm_name: string name of permission, e.g., 'EditIssue'. |
| 186 | effective_ids: set of int user IDs for the user (including any groups), |
| 187 | or an empty set if user is not signed in. |
| 188 | project: Project PB for the project being accessed, or None if not |
| 189 | in a project. |
| 190 | restriction_labels: list of strings that restrict permission usage. |
| 191 | granted_perms: optional list of lowercase strings of permissions that the |
| 192 | user is granted only within the scope of one issue, e.g., by being |
| 193 | named in a user-type custom field that grants permissions. |
| 194 | |
| 195 | Restriction labels have 3 parts, e.g.: |
| 196 | 'Restrict-EditIssue-InnerCircle' blocks the use of just the |
| 197 | EditIssue permission, unless the user also has the InnerCircle |
| 198 | permission. This allows fine-grained restrictions on specific |
| 199 | actions, such as editing, commenting, or deleting. |
| 200 | |
| 201 | Restriction labels and permissions are case-insensitive. |
| 202 | |
| 203 | Returns: |
| 204 | True if the user can use the given permission, or False |
| 205 | if they cannot (either because they don't have that permission |
| 206 | or because it is blocked by a relevant restriction label). |
| 207 | """ |
| 208 | # TODO(jrobbins): room for performance improvement: avoid set creation and |
| 209 | # repeated string operations. |
| 210 | granted_perms = granted_perms or set() |
| 211 | perm_lower = perm_name.lower() |
| 212 | if perm_lower in granted_perms: |
| 213 | return True |
| 214 | |
| 215 | needed_perms = {perm_lower} |
| 216 | if self.consider_restrictions: |
| 217 | for label in restriction_labels: |
| 218 | label = label.lower() |
| 219 | # format: Restrict-Action-ToThisPerm |
| 220 | _kw, requested_perm, needed_perm = label.split('-', 2) |
| 221 | if requested_perm == perm_lower and needed_perm not in granted_perms: |
| 222 | needed_perms.add(needed_perm) |
| 223 | |
| 224 | if not effective_ids: |
| 225 | effective_ids = {framework_constants.NO_USER_SPECIFIED} |
| 226 | |
| 227 | # Get all extra perms for all effective ids. |
| 228 | # Id X might have perm A and Y might have B, if both A and B are needed |
| 229 | # True should be returned. |
| 230 | extra_perms = set() |
| 231 | for user_id in effective_ids: |
| 232 | extra_perms.update(p.lower() for p in GetExtraPerms(project, user_id)) |
| 233 | return all(self.HasPerm(perm, None, None, extra_perms) |
| 234 | for perm in needed_perms) |
| 235 | |
| 236 | def HasPerm(self, perm_name, user_id, project, extra_perms=None): |
| 237 | """Return True if the user has the given permission (ignoring user groups). |
| 238 | |
| 239 | Args: |
| 240 | perm_name: string name of permission, e.g., 'EditIssue'. |
| 241 | user_id: int user id of the user, or None if user is not signed in. |
| 242 | project: Project PB for the project being accessed, or None if not |
| 243 | in a project. |
| 244 | extra_perms: list of extra perms. If not given, GetExtraPerms will be |
| 245 | called to get them. |
| 246 | |
| 247 | Returns: |
| 248 | True if the user has the given perm. |
| 249 | """ |
| 250 | perm_name = perm_name.lower() |
| 251 | |
| 252 | # Return early if possible. |
| 253 | if perm_name in self.perm_names: |
| 254 | return True |
| 255 | |
| 256 | if extra_perms is None: |
| 257 | # TODO(jrobbins): room for performance improvement: pre-compute |
| 258 | # extra perms (maybe merge them into the perms object), avoid |
| 259 | # redundant call to lower(). |
| 260 | return any( |
| 261 | p.lower() == perm_name |
| 262 | for p in GetExtraPerms(project, user_id)) |
| 263 | |
| 264 | return perm_name in extra_perms |
| 265 | |
| 266 | def DebugString(self): |
| 267 | """Return a useful string to show when debugging.""" |
| 268 | return 'PermissionSet(%s)' % ', '.join(sorted(self.perm_names)) |
| 269 | |
| 270 | def __repr__(self): |
| 271 | return '%s(%r)' % (self.__class__.__name__, self.perm_names) |
| 272 | |
| 273 | |
| 274 | EMPTY_PERMISSIONSET = PermissionSet([]) |
| 275 | |
| 276 | READ_ONLY_PERMISSIONSET = PermissionSet([VIEW]) |
| 277 | |
| 278 | USER_PERMISSIONSET = PermissionSet([ |
| 279 | VIEW, FLAG_SPAM, SET_STAR, |
| 280 | CREATE_ISSUE, ADD_ISSUE_COMMENT, |
| 281 | DELETE_OWN]) |
| 282 | |
| 283 | CONTRIBUTOR_ACTIVE_PERMISSIONSET = PermissionSet( |
| 284 | [VIEW, |
| 285 | FLAG_SPAM, VERDICT_SPAM, SET_STAR, |
| 286 | CREATE_ISSUE, ADD_ISSUE_COMMENT, |
| 287 | DELETE_OWN]) |
| 288 | |
| 289 | CONTRIBUTOR_INACTIVE_PERMISSIONSET = PermissionSet( |
| 290 | [VIEW]) |
| 291 | |
| 292 | COMMITTER_ACTIVE_PERMISSIONSET = PermissionSet( |
| 293 | [VIEW, COMMIT, VIEW_CONTRIBUTOR_LIST, |
| 294 | FLAG_SPAM, VERDICT_SPAM, SET_STAR, VIEW_QUOTA, |
| 295 | CREATE_ISSUE, ADD_ISSUE_COMMENT, EDIT_ISSUE, VIEW_INBOUND_MESSAGES, |
| 296 | DELETE_OWN]) |
| 297 | |
| 298 | COMMITTER_INACTIVE_PERMISSIONSET = PermissionSet( |
| 299 | [VIEW, VIEW_CONTRIBUTOR_LIST, |
| 300 | VIEW_INBOUND_MESSAGES, VIEW_QUOTA]) |
| 301 | |
| 302 | OWNER_ACTIVE_PERMISSIONSET = PermissionSet( |
| 303 | [VIEW, VIEW_CONTRIBUTOR_LIST, EDIT_PROJECT, COMMIT, |
| 304 | FLAG_SPAM, VERDICT_SPAM, SET_STAR, VIEW_QUOTA, |
| 305 | CREATE_ISSUE, ADD_ISSUE_COMMENT, EDIT_ISSUE, DELETE_ISSUE, |
| 306 | VIEW_INBOUND_MESSAGES, |
| 307 | DELETE_ANY, EDIT_ANY_MEMBER_NOTES], |
| 308 | consider_restrictions=False) |
| 309 | |
| 310 | OWNER_INACTIVE_PERMISSIONSET = PermissionSet( |
| 311 | [VIEW, VIEW_CONTRIBUTOR_LIST, EDIT_PROJECT, |
| 312 | VIEW_INBOUND_MESSAGES, VIEW_QUOTA], |
| 313 | consider_restrictions=False) |
| 314 | |
| 315 | ADMIN_PERMISSIONSET = PermissionSet( |
Adrià Vilanova Martínez | de94280 | 2022-07-15 14:06:55 +0200 | [diff] [blame] | 316 | [ |
| 317 | VIEW, VIEW_CONTRIBUTOR_LIST, CREATE_PROJECT, EDIT_PROJECT, |
| 318 | PUBLISH_PROJECT, VIEW_DEBUG, COMMIT, CUSTOMIZE_PROCESS, FLAG_SPAM, |
| 319 | VERDICT_SPAM, SET_STAR, ADMINISTER_SITE, VIEW_EXPIRED_PROJECT, |
| 320 | EDIT_OTHER_USERS, VIEW_QUOTA, EDIT_QUOTA, CREATE_ISSUE, |
| 321 | ADD_ISSUE_COMMENT, EDIT_ISSUE, DELETE_ISSUE, EDIT_ISSUE_APPROVAL, |
| 322 | VIEW_INBOUND_MESSAGES, DELETE_ANY, EDIT_ANY_MEMBER_NOTES, CREATE_GROUP, |
| 323 | EDIT_GROUP, DELETE_GROUP, VIEW_GROUP, CREATE_HOTLIST |
| 324 | ], |
| 325 | consider_restrictions=False) |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 326 | |
| 327 | GROUP_IMPORT_BORG_PERMISSIONSET = PermissionSet( |
| 328 | [CREATE_GROUP, VIEW_GROUP, EDIT_GROUP]) |
| 329 | |
| 330 | # Permissions for project pages, e.g., the project summary page |
| 331 | _PERMISSIONS_TABLE = { |
| 332 | |
| 333 | # Project owners can view and edit artifacts in a LIVE project. |
| 334 | (OWNER_ROLE, project_pb2.ProjectState.LIVE, WILDCARD_ACCESS): |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 335 | OWNER_ACTIVE_PERMISSIONSET, |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 336 | |
| 337 | # Project owners can view, but not edit artifacts in ARCHIVED. |
| 338 | # Note: EDIT_PROJECT is not enough permission to change an ARCHIVED project |
| 339 | # back to LIVE if a delete_time was set. |
| 340 | (OWNER_ROLE, project_pb2.ProjectState.ARCHIVED, WILDCARD_ACCESS): |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 341 | OWNER_INACTIVE_PERMISSIONSET, |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 342 | |
| 343 | # Project members can view their own project, regardless of state. |
| 344 | (COMMITTER_ROLE, project_pb2.ProjectState.LIVE, WILDCARD_ACCESS): |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 345 | COMMITTER_ACTIVE_PERMISSIONSET, |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 346 | (COMMITTER_ROLE, project_pb2.ProjectState.ARCHIVED, WILDCARD_ACCESS): |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 347 | COMMITTER_INACTIVE_PERMISSIONSET, |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 348 | |
| 349 | # Project contributors can view their own project, regardless of state. |
| 350 | (CONTRIBUTOR_ROLE, project_pb2.ProjectState.LIVE, WILDCARD_ACCESS): |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 351 | CONTRIBUTOR_ACTIVE_PERMISSIONSET, |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 352 | (CONTRIBUTOR_ROLE, project_pb2.ProjectState.ARCHIVED, WILDCARD_ACCESS): |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 353 | CONTRIBUTOR_INACTIVE_PERMISSIONSET, |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 354 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 355 | # Non-members users can read and comment in projects with access == ANYONE. |
| 356 | ( |
| 357 | USER_ROLE, project_pb2.ProjectState.LIVE, |
| 358 | project_pb2.ProjectAccess.ANYONE): |
| 359 | USER_PERMISSIONSET, |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 360 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 361 | # Non-members users can read archived projects with access == ANYONE. |
| 362 | ( |
| 363 | USER_ROLE, project_pb2.ProjectState.ARCHIVED, |
| 364 | project_pb2.ProjectAccess.ANYONE): |
| 365 | READ_ONLY_PERMISSIONSET, |
| 366 | |
| 367 | # Anonymous users can only read projects with access == ANYONE, |
| 368 | # regardless of state. |
| 369 | ( |
| 370 | ANON_ROLE, project_pb2.ProjectState.LIVE, |
| 371 | project_pb2.ProjectAccess.ANYONE): |
| 372 | READ_ONLY_PERMISSIONSET, |
| 373 | ( |
| 374 | ANON_ROLE, project_pb2.ProjectState.ARCHIVED, |
| 375 | project_pb2.ProjectAccess.ANYONE): |
| 376 | READ_ONLY_PERMISSIONSET, |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 377 | |
| 378 | # Permissions for site pages, e.g., creating a new project |
| 379 | (USER_ROLE, UNDEFINED_STATUS, UNDEFINED_ACCESS): |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 380 | PermissionSet([CREATE_PROJECT, CREATE_GROUP, CREATE_HOTLIST]), |
| 381 | } |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 382 | |
| 383 | def GetPermissions(user, effective_ids, project): |
| 384 | """Return a permission set appropriate for the user and project. |
| 385 | |
| 386 | Args: |
| 387 | user: The User PB for the signed-in user, or None for anon users. |
| 388 | effective_ids: set of int user IDs for the current user and all user |
| 389 | groups that they are a member of. This will be an empty set for |
| 390 | anonymous users. |
| 391 | project: either a Project protobuf, or None for a page whose scope is |
| 392 | wider than a single project. |
| 393 | |
| 394 | Returns: |
| 395 | a PermissionSet object for the current user and project (or for |
| 396 | site-wide operations if project is None). |
| 397 | |
| 398 | If an exact match for the user's role and project status is found, that is |
| 399 | returned. Otherwise, we look for permissions for the user's role that is |
| 400 | not specific to any project status, or not specific to any project access |
| 401 | level. If neither of those are defined, we give the user an empty |
| 402 | permission set. |
| 403 | """ |
| 404 | # Site admins get ADMIN_PERMISSIONSET regardless of groups or projects. |
| 405 | if user and user.is_site_admin: |
| 406 | return ADMIN_PERMISSIONSET |
| 407 | |
| 408 | # Grant the borg job permission to view/edit groups |
| 409 | if user and user.email == settings.borg_service_account: |
| 410 | return GROUP_IMPORT_BORG_PERMISSIONSET |
| 411 | |
| 412 | # Anon users don't need to accumulate anything. |
| 413 | if not effective_ids: |
| 414 | role, status, access = _GetPermissionKey(None, project) |
| 415 | return _LookupPermset(role, status, access) |
| 416 | |
| 417 | effective_perms = set() |
| 418 | consider_restrictions = True |
| 419 | |
| 420 | # Check for signed-in user with no roles in the current project. |
| 421 | if not project or not framework_bizobj.UserIsInProject( |
| 422 | project, effective_ids): |
| 423 | role, status, access = _GetPermissionKey(None, project) |
| 424 | return _LookupPermset(USER_ROLE, status, access) |
| 425 | |
| 426 | # Signed-in user gets the union of all their PermissionSets from the table. |
| 427 | for user_id in effective_ids: |
| 428 | role, status, access = _GetPermissionKey(user_id, project) |
| 429 | role_perms = _LookupPermset(role, status, access) |
| 430 | # Accumulate a union of all the user's permissions. |
| 431 | effective_perms.update(role_perms.perm_names) |
| 432 | # If any role allows the user to ignore restriction labels, then |
| 433 | # ignore them overall. |
| 434 | if not role_perms.consider_restrictions: |
| 435 | consider_restrictions = False |
| 436 | |
| 437 | return PermissionSet( |
| 438 | effective_perms, consider_restrictions=consider_restrictions) |
| 439 | |
| 440 | |
| 441 | def UpdateIssuePermissions( |
| 442 | perms, project, issue, effective_ids, granted_perms=None, config=None): |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 443 | """Update the PermissionSet for a specific issue. |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 444 | |
| 445 | Take into account granted permissions and label restrictions to filter the |
| 446 | permissions, and updates the VIEW and EDIT_ISSUE permissions depending on the |
| 447 | role of the user in the issue (i.e. owner, reporter, cc or approver). |
| 448 | |
| 449 | Args: |
| 450 | perms: The PermissionSet to update. |
| 451 | project: The Project PB for the issue project. |
| 452 | issue: The Issue PB. |
| 453 | effective_ids: Set of int user IDs for the current user and all user |
| 454 | groups that they are a member of. This will be an empty set for |
| 455 | anonymous users. |
| 456 | granted_perms: optional list of strings of permissions that the user is |
| 457 | granted only within the scope of one issue, e.g., by being named in |
| 458 | a user-type custom field that grants permissions. |
| 459 | config: optional ProjectIssueConfig PB where granted perms should be |
| 460 | extracted from, if granted_perms is not given. |
| 461 | """ |
| 462 | if config: |
| 463 | granted_perms = tracker_bizobj.GetGrantedPerms( |
| 464 | issue, effective_ids, config) |
| 465 | elif granted_perms is None: |
| 466 | granted_perms = [] |
| 467 | |
| 468 | # If the user has no permission to view the project, it has no permissions on |
| 469 | # this issue. |
| 470 | if not perms.HasPerm(VIEW, None, None): |
| 471 | return EMPTY_PERMISSIONSET |
| 472 | |
| 473 | # Compute the restrictions for the given issue and store them in a dictionary |
| 474 | # of {perm: set(needed_perms)}. |
| 475 | restrictions = collections.defaultdict(set) |
| 476 | if perms.consider_restrictions: |
| 477 | for label in GetRestrictions(issue): |
| 478 | label = label.lower() |
| 479 | # format: Restrict-Action-ToThisPerm |
| 480 | _, requested_perm, needed_perm = label.split('-', 2) |
| 481 | restrictions[requested_perm.lower()].add(needed_perm.lower()) |
| 482 | |
| 483 | # Store the user permissions, and the extra permissions of all effective IDs |
| 484 | # in the given project. |
| 485 | all_perms = set(perms.perm_names) |
| 486 | for effective_id in effective_ids: |
| 487 | all_perms.update(p.lower() for p in GetExtraPerms(project, effective_id)) |
| 488 | |
| 489 | # And filter them applying the restriction labels. |
| 490 | filtered_perms = set() |
| 491 | for perm_name in all_perms: |
| 492 | perm_name = perm_name.lower() |
| 493 | restricted = any( |
| 494 | restriction not in all_perms and restriction not in granted_perms |
| 495 | for restriction in restrictions.get(perm_name, [])) |
| 496 | if not restricted: |
| 497 | filtered_perms.add(perm_name) |
| 498 | |
| 499 | # Add any granted permissions. |
| 500 | filtered_perms.update(granted_perms) |
| 501 | |
| 502 | # The VIEW perm might have been removed due to restrictions, but the issue |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 503 | # owner, reporter, cc and approvers can always view an issue. |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 504 | allowed_ids = set( |
| 505 | tracker_bizobj.GetCcIds(issue) |
| 506 | + tracker_bizobj.GetApproverIds(issue) |
| 507 | + [issue.reporter_id, tracker_bizobj.GetOwnerId(issue)]) |
| 508 | if effective_ids and not allowed_ids.isdisjoint(effective_ids): |
| 509 | filtered_perms.add(VIEW.lower()) |
| 510 | |
| 511 | # If the issue is deleted, only the VIEW and DELETE_ISSUE permissions are |
| 512 | # relevant. |
| 513 | if issue.deleted: |
| 514 | if VIEW.lower() not in filtered_perms: |
| 515 | return EMPTY_PERMISSIONSET |
| 516 | if DELETE_ISSUE.lower() in filtered_perms: |
| 517 | return PermissionSet([VIEW, DELETE_ISSUE], perms.consider_restrictions) |
| 518 | return PermissionSet([VIEW], perms.consider_restrictions) |
| 519 | |
| 520 | # The EDIT_ISSUE permission might have been removed due to restrictions, but |
| 521 | # the owner always has permission to edit it. |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 522 | if (effective_ids and tracker_bizobj.GetOwnerId(issue) in effective_ids and |
| 523 | project and project.state != project_pb2.ProjectState.ARCHIVED): |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 524 | filtered_perms.add(EDIT_ISSUE.lower()) |
| 525 | |
| 526 | return PermissionSet(filtered_perms, perms.consider_restrictions) |
| 527 | |
| 528 | |
| 529 | def _LookupPermset(role, status, access): |
| 530 | """Lookup the appropriate PermissionSet in _PERMISSIONS_TABLE. |
| 531 | |
| 532 | Args: |
| 533 | role: a string indicating the user's role in the project. |
| 534 | status: a Project PB status value, or UNDEFINED_STATUS. |
| 535 | access: a Project PB access value, or UNDEFINED_ACCESS. |
| 536 | |
| 537 | Returns: |
| 538 | A PermissionSet that is appropriate for that kind of user in that |
| 539 | project context. |
| 540 | """ |
| 541 | if (role, status, access) in _PERMISSIONS_TABLE: |
| 542 | return _PERMISSIONS_TABLE[(role, status, access)] |
| 543 | elif (role, status, WILDCARD_ACCESS) in _PERMISSIONS_TABLE: |
| 544 | return _PERMISSIONS_TABLE[(role, status, WILDCARD_ACCESS)] |
| 545 | else: |
| 546 | return EMPTY_PERMISSIONSET |
| 547 | |
| 548 | |
| 549 | def _GetPermissionKey(user_id, project, expired_before=None): |
| 550 | """Return a permission lookup key appropriate for the user and project.""" |
| 551 | if user_id is None: |
| 552 | role = ANON_ROLE |
| 553 | elif project and IsExpired(project, expired_before=expired_before): |
| 554 | role = USER_ROLE # Do not honor roles in expired projects. |
| 555 | elif project and user_id in project.owner_ids: |
| 556 | role = OWNER_ROLE |
| 557 | elif project and user_id in project.committer_ids: |
| 558 | role = COMMITTER_ROLE |
| 559 | elif project and user_id in project.contributor_ids: |
| 560 | role = CONTRIBUTOR_ROLE |
| 561 | else: |
| 562 | role = USER_ROLE |
| 563 | |
| 564 | if project is None: |
| 565 | status = UNDEFINED_STATUS |
| 566 | else: |
| 567 | status = project.state |
| 568 | |
| 569 | if project is None: |
| 570 | access = UNDEFINED_ACCESS |
| 571 | else: |
| 572 | access = project.access |
| 573 | |
| 574 | return role, status, access |
| 575 | |
| 576 | |
| 577 | def GetExtraPerms(project, member_id): |
| 578 | """Return a list of extra perms for the user in the project. |
| 579 | |
| 580 | Args: |
| 581 | project: Project PB for the current project. |
| 582 | member_id: user id of a project owner, member, or contributor. |
| 583 | |
| 584 | Returns: |
| 585 | A list of strings for the extra perms granted to the |
| 586 | specified user in this project. The list will often be empty. |
| 587 | """ |
| 588 | |
| 589 | _, extra_perms = FindExtraPerms(project, member_id) |
| 590 | |
| 591 | if extra_perms: |
| 592 | return list(extra_perms.perms) |
| 593 | else: |
| 594 | return [] |
| 595 | |
| 596 | |
| 597 | def FindExtraPerms(project, member_id): |
| 598 | """Return a ExtraPerms PB for the given user in the project. |
| 599 | |
| 600 | Args: |
| 601 | project: Project PB for the current project, or None if the user is |
| 602 | not currently in a project. |
| 603 | member_id: user ID of a project owner, member, or contributor. |
| 604 | |
| 605 | Returns: |
| 606 | A pair (idx, extra_perms). |
| 607 | * If project is None or member_id is not part of the project, both are None. |
| 608 | * If member_id has no extra_perms, extra_perms is None, and idx points to |
| 609 | the position where it should go to keep the ExtraPerms sorted in project. |
| 610 | * Otherwise, idx is the position of member_id in the project's extra_perms, |
| 611 | and extra_perms is an ExtraPerms PB. |
| 612 | """ |
| 613 | class ExtraPermsView(object): |
| 614 | def __len__(self): |
| 615 | return len(project.extra_perms) |
| 616 | def __getitem__(self, idx): |
| 617 | return project.extra_perms[idx].member_id |
| 618 | |
| 619 | if not project: |
| 620 | # TODO(jrobbins): maybe define extra perms for site-wide operations. |
| 621 | return None, None |
| 622 | |
| 623 | # Users who have no current role cannot have any extra perms. Don't |
| 624 | # consider effective_ids (which includes user groups) for this check. |
| 625 | if not framework_bizobj.UserIsInProject(project, {member_id}): |
| 626 | return None, None |
| 627 | |
| 628 | extra_perms_view = ExtraPermsView() |
| 629 | # Find the index of the first extra_perms.member_id greater than or equal to |
| 630 | # member_id. |
| 631 | idx = bisect.bisect_left(extra_perms_view, member_id) |
| 632 | if idx >= len(project.extra_perms) or extra_perms_view[idx] > member_id: |
| 633 | return idx, None |
| 634 | return idx, project.extra_perms[idx] |
| 635 | |
| 636 | |
| 637 | def GetCustomPermissions(project): |
| 638 | """Return a sorted iterable of custom perms granted in a project.""" |
| 639 | custom_permissions = set() |
| 640 | for extra_perms in project.extra_perms: |
| 641 | for perm in extra_perms.perms: |
| 642 | if perm not in STANDARD_PERMISSIONS: |
| 643 | custom_permissions.add(perm) |
| 644 | |
| 645 | return sorted(custom_permissions) |
| 646 | |
| 647 | |
| 648 | def UserCanViewProject(user, effective_ids, project, expired_before=None): |
| 649 | """Return True if the user can view the given project. |
| 650 | |
| 651 | Args: |
| 652 | user: User protobuf for the user trying to view the project. |
| 653 | effective_ids: set of int user IDs of the user trying to view the project |
| 654 | (including any groups), or an empty set for anonymous users. |
| 655 | project: the Project protobuf to check. |
| 656 | expired_before: option time value for testing. |
| 657 | |
| 658 | Returns: |
| 659 | True if the user should be allowed to view the project. |
| 660 | """ |
| 661 | perms = GetPermissions(user, effective_ids, project) |
| 662 | |
| 663 | if IsExpired(project, expired_before=expired_before): |
| 664 | needed_perm = VIEW_EXPIRED_PROJECT |
| 665 | else: |
| 666 | needed_perm = VIEW |
| 667 | |
| 668 | return perms.CanUsePerm(needed_perm, effective_ids, project, []) |
| 669 | |
| 670 | |
| 671 | def IsExpired(project, expired_before=None): |
| 672 | """Return True if a project deletion has been pending long enough already. |
| 673 | |
| 674 | Args: |
| 675 | project: The project being viewed. |
| 676 | expired_before: If supplied, this method will return True only if the |
| 677 | project expired before the given time. |
| 678 | |
| 679 | Returns: |
| 680 | True if the project is eligible for reaping. |
| 681 | """ |
| 682 | if project.state != project_pb2.ProjectState.ARCHIVED: |
| 683 | return False |
| 684 | |
| 685 | if expired_before is None: |
| 686 | expired_before = int(time.time()) |
| 687 | |
| 688 | return project.delete_time and project.delete_time < expired_before |
| 689 | |
| 690 | |
| 691 | def CanDeleteComment(comment, commenter, user_id, perms): |
| 692 | """Returns true if the user can (un)delete the given comment. |
| 693 | |
| 694 | UpdateIssuePermissions must have been called first. |
| 695 | |
| 696 | Args: |
| 697 | comment: An IssueComment PB object. |
| 698 | commenter: An User PB object with the user who created the comment. |
| 699 | user_id: The ID of the user whose permission we want to check. |
| 700 | perms: The PermissionSet with the issue permissions. |
| 701 | |
| 702 | Returns: |
| 703 | True if the user can (un)delete the comment. |
| 704 | """ |
| 705 | # User is not logged in or has no permissions. |
| 706 | if not user_id or not perms: |
| 707 | return False |
| 708 | |
| 709 | # Nobody can (un)delete comments by banned users or spam comments, which |
| 710 | # should be un-flagged instead. |
| 711 | if commenter.banned or comment.is_spam: |
| 712 | return False |
| 713 | |
| 714 | # Site admin or project owners can delete any comment. |
| 715 | permit_delete_any = perms.HasPerm(DELETE_ANY, None, None, []) |
| 716 | if permit_delete_any: |
| 717 | return True |
| 718 | |
| 719 | # Users cannot undelete unless they deleted. |
| 720 | if comment.deleted_by and comment.deleted_by != user_id: |
| 721 | return False |
| 722 | |
| 723 | # Users can delete their own items. |
| 724 | permit_delete_own = perms.HasPerm(DELETE_OWN, None, None, []) |
| 725 | if permit_delete_own and comment.user_id == user_id: |
| 726 | return True |
| 727 | |
| 728 | return False |
| 729 | |
| 730 | |
| 731 | def CanFlagComment(comment, commenter, comment_reporters, user_id, perms): |
| 732 | """Returns true if the user can flag the given comment. |
| 733 | |
| 734 | UpdateIssuePermissions must have been called first. |
| 735 | Assumes that the user has permission to view the issue. |
| 736 | |
| 737 | Args: |
| 738 | comment: An IssueComment PB object. |
| 739 | commenter: An User PB object with the user who created the comment. |
| 740 | comment_reporters: A collection of user IDs who flagged the comment as spam. |
| 741 | user_id: The ID of the user for whom we're checking permissions. |
| 742 | perms: The PermissionSet with the issue permissions. |
| 743 | |
| 744 | Returns: |
| 745 | A tuple (can_flag, is_flagged). |
| 746 | can_flag is True if the user can flag the comment. and is_flagged is True |
| 747 | if the user sees the comment marked as spam. |
| 748 | """ |
| 749 | # Nobody can flag comments by banned users. |
| 750 | if commenter.banned: |
| 751 | return False, comment.is_spam |
| 752 | |
| 753 | # If a comment was deleted for a reason other than being spam, nobody can |
| 754 | # flag or un-flag it. |
| 755 | if comment.deleted_by and not comment.is_spam: |
| 756 | return False, comment.is_spam |
| 757 | |
| 758 | # A user with the VerdictSpam permission sees whether the comment is flagged |
| 759 | # as spam or not, and can mark it as flagged or un-flagged. |
| 760 | # If the comment is flagged as spam, all users see it as flagged, but only |
| 761 | # those with the VerdictSpam can un-flag it. |
| 762 | permit_verdict_spam = perms.HasPerm(VERDICT_SPAM, None, None, []) |
| 763 | if permit_verdict_spam or comment.is_spam: |
| 764 | return permit_verdict_spam, comment.is_spam |
| 765 | |
| 766 | # Otherwise, the comment is not marked as flagged and the user doesn't have |
| 767 | # the VerdictSpam permission. |
| 768 | # They are able to report a comment as spam if they have the FlagSpam |
| 769 | # permission, and they see the comment as flagged if the have previously |
| 770 | # reported it as spam. |
| 771 | permit_flag_spam = perms.HasPerm(FLAG_SPAM, None, None, []) |
| 772 | return permit_flag_spam, user_id in comment_reporters |
| 773 | |
| 774 | |
| 775 | def CanViewComment(comment, commenter, user_id, perms): |
| 776 | """Returns true if the user can view the given comment. |
| 777 | |
| 778 | UpdateIssuePermissions must have been called first. |
| 779 | Assumes that the user has permission to view the issue. |
| 780 | |
| 781 | Args: |
| 782 | comment: An IssueComment PB object. |
| 783 | commenter: An User PB object with the user who created the comment. |
| 784 | user_id: The ID of the user whose permission we want to check. |
| 785 | perms: The PermissionSet with the issue permissions. |
| 786 | |
| 787 | Returns: |
| 788 | True if the user can view the comment. |
| 789 | """ |
| 790 | # Nobody can view comments by banned users. |
| 791 | if commenter.banned: |
| 792 | return False |
| 793 | |
| 794 | # Only users with the permission to un-flag comments can view flagged |
| 795 | # comments. |
| 796 | if comment.is_spam: |
| 797 | # If the comment is marked as spam, whether the user can un-flag the comment |
| 798 | # or not doesn't depend on who reported it as spam. |
| 799 | can_flag, _ = CanFlagComment(comment, commenter, [], user_id, perms) |
| 800 | return can_flag |
| 801 | |
| 802 | # Only users with the permission to un-delete comments can view deleted |
| 803 | # comments. |
| 804 | if comment.deleted_by: |
| 805 | return CanDeleteComment(comment, commenter, user_id, perms) |
| 806 | |
| 807 | return True |
| 808 | |
| 809 | |
| 810 | def CanViewInboundMessage(comment, user_id, perms): |
| 811 | """Returns true if the user can view the given comment's inbound message. |
| 812 | |
| 813 | UpdateIssuePermissions must have been called first. |
| 814 | Assumes that the user has permission to view the comment. |
| 815 | |
| 816 | Args: |
| 817 | comment: An IssueComment PB object. |
| 818 | commenter: An User PB object with the user who created the comment. |
| 819 | user_id: The ID of the user whose permission we want to check. |
| 820 | perms: The PermissionSet with the issue permissions. |
| 821 | |
| 822 | Returns: |
| 823 | True if the user can view the comment's inbound message. |
| 824 | """ |
| 825 | return (perms.HasPerm(VIEW_INBOUND_MESSAGES, None, None, []) |
| 826 | or comment.user_id == user_id) |
| 827 | |
| 828 | |
| 829 | def CanView(effective_ids, perms, project, restrictions, granted_perms=None): |
| 830 | """Checks if user has permission to view an issue.""" |
| 831 | return perms.CanUsePerm( |
| 832 | VIEW, effective_ids, project, restrictions, granted_perms=granted_perms) |
| 833 | |
| 834 | |
| 835 | def CanCreateProject(perms): |
| 836 | """Return True if the given user may create a project. |
| 837 | |
| 838 | Args: |
| 839 | perms: Permissionset for the current user. |
| 840 | |
| 841 | Returns: |
| 842 | True if the user should be allowed to create a project. |
| 843 | """ |
| 844 | # "ANYONE" means anyone who has the needed perm. |
| 845 | if (settings.project_creation_restriction == |
| 846 | site_pb2.UserTypeRestriction.ANYONE): |
| 847 | return perms.HasPerm(CREATE_PROJECT, None, None) |
| 848 | |
| 849 | if (settings.project_creation_restriction == |
| 850 | site_pb2.UserTypeRestriction.ADMIN_ONLY): |
| 851 | return perms.HasPerm(ADMINISTER_SITE, None, None) |
| 852 | |
| 853 | return False |
| 854 | |
| 855 | |
| 856 | def CanCreateGroup(perms): |
| 857 | """Return True if the given user may create a user group. |
| 858 | |
| 859 | Args: |
| 860 | perms: Permissionset for the current user. |
| 861 | |
| 862 | Returns: |
| 863 | True if the user should be allowed to create a group. |
| 864 | """ |
| 865 | # "ANYONE" means anyone who has the needed perm. |
| 866 | if (settings.group_creation_restriction == |
| 867 | site_pb2.UserTypeRestriction.ANYONE): |
| 868 | return perms.HasPerm(CREATE_GROUP, None, None) |
| 869 | |
| 870 | if (settings.group_creation_restriction == |
| 871 | site_pb2.UserTypeRestriction.ADMIN_ONLY): |
| 872 | return perms.HasPerm(ADMINISTER_SITE, None, None) |
| 873 | |
| 874 | return False |
| 875 | |
| 876 | |
| 877 | def CanEditGroup(perms, effective_ids, group_owner_ids): |
| 878 | """Return True if the given user may edit a user group. |
| 879 | |
| 880 | Args: |
| 881 | perms: Permissionset for the current user. |
| 882 | effective_ids: set of user IDs for the logged in user. |
| 883 | group_owner_ids: set of user IDs of the user group owners. |
| 884 | |
| 885 | Returns: |
| 886 | True if the user should be allowed to edit the group. |
| 887 | """ |
| 888 | return (perms.HasPerm(EDIT_GROUP, None, None) or |
| 889 | not effective_ids.isdisjoint(group_owner_ids)) |
| 890 | |
| 891 | |
| 892 | def CanViewGroupMembers(perms, effective_ids, group_settings, member_ids, |
| 893 | owner_ids, user_project_ids): |
| 894 | """Return True if the given user may view a user group's members. |
| 895 | |
| 896 | Args: |
| 897 | perms: Permissionset for the current user. |
| 898 | effective_ids: set of user IDs for the logged in user. |
| 899 | group_settings: PB of UserGroupSettings. |
| 900 | member_ids: A list of member ids of this user group. |
| 901 | owner_ids: A list of owner ids of this user group. |
| 902 | user_project_ids: A list of project ids which the user has a role. |
| 903 | |
| 904 | Returns: |
| 905 | True if the user should be allowed to view the group's members. |
| 906 | """ |
| 907 | if perms.HasPerm(VIEW_GROUP, None, None): |
| 908 | return True |
| 909 | # The user could view this group with membership of some projects which are |
| 910 | # friends of the group. |
| 911 | if (group_settings.friend_projects and user_project_ids |
| 912 | and (set(group_settings.friend_projects) & set(user_project_ids))): |
| 913 | return True |
| 914 | visibility = group_settings.who_can_view_members |
| 915 | if visibility == usergroup_pb2.MemberVisibility.OWNERS: |
| 916 | return not effective_ids.isdisjoint(owner_ids) |
| 917 | elif visibility == usergroup_pb2.MemberVisibility.MEMBERS: |
| 918 | return (not effective_ids.isdisjoint(member_ids) or |
| 919 | not effective_ids.isdisjoint(owner_ids)) |
| 920 | else: |
| 921 | return True |
| 922 | |
| 923 | |
| 924 | def IsBanned(user, user_view): |
| 925 | """Return True if this user is banned from using our site.""" |
| 926 | if user is None: |
| 927 | return False # Anyone is welcome to browse |
| 928 | |
| 929 | if user.banned: |
| 930 | return True # We checked the "Banned" checkbox for this user. |
| 931 | |
| 932 | if user_view: |
| 933 | if user_view.domain in settings.banned_user_domains: |
| 934 | return True # Some spammers create many accounts with the same domain. |
| 935 | |
| 936 | if '+' in (user.email or ''): |
| 937 | # Spammers can make plus-addr Google accounts in unexpected domains. |
| 938 | return True |
| 939 | |
| 940 | return False |
| 941 | |
| 942 | |
| 943 | def CanBan(mr, services): |
| 944 | """Return True if the user is allowed to ban other users, site-wide.""" |
| 945 | if mr.perms.HasPerm(ADMINISTER_SITE, None, None): |
| 946 | return True |
| 947 | |
| 948 | owned, _, _ = services.project.GetUserRolesInAllProjects(mr.cnxn, |
| 949 | mr.auth.effective_ids) |
| 950 | return len(owned) > 0 |
| 951 | |
| 952 | |
| 953 | def CanExpungeUsers(mr): |
| 954 | """Return True is the user is allowed to delete user accounts.""" |
| 955 | return mr.perms.HasPerm(ADMINISTER_SITE, None, None) |
| 956 | |
| 957 | |
| 958 | def CanViewContributorList(mr, project): |
| 959 | """Return True if we should display the list project contributors. |
| 960 | |
| 961 | This is used on the project summary page, when deciding to offer the |
| 962 | project People page link, and when generating autocomplete options |
| 963 | that include project members. |
| 964 | |
| 965 | Args: |
| 966 | mr: commonly used info parsed from the request. |
| 967 | project: the Project we're interested in. |
| 968 | |
| 969 | Returns: |
| 970 | True if we should display the project contributor list. |
| 971 | """ |
| 972 | if not project: |
| 973 | return False # We are not even in a project context. |
| 974 | |
| 975 | if not project.only_owners_see_contributors: |
| 976 | return True # Contributor list is not resticted. |
| 977 | |
| 978 | # If it is hub-and-spoke, check for the perm that allows the user to |
| 979 | # view it anyway. |
| 980 | return mr.perms.HasPerm( |
| 981 | VIEW_CONTRIBUTOR_LIST, mr.auth.user_id, project) |
| 982 | |
| 983 | |
| 984 | def ShouldCheckForAbandonment(mr): |
| 985 | """Return True if user should be warned before changing/deleting their role. |
| 986 | |
| 987 | Args: |
| 988 | mr: common info parsed from the user's request. |
| 989 | |
| 990 | Returns: |
| 991 | True if user should be warned before changing/deleting their role. |
| 992 | """ |
| 993 | # Note: No need to warn admins because they won't lose access anyway. |
| 994 | if mr.perms.CanUsePerm( |
| 995 | ADMINISTER_SITE, mr.auth.effective_ids, mr.project, []): |
| 996 | return False |
| 997 | |
| 998 | return mr.perms.CanUsePerm( |
| 999 | EDIT_PROJECT, mr.auth.effective_ids, mr.project, []) |
| 1000 | |
| 1001 | |
| 1002 | # For speed, we remember labels that we have already classified as being |
| 1003 | # restriction labels or not being restriction labels. These sets are for |
| 1004 | # restrictions in general, not for any particular perm. |
| 1005 | _KNOWN_RESTRICTION_LABELS = set() |
| 1006 | _KNOWN_NON_RESTRICTION_LABELS = set() |
| 1007 | |
| 1008 | |
| 1009 | def IsRestrictLabel(label, perm=''): |
| 1010 | """Returns True if a given label is a restriction label. |
| 1011 | |
| 1012 | Args: |
| 1013 | label: string for the label to examine. |
| 1014 | perm: a permission that can be restricted (e.g. 'View' or 'Edit'). |
| 1015 | Defaults to '' to mean 'any'. |
| 1016 | |
| 1017 | Returns: |
| 1018 | True if a given label is a restriction label (of the specified perm) |
| 1019 | """ |
| 1020 | if label in _KNOWN_NON_RESTRICTION_LABELS: |
| 1021 | return False |
| 1022 | if not perm and label in _KNOWN_RESTRICTION_LABELS: |
| 1023 | return True |
| 1024 | |
| 1025 | prefix = ('restrict-%s-' % perm.lower()) if perm else 'restrict-' |
| 1026 | is_restrict = label.lower().startswith(prefix) and label.count('-') >= 2 |
| 1027 | |
| 1028 | if is_restrict: |
| 1029 | _KNOWN_RESTRICTION_LABELS.add(label) |
| 1030 | elif not perm: |
| 1031 | _KNOWN_NON_RESTRICTION_LABELS.add(label) |
| 1032 | |
| 1033 | return is_restrict |
| 1034 | |
| 1035 | |
| 1036 | def HasRestrictions(issue, perm=''): |
| 1037 | """Return True if the issue has any restrictions (on the specified perm).""" |
| 1038 | return ( |
| 1039 | any(IsRestrictLabel(lab, perm=perm) for lab in issue.labels) or |
| 1040 | any(IsRestrictLabel(lab, perm=perm) for lab in issue.derived_labels)) |
| 1041 | |
| 1042 | |
| 1043 | def GetRestrictions(issue, perm=''): |
| 1044 | """Return a list of restriction labels on the given issue.""" |
| 1045 | if not issue: |
| 1046 | return [] |
| 1047 | |
| 1048 | return [lab.lower() for lab in tracker_bizobj.GetLabels(issue) |
| 1049 | if IsRestrictLabel(lab, perm=perm)] |
| 1050 | |
| 1051 | |
| 1052 | def CanViewIssue( |
| 1053 | effective_ids, perms, project, issue, allow_viewing_deleted=False, |
| 1054 | granted_perms=None): |
| 1055 | """Checks if user has permission to view an artifact. |
| 1056 | |
| 1057 | Args: |
| 1058 | effective_ids: set of user IDs for the logged in user and any user |
| 1059 | group memberships. Should be an empty set for anon users. |
| 1060 | perms: PermissionSet for the user. |
| 1061 | project: Project PB for the project that contains this issue. |
| 1062 | issue: Issue PB for the issue being viewed. |
| 1063 | allow_viewing_deleted: True if the user should be allowed to view |
| 1064 | deleted artifacts. |
| 1065 | granted_perms: optional list of strings of permissions that the user is |
| 1066 | granted only within the scope of one issue, e.g., by being named in |
| 1067 | a user-type custom field that grants permissions. |
| 1068 | |
| 1069 | Returns: |
| 1070 | True iff the user can view the specified issue. |
| 1071 | """ |
| 1072 | if issue.deleted and not allow_viewing_deleted: |
| 1073 | return False |
| 1074 | |
| 1075 | perms = UpdateIssuePermissions( |
| 1076 | perms, project, issue, effective_ids, granted_perms=granted_perms) |
| 1077 | return perms.HasPerm(VIEW, None, None) |
| 1078 | |
| 1079 | |
| 1080 | def CanEditIssue(effective_ids, perms, project, issue, granted_perms=None): |
| 1081 | """Return True if a user can edit an issue. |
| 1082 | |
| 1083 | Args: |
| 1084 | effective_ids: set of user IDs for the logged in user and any user |
| 1085 | group memberships. Should be an empty set for anon users. |
| 1086 | perms: PermissionSet for the user. |
| 1087 | project: Project PB for the project that contains this issue. |
| 1088 | issue: Issue PB for the issue being viewed. |
| 1089 | granted_perms: optional list of strings of permissions that the user is |
| 1090 | granted only within the scope of one issue, e.g., by being named in |
| 1091 | a user-type custom field that grants permissions. |
| 1092 | |
| 1093 | Returns: |
| 1094 | True iff the user can edit the specified issue. |
| 1095 | """ |
| 1096 | perms = UpdateIssuePermissions( |
| 1097 | perms, project, issue, effective_ids, granted_perms=granted_perms) |
| 1098 | return perms.HasPerm(EDIT_ISSUE, None, None) |
| 1099 | |
| 1100 | |
| 1101 | def CanCommentIssue(effective_ids, perms, project, issue, granted_perms=None): |
| 1102 | """Return True if a user can comment on an issue.""" |
| 1103 | |
| 1104 | return perms.CanUsePerm( |
| 1105 | ADD_ISSUE_COMMENT, effective_ids, project, |
| 1106 | GetRestrictions(issue), granted_perms=granted_perms) |
| 1107 | |
| 1108 | |
| 1109 | def CanUpdateApprovalStatus( |
| 1110 | effective_ids, perms, project, approver_ids, new_status): |
| 1111 | """Return True if a user can change the approval status to the new status.""" |
| 1112 | if not effective_ids.isdisjoint(approver_ids): |
| 1113 | return True # Approval approvers can always change the approval status |
| 1114 | |
| 1115 | if new_status not in RESTRICTED_APPROVAL_STATUSES: |
| 1116 | return True |
| 1117 | |
| 1118 | return perms.CanUsePerm(EDIT_ISSUE_APPROVAL, effective_ids, project, []) |
| 1119 | |
| 1120 | |
| 1121 | def CanUpdateApprovers(effective_ids, perms, project, current_approver_ids): |
| 1122 | """Return True if a user can edit the list of approvers for an approval.""" |
| 1123 | if not effective_ids.isdisjoint(current_approver_ids): |
| 1124 | return True |
| 1125 | |
| 1126 | return perms.CanUsePerm(EDIT_ISSUE_APPROVAL, effective_ids, project, []) |
| 1127 | |
| 1128 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 1129 | def CanEditProjectConfig(mr, services): |
| 1130 | """ Special function to check if a user can edit a project config. |
| 1131 | |
| 1132 | This function accounts for special edge cases pertaining only to project |
| 1133 | configuration editing permissions, such as checking if a project is frozen |
| 1134 | for config edits or if a user is in the allowlist of users who can override |
| 1135 | a config freeze. |
| 1136 | |
| 1137 | Args: |
| 1138 | mr: MonorailRequest object. |
| 1139 | services: reference to database layer. |
| 1140 | |
| 1141 | Returns: |
| 1142 | True if the user can edit the project. |
| 1143 | """ |
| 1144 | if mr.project.project_id not in settings.config_freeze_project_ids: |
| 1145 | return mr.perms.CanUsePerm( |
| 1146 | EDIT_PROJECT, mr.auth.effective_ids, mr.project, []) |
| 1147 | |
| 1148 | effective_users = services.user.GetUsersByIDs( |
| 1149 | mr.cnxn, list(mr.auth.effective_ids)) |
| 1150 | |
| 1151 | for _, user in effective_users.items(): |
| 1152 | if user.email in settings.config_freeze_override_users.get( |
| 1153 | mr.project.project_id, {}): |
| 1154 | return True |
| 1155 | |
| 1156 | return False |
| 1157 | |
| 1158 | |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 1159 | def CanViewComponentDef(effective_ids, perms, project, component_def): |
| 1160 | """Return True if a user can view the given component definition.""" |
| 1161 | if not effective_ids.isdisjoint(component_def.admin_ids): |
| 1162 | return True # Component admins can view that component. |
| 1163 | |
| 1164 | # TODO(jrobbins): check restrictions on the component definition. |
| 1165 | return perms.CanUsePerm(VIEW, effective_ids, project, []) |
| 1166 | |
| 1167 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 1168 | def CanEditComponentDef(mr, services, component_def, config): |
| 1169 | """ Checks if the currently logged in user can edit a component. |
| 1170 | |
| 1171 | Args: |
| 1172 | mr: MonorailRequest object. |
| 1173 | services: reference to database layer. |
| 1174 | component_def: the component to check permissions for. |
| 1175 | config: project config of the project the component is in. |
| 1176 | |
| 1177 | Returns: |
| 1178 | True if a user can edit the given component definition.""" |
| 1179 | if mr.project.project_id in settings.config_freeze_project_ids: |
| 1180 | return CanEditProjectConfig(mr, services) |
| 1181 | |
| 1182 | if not mr.auth.effective_ids.isdisjoint(component_def.admin_ids): |
| 1183 | return True # Component admins can edit that component. |
| 1184 | |
| 1185 | # Check to see if user is admin of any parent component. |
| 1186 | parent_components = tracker_bizobj.FindAncestorComponents( |
| 1187 | config, component_def) |
| 1188 | for parent in parent_components: |
| 1189 | if not mr.auth.effective_ids.isdisjoint(parent.admin_ids): |
| 1190 | return True |
| 1191 | |
| 1192 | return CanEditProjectConfig(mr, services) |
| 1193 | |
| 1194 | |
| 1195 | def CanEditComponentDefLegacy( |
| 1196 | effective_ids, perms, project, component_def, config): |
| 1197 | """ Legacy version of CanEditComponentDef for codepaths without access to mr. |
| 1198 | This function is entirely used in API clients. |
| 1199 | |
| 1200 | Args: |
| 1201 | effective_ids: Set containing IDs for the user and their groups |
| 1202 | linked accounts, etc. |
| 1203 | perms: PermissionSet for current user. |
| 1204 | project: the project the component is in. |
| 1205 | component_def: the component to check permissions for. |
| 1206 | config: project config of the project the component is in. |
| 1207 | |
| 1208 | Returns: |
| 1209 | True if a user can edit the given component definition.""" |
| 1210 | # Do not bother checking if API client users are allowlisted to override |
| 1211 | # the config freeze. Only human users are currently being allowlisted. |
| 1212 | if project and project.project_id in settings.config_freeze_project_ids: |
| 1213 | return False |
| 1214 | |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 1215 | if not effective_ids.isdisjoint(component_def.admin_ids): |
| 1216 | return True # Component admins can edit that component. |
| 1217 | |
| 1218 | # Check to see if user is admin of any parent component. |
| 1219 | parent_components = tracker_bizobj.FindAncestorComponents( |
| 1220 | config, component_def) |
| 1221 | for parent in parent_components: |
| 1222 | if not effective_ids.isdisjoint(parent.admin_ids): |
| 1223 | return True |
| 1224 | |
| 1225 | return perms.CanUsePerm(EDIT_PROJECT, effective_ids, project, []) |
| 1226 | |
| 1227 | |
| 1228 | def CanViewFieldDef(effective_ids, perms, project, field_def): |
| 1229 | """Return True if a user can view the given field definition.""" |
| 1230 | if not effective_ids.isdisjoint(field_def.admin_ids): |
| 1231 | return True # Field admins can view that field. |
| 1232 | |
| 1233 | # TODO(jrobbins): check restrictions on the field definition. |
| 1234 | return perms.CanUsePerm(VIEW, effective_ids, project, []) |
| 1235 | |
| 1236 | |
| 1237 | def CanEditFieldDef(effective_ids, perms, project, field_def): |
| 1238 | """Return True if a user can edit the given field definition.""" |
| 1239 | if not effective_ids.isdisjoint(field_def.admin_ids): |
| 1240 | return True # Field admins can edit that field. |
| 1241 | |
| 1242 | return perms.CanUsePerm(EDIT_PROJECT, effective_ids, project, []) |
| 1243 | |
| 1244 | |
| 1245 | def CanEditValueForFieldDef(effective_ids, perms, project, field_def): |
| 1246 | """Return True if a user can edit the given field definition value. |
| 1247 | This method does not check that a user can edit the project issues.""" |
| 1248 | if not effective_ids: |
| 1249 | return False |
| 1250 | if not field_def.is_restricted_field: |
| 1251 | return True |
| 1252 | if not effective_ids.isdisjoint(field_def.editor_ids): |
| 1253 | return True |
| 1254 | return CanEditFieldDef(effective_ids, perms, project, field_def) |
| 1255 | |
| 1256 | |
| 1257 | def CanViewTemplate(effective_ids, perms, project, template): |
| 1258 | """Return True if a user can view the given issue template.""" |
| 1259 | if not effective_ids.isdisjoint(template.admin_ids): |
| 1260 | return True # template admins can view that template. |
| 1261 | |
| 1262 | # Members-only templates are only shown to members, other templates are |
| 1263 | # shown to any user that is generally allowed to view project content. |
| 1264 | if template.members_only: |
| 1265 | return framework_bizobj.UserIsInProject(project, effective_ids) |
| 1266 | else: |
| 1267 | return perms.CanUsePerm(VIEW, effective_ids, project, []) |
| 1268 | |
| 1269 | |
| 1270 | def CanEditTemplate(effective_ids, perms, project, template): |
| 1271 | """Return True if a user can edit the given field definition.""" |
| 1272 | if not effective_ids.isdisjoint(template.admin_ids): |
| 1273 | return True # Template admins can edit that template. |
| 1274 | |
| 1275 | return perms.CanUsePerm(EDIT_PROJECT, effective_ids, project, []) |
| 1276 | |
| 1277 | |
| 1278 | def CanViewHotlist(effective_ids, perms, hotlist): |
| 1279 | """Return True if a user can view the given hotlist.""" |
| 1280 | if not hotlist.is_private or perms.HasPerm(ADMINISTER_SITE, None, None): |
| 1281 | return True |
| 1282 | |
| 1283 | return any([user_id in (hotlist.owner_ids + hotlist.editor_ids) |
| 1284 | for user_id in effective_ids]) |
| 1285 | |
| 1286 | |
| 1287 | def CanEditHotlist(effective_ids, perms, hotlist): |
| 1288 | """Return True if a user is editor(add/remove issues and change rankings).""" |
| 1289 | return perms.HasPerm(ADMINISTER_SITE, None, None) or any( |
| 1290 | [user_id in (hotlist.owner_ids + hotlist.editor_ids) |
| 1291 | for user_id in effective_ids]) |
| 1292 | |
| 1293 | |
| 1294 | def CanAdministerHotlist(effective_ids, perms, hotlist): |
| 1295 | """Return True if user is owner(add/remove members, edit/delete hotlist).""" |
| 1296 | return perms.HasPerm(ADMINISTER_SITE, None, None) or any( |
| 1297 | [user_id in hotlist.owner_ids for user_id in effective_ids]) |
| 1298 | |
| 1299 | |
| 1300 | def CanCreateHotlist(perms): |
| 1301 | """Return True if the given user may create a hotlist. |
| 1302 | |
| 1303 | Args: |
| 1304 | perms: Permissionset for the current user. |
| 1305 | |
| 1306 | Returns: |
| 1307 | True if the user should be allowed to create a hotlist. |
| 1308 | """ |
| 1309 | if (settings.hotlist_creation_restriction == |
| 1310 | site_pb2.UserTypeRestriction.ANYONE): |
| 1311 | return perms.HasPerm(CREATE_HOTLIST, None, None) |
| 1312 | |
| 1313 | if (settings.hotlist_creation_restriction == |
| 1314 | site_pb2.UserTypeRestriction.ADMIN_ONLY): |
| 1315 | return perms.HasPerm(ADMINISTER_SITE, None, None) |
| 1316 | |
| 1317 | |
| 1318 | class Error(Exception): |
| 1319 | """Base class for errors from this module.""" |
| 1320 | |
| 1321 | |
| 1322 | class PermissionException(Error): |
| 1323 | """The user is not authorized to make the current request.""" |
| 1324 | |
| 1325 | |
| 1326 | class BannedUserException(Error): |
| 1327 | """The user has been banned from using our service.""" |