OLD | NEW |
(Empty) | |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is govered by a BSD-style |
| 3 # license that can be found in the LICENSE file or at |
| 4 # https://developers.google.com/open-source/licenses/bsd |
| 5 |
| 6 """Classes and functions to implement permission checking. |
| 7 |
| 8 The main data structure is a simple map from (user role, project status, |
| 9 project_access_level) to specific perms. |
| 10 |
| 11 A perm is simply a string that indicates that the user has a given |
| 12 permission. The servlets and templates can test whether the current |
| 13 user has permission to see a UI element or perform an action by |
| 14 testing for the presence of the corresponding perm in the user's |
| 15 permission set. |
| 16 |
| 17 The user role is one of admin, owner, member, outsider user, or anon. |
| 18 The project status is one of the project states defined in project_pb2, |
| 19 or a special constant defined below. Likewise for access level. |
| 20 """ |
| 21 |
| 22 import logging |
| 23 import time |
| 24 |
| 25 from third_party import ezt |
| 26 |
| 27 import settings |
| 28 from framework import framework_bizobj |
| 29 from framework import framework_constants |
| 30 from proto import project_pb2 |
| 31 from proto import site_pb2 |
| 32 from proto import usergroup_pb2 |
| 33 from tracker import tracker_bizobj |
| 34 |
| 35 # Constants that define permissions. |
| 36 # Note that perms with a leading "_" can never be granted |
| 37 # to users who are not site admins. |
| 38 VIEW = 'View' |
| 39 EDIT_PROJECT = 'EditProject' |
| 40 CREATE_PROJECT = 'CreateProject' |
| 41 PUBLISH_PROJECT = '_PublishProject' # for making "doomed" projects LIVE |
| 42 VIEW_DEBUG = '_ViewDebug' # on-page debugging info |
| 43 EDIT_OTHER_USERS = '_EditOtherUsers' # can edit other user's prefs, ban, etc. |
| 44 CUSTOMIZE_PROCESS = 'CustomizeProcess' # can use some enterprise features |
| 45 VIEW_EXPIRED_PROJECT = '_ViewExpiredProject' # view long-deleted projects |
| 46 # View the list of contributors even in hub-and-spoke projects. |
| 47 VIEW_CONTRIBUTOR_LIST = 'ViewContributorList' |
| 48 |
| 49 # Quota |
| 50 VIEW_QUOTA = 'ViewQuota' |
| 51 EDIT_QUOTA = 'EditQuota' |
| 52 |
| 53 # Permissions for editing user groups |
| 54 CREATE_GROUP = 'CreateGroup' |
| 55 EDIT_GROUP = 'EditGroup' |
| 56 DELETE_GROUP = 'DeleteGroup' |
| 57 VIEW_GROUP = 'ViewGroup' |
| 58 |
| 59 # Perms for Source tools |
| 60 # TODO(jrobbins): Monorail is just issue tracking with no version control, so |
| 61 # phase out use of the term "Commit", sometime after Monorail's initial launch. |
| 62 COMMIT = 'Commit' |
| 63 |
| 64 # Perms for issue tracking |
| 65 CREATE_ISSUE = 'CreateIssue' |
| 66 EDIT_ISSUE = 'EditIssue' |
| 67 EDIT_ISSUE_OWNER = 'EditIssueOwner' |
| 68 EDIT_ISSUE_SUMMARY = 'EditIssueSummary' |
| 69 EDIT_ISSUE_STATUS = 'EditIssueStatus' |
| 70 EDIT_ISSUE_CC = 'EditIssueCc' |
| 71 DELETE_ISSUE = 'DeleteIssue' |
| 72 ADD_ISSUE_COMMENT = 'AddIssueComment' |
| 73 VIEW_INBOUND_MESSAGES = 'ViewInboundMessages' |
| 74 # Note, there is no separate DELETE_ATTACHMENT perm. We |
| 75 # allow a user to delete an attachment iff they could soft-delete |
| 76 # the comment that holds the attachment. |
| 77 |
| 78 # Note: the "_" in the perm name makes it impossible for a |
| 79 # project owner to grant it to anyone as an extra perm. |
| 80 ADMINISTER_SITE = '_AdministerSite' |
| 81 |
| 82 # Permissions to soft-delete artifact comment |
| 83 DELETE_ANY = 'DeleteAny' |
| 84 DELETE_OWN = 'DeleteOwn' |
| 85 |
| 86 # Granting this allows owners to delegate some team management work. |
| 87 EDIT_ANY_MEMBER_NOTES = 'EditAnyMemberNotes' |
| 88 |
| 89 # Permission to star/unstar any artifact. |
| 90 SET_STAR = 'SetStar' |
| 91 |
| 92 # Permission to flag any artifact as spam. |
| 93 FLAG_SPAM = 'FlagSpam' |
| 94 VERDICT_SPAM = 'VerdictSpam' |
| 95 MODERATE_SPAM = 'ModerateSpam' |
| 96 |
| 97 STANDARD_ADMIN_PERMISSIONS = [ |
| 98 EDIT_PROJECT, CREATE_PROJECT, PUBLISH_PROJECT, VIEW_DEBUG, |
| 99 EDIT_OTHER_USERS, CUSTOMIZE_PROCESS, |
| 100 VIEW_QUOTA, EDIT_QUOTA, ADMINISTER_SITE, |
| 101 EDIT_ANY_MEMBER_NOTES, VERDICT_SPAM, MODERATE_SPAM] |
| 102 |
| 103 STANDARD_ISSUE_PERMISSIONS = [ |
| 104 VIEW, EDIT_ISSUE, ADD_ISSUE_COMMENT, DELETE_ISSUE, FLAG_SPAM] |
| 105 |
| 106 # Monorail has no source control, but keep COMMIT for backward compatability. |
| 107 STANDARD_SOURCE_PERMISSIONS = [COMMIT] |
| 108 |
| 109 STANDARD_COMMENT_PERMISSIONS = [DELETE_OWN, DELETE_ANY] |
| 110 |
| 111 STANDARD_OTHER_PERMISSIONS = [CREATE_ISSUE, FLAG_SPAM, SET_STAR] |
| 112 |
| 113 STANDARD_PERMISSIONS = (STANDARD_ADMIN_PERMISSIONS + |
| 114 STANDARD_ISSUE_PERMISSIONS + |
| 115 STANDARD_SOURCE_PERMISSIONS + |
| 116 STANDARD_COMMENT_PERMISSIONS + |
| 117 STANDARD_OTHER_PERMISSIONS) |
| 118 |
| 119 # roles |
| 120 SITE_ADMIN_ROLE = 'admin' |
| 121 OWNER_ROLE = 'owner' |
| 122 COMMITTER_ROLE = 'committer' |
| 123 CONTRIBUTOR_ROLE = 'contributor' |
| 124 USER_ROLE = 'user' |
| 125 ANON_ROLE = 'anon' |
| 126 |
| 127 # Project state out-of-band values for keys |
| 128 UNDEFINED_STATUS = 'undefined_status' |
| 129 UNDEFINED_ACCESS = 'undefined_access' |
| 130 WILDCARD_ACCESS = 'wildcard_access' |
| 131 |
| 132 |
| 133 class PermissionSet(object): |
| 134 """Class to represent the set of permissions available to the user.""" |
| 135 |
| 136 def __init__(self, perm_names, consider_restrictions=True): |
| 137 """Create a PermissionSet with the given permissions. |
| 138 |
| 139 Args: |
| 140 perm_names: a list of permission name strings. |
| 141 consider_restrictions: if true, the user's permissions can be blocked |
| 142 by restriction labels on an artifact. Project owners and site |
| 143 admins do not consider restrictions so that they cannot |
| 144 "lock themselves out" of editing an issue. |
| 145 """ |
| 146 self.perm_names = frozenset(p.lower() for p in perm_names) |
| 147 self.consider_restrictions = consider_restrictions |
| 148 |
| 149 def __getattr__(self, perm_name): |
| 150 """Easy permission testing in EZT. E.g., [if-any perms.format_drive].""" |
| 151 return ezt.boolean(self.HasPerm(perm_name, None, None)) |
| 152 |
| 153 def CanUsePerm( |
| 154 self, perm_name, effective_ids, project, restriction_labels, |
| 155 granted_perms=None): |
| 156 """Return True if the user can use the given permission. |
| 157 |
| 158 Args: |
| 159 perm_name: string name of permission, e.g., 'EditIssue'. |
| 160 effective_ids: set of int user IDs for the user (including any groups), |
| 161 or an empty set if user is not signed in. |
| 162 project: Project PB for the project being accessed, or None if not |
| 163 in a project. |
| 164 restriction_labels: list of strings that restrict permission usage. |
| 165 granted_perms: optional list of lowercase strings of permissions that the |
| 166 user is granted only within the scope of one issue, e.g., by being |
| 167 named in a user-type custom field that grants permissions. |
| 168 |
| 169 Restriction labels have 3 parts, e.g.: |
| 170 'Restrict-EditIssue-InnerCircle' blocks the use of just the |
| 171 EditIssue permission, unless the user also has the InnerCircle |
| 172 permission. This allows fine-grained restrictions on specific |
| 173 actions, such as editing, commenting, or deleting. |
| 174 |
| 175 Restriction labels and permissions are case-insensitive. |
| 176 |
| 177 Returns: |
| 178 True if the user can use the given permission, or False |
| 179 if they cannot (either because they don't have that permission |
| 180 or because it is blocked by a relevant restriction label). |
| 181 """ |
| 182 # TODO(jrobbins): room for performance improvement: avoid set creation and |
| 183 # repeated string operations. |
| 184 granted_perms = granted_perms or set() |
| 185 perm_lower = perm_name.lower() |
| 186 if perm_lower in granted_perms: |
| 187 return True |
| 188 |
| 189 needed_perms = {perm_lower} |
| 190 if self.consider_restrictions: |
| 191 for label in restriction_labels: |
| 192 label = label.lower() |
| 193 # format: Restrict-Action-ToThisPerm |
| 194 _kw, requested_perm, needed_perm = label.split('-', 2) |
| 195 if requested_perm == perm_lower and needed_perm not in granted_perms: |
| 196 needed_perms.add(needed_perm) |
| 197 |
| 198 if not effective_ids: |
| 199 effective_ids = {framework_constants.NO_USER_SPECIFIED} |
| 200 # Id X might have perm A and Y might have B, if both A and B are needed |
| 201 # True should be returned. |
| 202 for perm in needed_perms: |
| 203 if not any( |
| 204 self.HasPerm(perm, user_id, project) for user_id in effective_ids): |
| 205 return False |
| 206 |
| 207 return True |
| 208 |
| 209 def HasPerm(self, perm_name, user_id, project): |
| 210 """Return True if the user has the given permission (ignoring user groups). |
| 211 |
| 212 Args: |
| 213 perm_name: string name of permission, e.g., 'EditIssue'. |
| 214 user_id: int user id of the user, or None if user is not signed in. |
| 215 project: Project PB for the project being accessed, or None if not |
| 216 in a project. |
| 217 |
| 218 Returns: |
| 219 True if the user has the given perm. |
| 220 """ |
| 221 # TODO(jrobbins): room for performance improvement: pre-compute |
| 222 # extra perms (maybe merge them into the perms object), avoid |
| 223 # redundant call to lower(). |
| 224 extra_perms = [p.lower() for p in GetExtraPerms(project, user_id)] |
| 225 perm_name = perm_name.lower() |
| 226 return perm_name in self.perm_names or perm_name in extra_perms |
| 227 |
| 228 def DebugString(self): |
| 229 """Return a useful string to show when debugging.""" |
| 230 return 'PermissionSet(%s)' % ', '.join(sorted(self.perm_names)) |
| 231 |
| 232 def __repr__(self): |
| 233 return '%s(%r)' % (self.__class__.__name__, self.perm_names) |
| 234 |
| 235 |
| 236 EMPTY_PERMISSIONSET = PermissionSet([]) |
| 237 |
| 238 READ_ONLY_PERMISSIONSET = PermissionSet([VIEW]) |
| 239 |
| 240 USER_PERMISSIONSET = PermissionSet([ |
| 241 VIEW, FLAG_SPAM, SET_STAR, |
| 242 CREATE_ISSUE, ADD_ISSUE_COMMENT, |
| 243 DELETE_OWN]) |
| 244 |
| 245 CONTRIBUTOR_ACTIVE_PERMISSIONSET = PermissionSet( |
| 246 [VIEW, |
| 247 FLAG_SPAM, SET_STAR, |
| 248 CREATE_ISSUE, ADD_ISSUE_COMMENT, |
| 249 DELETE_OWN]) |
| 250 |
| 251 CONTRIBUTOR_INACTIVE_PERMISSIONSET = PermissionSet( |
| 252 [VIEW]) |
| 253 |
| 254 COMMITTER_ACTIVE_PERMISSIONSET = PermissionSet( |
| 255 [VIEW, COMMIT, VIEW_CONTRIBUTOR_LIST, |
| 256 FLAG_SPAM, SET_STAR, VIEW_QUOTA, |
| 257 CREATE_ISSUE, ADD_ISSUE_COMMENT, EDIT_ISSUE, VIEW_INBOUND_MESSAGES, |
| 258 DELETE_OWN]) |
| 259 |
| 260 COMMITTER_INACTIVE_PERMISSIONSET = PermissionSet( |
| 261 [VIEW, VIEW_CONTRIBUTOR_LIST, |
| 262 VIEW_INBOUND_MESSAGES, VIEW_QUOTA]) |
| 263 |
| 264 OWNER_ACTIVE_PERMISSIONSET = PermissionSet( |
| 265 [VIEW, VIEW_CONTRIBUTOR_LIST, EDIT_PROJECT, COMMIT, |
| 266 FLAG_SPAM, VERDICT_SPAM, SET_STAR, VIEW_QUOTA, |
| 267 CREATE_ISSUE, ADD_ISSUE_COMMENT, EDIT_ISSUE, DELETE_ISSUE, |
| 268 VIEW_INBOUND_MESSAGES, |
| 269 DELETE_ANY, EDIT_ANY_MEMBER_NOTES], |
| 270 consider_restrictions=False) |
| 271 |
| 272 OWNER_INACTIVE_PERMISSIONSET = PermissionSet( |
| 273 [VIEW, VIEW_CONTRIBUTOR_LIST, EDIT_PROJECT, |
| 274 VIEW_INBOUND_MESSAGES, VIEW_QUOTA], |
| 275 consider_restrictions=False) |
| 276 |
| 277 ADMIN_PERMISSIONSET = PermissionSet( |
| 278 [VIEW, VIEW_CONTRIBUTOR_LIST, |
| 279 CREATE_PROJECT, EDIT_PROJECT, PUBLISH_PROJECT, VIEW_DEBUG, |
| 280 COMMIT, CUSTOMIZE_PROCESS, FLAG_SPAM, VERDICT_SPAM, SET_STAR, |
| 281 ADMINISTER_SITE, VIEW_EXPIRED_PROJECT, EDIT_OTHER_USERS, |
| 282 VIEW_QUOTA, EDIT_QUOTA, |
| 283 CREATE_ISSUE, ADD_ISSUE_COMMENT, EDIT_ISSUE, DELETE_ISSUE, |
| 284 VIEW_INBOUND_MESSAGES, |
| 285 DELETE_ANY, EDIT_ANY_MEMBER_NOTES, |
| 286 CREATE_GROUP, EDIT_GROUP, DELETE_GROUP, VIEW_GROUP, |
| 287 MODERATE_SPAM], |
| 288 consider_restrictions=False) |
| 289 |
| 290 GROUP_IMPORT_BORG_PERMISSIONSET = PermissionSet( |
| 291 [CREATE_GROUP, VIEW_GROUP, EDIT_GROUP]) |
| 292 |
| 293 |
| 294 # Permissions for project pages, e.g., the project summary page |
| 295 _PERMISSIONS_TABLE = { |
| 296 |
| 297 # Project owners can view and edit artifacts in a LIVE project. |
| 298 (OWNER_ROLE, project_pb2.ProjectState.LIVE, WILDCARD_ACCESS): |
| 299 OWNER_ACTIVE_PERMISSIONSET, |
| 300 |
| 301 # Project owners can view, but not edit artifacts in ARCHIVED. |
| 302 # Note: EDIT_PROJECT is not enough permission to change an ARCHIVED project |
| 303 # back to LIVE if a delete_time was set. |
| 304 (OWNER_ROLE, project_pb2.ProjectState.ARCHIVED, WILDCARD_ACCESS): |
| 305 OWNER_INACTIVE_PERMISSIONSET, |
| 306 |
| 307 # Project members can view their own project, regardless of state. |
| 308 (COMMITTER_ROLE, project_pb2.ProjectState.LIVE, WILDCARD_ACCESS): |
| 309 COMMITTER_ACTIVE_PERMISSIONSET, |
| 310 (COMMITTER_ROLE, project_pb2.ProjectState.ARCHIVED, WILDCARD_ACCESS): |
| 311 COMMITTER_INACTIVE_PERMISSIONSET, |
| 312 |
| 313 # Project contributors can view their own project, regardless of state. |
| 314 (CONTRIBUTOR_ROLE, project_pb2.ProjectState.LIVE, WILDCARD_ACCESS): |
| 315 CONTRIBUTOR_ACTIVE_PERMISSIONSET, |
| 316 (CONTRIBUTOR_ROLE, project_pb2.ProjectState.ARCHIVED, WILDCARD_ACCESS): |
| 317 CONTRIBUTOR_INACTIVE_PERMISSIONSET, |
| 318 |
| 319 # Non-members users can read and comment in projects with access == ANYONE |
| 320 (USER_ROLE, project_pb2.ProjectState.LIVE, |
| 321 project_pb2.ProjectAccess.ANYONE): |
| 322 USER_PERMISSIONSET, |
| 323 |
| 324 # Anonymous users can only read projects with access == ANYONE. |
| 325 (ANON_ROLE, project_pb2.ProjectState.LIVE, |
| 326 project_pb2.ProjectAccess.ANYONE): |
| 327 READ_ONLY_PERMISSIONSET, |
| 328 |
| 329 # Permissions for site pages, e.g., creating a new project |
| 330 (USER_ROLE, UNDEFINED_STATUS, UNDEFINED_ACCESS): |
| 331 PermissionSet([CREATE_PROJECT, CREATE_GROUP]), |
| 332 } |
| 333 |
| 334 |
| 335 def GetPermissions(user, effective_ids, project): |
| 336 """Return a permission set appropriate for the user and project. |
| 337 |
| 338 Args: |
| 339 user: The User PB for the signed-in user, or None for anon users. |
| 340 effective_ids: set of int user IDs for the current user and all user |
| 341 groups that s/he is a member of. This will be an empty set for |
| 342 anonymous users. |
| 343 project: either a Project protobuf, or None for a page whose scope is |
| 344 wider than a single project. |
| 345 |
| 346 Returns: |
| 347 a PermissionSet object for the current user and project (or for |
| 348 site-wide operations if project is None). |
| 349 |
| 350 If an exact match for the user's role and project status is found, that is |
| 351 returned. Otherwise, we look for permissions for the user's role that is |
| 352 not specific to any project status, or not specific to any project access |
| 353 level. If neither of those are defined, we give the user an empty |
| 354 permission set. |
| 355 """ |
| 356 # Site admins get ADMIN_PERMISSIONSET regardless of groups or projects. |
| 357 if user and user.is_site_admin: |
| 358 return ADMIN_PERMISSIONSET |
| 359 |
| 360 # Grant the borg job permission to view/edit groups |
| 361 if user and user.email == settings.borg_service_account: |
| 362 return GROUP_IMPORT_BORG_PERMISSIONSET |
| 363 |
| 364 # Anon users don't need to accumulate anything. |
| 365 if not effective_ids: |
| 366 role, status, access = _GetPermissionKey(None, project) |
| 367 return _LookupPermset(role, status, access) |
| 368 |
| 369 effective_perms = set() |
| 370 consider_restrictions = True |
| 371 |
| 372 # Check for signed-in user with no roles in the current project. |
| 373 if not project or not framework_bizobj.UserIsInProject( |
| 374 project, effective_ids): |
| 375 role, status, access = _GetPermissionKey(None, project) |
| 376 return _LookupPermset(USER_ROLE, status, access) |
| 377 |
| 378 # Signed-in user gets the union of all his/her PermissionSets from the table. |
| 379 for user_id in effective_ids: |
| 380 role, status, access = _GetPermissionKey(user_id, project) |
| 381 role_perms = _LookupPermset(role, status, access) |
| 382 # Accumulate a union of all the user's permissions. |
| 383 effective_perms.update(role_perms.perm_names) |
| 384 # If any role allows the user to ignore restriction labels, then |
| 385 # ignore them overall. |
| 386 if not role_perms.consider_restrictions: |
| 387 consider_restrictions = False |
| 388 |
| 389 return PermissionSet( |
| 390 effective_perms, consider_restrictions=consider_restrictions) |
| 391 |
| 392 |
| 393 def _LookupPermset(role, status, access): |
| 394 """Lookup the appropriate PermissionSet in _PERMISSIONS_TABLE. |
| 395 |
| 396 Args: |
| 397 role: a string indicating the user's role in the project. |
| 398 status: a Project PB status value, or UNDEFINED_STATUS. |
| 399 access: a Project PB access value, or UNDEFINED_ACCESS. |
| 400 |
| 401 Returns: |
| 402 A PermissionSet that is appropriate for that kind of user in that |
| 403 project context. |
| 404 """ |
| 405 if (role, status, access) in _PERMISSIONS_TABLE: |
| 406 return _PERMISSIONS_TABLE[(role, status, access)] |
| 407 elif (role, status, WILDCARD_ACCESS) in _PERMISSIONS_TABLE: |
| 408 return _PERMISSIONS_TABLE[(role, status, WILDCARD_ACCESS)] |
| 409 else: |
| 410 return EMPTY_PERMISSIONSET |
| 411 |
| 412 |
| 413 def _GetPermissionKey(user_id, project, expired_before=None): |
| 414 """Return a permission lookup key appropriate for the user and project.""" |
| 415 if user_id is None: |
| 416 role = ANON_ROLE |
| 417 elif project and IsExpired(project, expired_before=expired_before): |
| 418 role = USER_ROLE # Do not honor roles in expired projects. |
| 419 elif project and user_id in project.owner_ids: |
| 420 role = OWNER_ROLE |
| 421 elif project and user_id in project.committer_ids: |
| 422 role = COMMITTER_ROLE |
| 423 elif project and user_id in project.contributor_ids: |
| 424 role = CONTRIBUTOR_ROLE |
| 425 else: |
| 426 role = USER_ROLE |
| 427 |
| 428 # TODO(jrobbins): re-implement same_org |
| 429 |
| 430 if project is None: |
| 431 status = UNDEFINED_STATUS |
| 432 else: |
| 433 status = project.state |
| 434 |
| 435 if project is None: |
| 436 access = UNDEFINED_ACCESS |
| 437 else: |
| 438 access = project.access |
| 439 |
| 440 return role, status, access |
| 441 |
| 442 |
| 443 def GetExtraPerms(project, member_id): |
| 444 """Return a list of extra perms for the user in the project. |
| 445 |
| 446 Args: |
| 447 project: Project PB for the current project. |
| 448 member_id: user id of a project owner, member, or contributor. |
| 449 |
| 450 Returns: |
| 451 A list of strings for the extra perms granted to the |
| 452 specified user in this project. The list will often be empty. |
| 453 """ |
| 454 |
| 455 extra_perms = FindExtraPerms(project, member_id) |
| 456 |
| 457 if extra_perms: |
| 458 return list(extra_perms.perms) |
| 459 else: |
| 460 return [] |
| 461 |
| 462 |
| 463 def FindExtraPerms(project, member_id): |
| 464 """Return a ExtraPerms PB for the given user in the project. |
| 465 |
| 466 Args: |
| 467 project: Project PB for the current project, or None if the user is |
| 468 not currently in a project. |
| 469 member_id: user ID of a project owner, member, or contributor. |
| 470 |
| 471 Returns: |
| 472 An ExtraPerms PB, or None. |
| 473 """ |
| 474 if not project: |
| 475 # TODO(jrobbins): maybe define extra perms for site-wide operations. |
| 476 return None |
| 477 |
| 478 # Users who have no current role cannot have any extra perms. Don't |
| 479 # consider effective_ids (which includes user groups) for this check. |
| 480 if not framework_bizobj.UserIsInProject(project, {member_id}): |
| 481 return None |
| 482 |
| 483 for extra_perms in project.extra_perms: |
| 484 if extra_perms.member_id == member_id: |
| 485 return extra_perms |
| 486 |
| 487 return None |
| 488 |
| 489 |
| 490 def GetCustomPermissions(project): |
| 491 """Return a sorted iterable of custom perms granted in a project.""" |
| 492 custom_permissions = set() |
| 493 for extra_perms in project.extra_perms: |
| 494 for perm in extra_perms.perms: |
| 495 if perm not in STANDARD_PERMISSIONS: |
| 496 custom_permissions.add(perm) |
| 497 |
| 498 return sorted(custom_permissions) |
| 499 |
| 500 |
| 501 def UserCanViewProject(user, effective_ids, project, expired_before=None): |
| 502 """Return True if the user can view the given project. |
| 503 |
| 504 Args: |
| 505 user: User protobuf for the user trying to view the project. |
| 506 effective_ids: set of int user IDs of the user trying to view the project |
| 507 (including any groups), or an empty set for anonymous users. |
| 508 project: the Project protobuf to check. |
| 509 expired_before: option time value for testing. |
| 510 |
| 511 Returns: |
| 512 True if the user should be allowed to view the project. |
| 513 """ |
| 514 perms = GetPermissions(user, effective_ids, project) |
| 515 |
| 516 if IsExpired(project, expired_before=expired_before): |
| 517 needed_perm = VIEW_EXPIRED_PROJECT |
| 518 else: |
| 519 needed_perm = VIEW |
| 520 |
| 521 return perms.CanUsePerm(needed_perm, effective_ids, project, []) |
| 522 |
| 523 |
| 524 def IsExpired(project, expired_before=None): |
| 525 """Return True if a project deletion has been pending long enough already. |
| 526 |
| 527 Args: |
| 528 project: The project being viewed. |
| 529 expired_before: If supplied, this method will return True only if the |
| 530 project expired before the given time. |
| 531 |
| 532 Returns: |
| 533 True if the project is eligible for reaping. |
| 534 """ |
| 535 if project.state != project_pb2.ProjectState.ARCHIVED: |
| 536 return False |
| 537 |
| 538 if expired_before is None: |
| 539 expired_before = int(time.time()) |
| 540 |
| 541 return project.delete_time and project.delete_time < expired_before |
| 542 |
| 543 |
| 544 def CanDelete(logged_in_user_id, effective_ids, perms, deleted_by_user_id, |
| 545 creator_user_id, project, restrictions, granted_perms=None): |
| 546 """Returns true if user has delete permission. |
| 547 |
| 548 Args: |
| 549 logged_in_user_id: int user id of the logged in user. |
| 550 effective_ids: set of int user IDs for the user (including any groups), |
| 551 or an empty set if user is not signed in. |
| 552 perms: instance of PermissionSet describing the current user's permissions. |
| 553 deleted_by_user_id: int user ID of the user having previously deleted this |
| 554 comment, or None, if the comment has never been deleted. |
| 555 creator_user_id: int user ID of the user having created this comment. |
| 556 project: Project PB for the project being accessed, or None if not |
| 557 in a project. |
| 558 restrictions: list of strings that restrict permission usage. |
| 559 granted_perms: optional list of strings of permissions that the user is |
| 560 granted only within the scope of one issue, e.g., by being named in |
| 561 a user-type custom field that grants permissions. |
| 562 |
| 563 Returns: |
| 564 True if the logged in user has delete permissions. |
| 565 """ |
| 566 |
| 567 # User is not logged in or has no permissions. |
| 568 if not logged_in_user_id or not perms: |
| 569 return False |
| 570 |
| 571 # Site admin or project owners can delete any comment. |
| 572 permit_delete_any = perms.CanUsePerm( |
| 573 DELETE_ANY, effective_ids, project, restrictions, |
| 574 granted_perms=granted_perms) |
| 575 if permit_delete_any: |
| 576 return True |
| 577 |
| 578 # Users cannot undelete unless they deleted. |
| 579 if deleted_by_user_id and deleted_by_user_id != logged_in_user_id: |
| 580 return False |
| 581 |
| 582 # Users can delete their own items. |
| 583 permit_delete_own = perms.CanUsePerm( |
| 584 DELETE_OWN, effective_ids, project, restrictions) |
| 585 if permit_delete_own and creator_user_id == logged_in_user_id: |
| 586 return True |
| 587 |
| 588 return False |
| 589 |
| 590 |
| 591 def CanView(effective_ids, perms, project, restrictions, granted_perms=None): |
| 592 """Checks if user has permission to view an issue.""" |
| 593 return perms.CanUsePerm( |
| 594 VIEW, effective_ids, project, restrictions, granted_perms=granted_perms) |
| 595 |
| 596 |
| 597 def CanCreateProject(perms): |
| 598 """Return True if the given user may create a project. |
| 599 |
| 600 Args: |
| 601 perms: Permissionset for the current user. |
| 602 |
| 603 Returns: |
| 604 True if the user should be allowed to create a project. |
| 605 """ |
| 606 # "ANYONE" means anyone who has the needed perm. |
| 607 if (settings.project_creation_restriction == |
| 608 site_pb2.UserTypeRestriction.ANYONE): |
| 609 return perms.HasPerm(CREATE_PROJECT, None, None) |
| 610 |
| 611 if (settings.project_creation_restriction == |
| 612 site_pb2.UserTypeRestriction.ADMIN_ONLY): |
| 613 return perms.HasPerm(ADMINISTER_SITE, None, None) |
| 614 |
| 615 return False |
| 616 |
| 617 |
| 618 def CanCreateGroup(perms): |
| 619 """Return True if the given user may create a user group. |
| 620 |
| 621 Args: |
| 622 perms: Permissionset for the current user. |
| 623 |
| 624 Returns: |
| 625 True if the user should be allowed to create a group. |
| 626 """ |
| 627 # "ANYONE" means anyone who has the needed perm. |
| 628 if (settings.group_creation_restriction == |
| 629 site_pb2.UserTypeRestriction.ANYONE): |
| 630 return perms.HasPerm(CREATE_GROUP, None, None) |
| 631 |
| 632 if (settings.group_creation_restriction == |
| 633 site_pb2.UserTypeRestriction.ADMIN_ONLY): |
| 634 return perms.HasPerm(ADMINISTER_SITE, None, None) |
| 635 |
| 636 return False |
| 637 |
| 638 |
| 639 def CanEditGroup(perms, effective_ids, group_owner_ids): |
| 640 """Return True if the given user may edit a user group. |
| 641 |
| 642 Args: |
| 643 perms: Permissionset for the current user. |
| 644 effective_ids: set of user IDs for the logged in user. |
| 645 group_owner_ids: set of user IDs of the user group owners. |
| 646 |
| 647 Returns: |
| 648 True if the user should be allowed to edit the group. |
| 649 """ |
| 650 return (perms.HasPerm(EDIT_GROUP, None, None) or |
| 651 not effective_ids.isdisjoint(group_owner_ids)) |
| 652 |
| 653 |
| 654 def CanViewGroup(perms, effective_ids, group_settings, member_ids, owner_ids, |
| 655 user_project_ids): |
| 656 """Return True if the given user may view a user group. |
| 657 |
| 658 Args: |
| 659 perms: Permissionset for the current user. |
| 660 effective_ids: set of user IDs for the logged in user. |
| 661 group_settings: PB of UserGroupSettings. |
| 662 member_ids: A list of member ids of this user group. |
| 663 owner_ids: A list of owner ids of this user group. |
| 664 user_project_ids: A list of project ids which the user has a role. |
| 665 |
| 666 Returns: |
| 667 True if the user should be allowed to view the group. |
| 668 """ |
| 669 if perms.HasPerm(VIEW_GROUP, None, None): |
| 670 return True |
| 671 # The user could view this group with membership of some projects which are |
| 672 # friends of the group. |
| 673 if (group_settings.friend_projects and user_project_ids |
| 674 and (set(group_settings.friend_projects) & set(user_project_ids))): |
| 675 return True |
| 676 visibility = group_settings.who_can_view_members |
| 677 if visibility == usergroup_pb2.MemberVisibility.OWNERS: |
| 678 return not effective_ids.isdisjoint(owner_ids) |
| 679 elif visibility == usergroup_pb2.MemberVisibility.MEMBERS: |
| 680 return (not effective_ids.isdisjoint(member_ids) or |
| 681 not effective_ids.isdisjoint(owner_ids)) |
| 682 else: |
| 683 return True |
| 684 |
| 685 |
| 686 def IsBanned(user, user_view): |
| 687 """Return True if this user is banned from using our site.""" |
| 688 if user is None: |
| 689 return False # Anyone is welcome to browse |
| 690 |
| 691 if user.banned: |
| 692 return True # We checked the "Banned" checkbox for this user. |
| 693 |
| 694 if user_view: |
| 695 if user_view.domain in settings.banned_user_domains: |
| 696 return True # Some spammers create many accounts with the same domain. |
| 697 |
| 698 return False |
| 699 |
| 700 |
| 701 def CanViewContributorList(mr): |
| 702 """Return True if we should display the list project contributors. |
| 703 |
| 704 This is used on the project summary page, when deciding to offer the |
| 705 project People page link, and when generating autocomplete options |
| 706 that include project members. |
| 707 |
| 708 Args: |
| 709 mr: commonly used info parsed from the request. |
| 710 |
| 711 Returns: |
| 712 True if we should display the project contributor list. |
| 713 """ |
| 714 if not mr.project: |
| 715 return False # We are not even in a project context. |
| 716 |
| 717 if not mr.project.only_owners_see_contributors: |
| 718 return True # Contributor list is not resticted. |
| 719 |
| 720 # If it is hub-and-spoke, check for the perm that allows the user to |
| 721 # view it anyway. |
| 722 return mr.perms.HasPerm( |
| 723 VIEW_CONTRIBUTOR_LIST, mr.auth.user_id, mr.project) |
| 724 |
| 725 |
| 726 def ShouldCheckForAbandonment(mr): |
| 727 """Return True if user should be warned before changing/deleting their role. |
| 728 |
| 729 Args: |
| 730 mr: common info parsed from the user's request. |
| 731 |
| 732 Returns: |
| 733 True if user should be warned before changing/deleting their role. |
| 734 """ |
| 735 # Note: No need to warn admins because they won't lose access anyway. |
| 736 if mr.perms.CanUsePerm( |
| 737 ADMINISTER_SITE, mr.auth.effective_ids, mr.project, []): |
| 738 return False |
| 739 |
| 740 return mr.perms.CanUsePerm( |
| 741 EDIT_PROJECT, mr.auth.effective_ids, mr.project, []) |
| 742 |
| 743 |
| 744 # For speed, we remember labels that we have already classified as being |
| 745 # restriction labels or not being restriction labels. These sets are for |
| 746 # restrictions in general, not for any particular perm. |
| 747 _KNOWN_RESTRICTION_LABELS = set() |
| 748 _KNOWN_NON_RESTRICTION_LABELS = set() |
| 749 |
| 750 |
| 751 def IsRestrictLabel(label, perm=''): |
| 752 """Returns True if a given label is a restriction label. |
| 753 |
| 754 Args: |
| 755 label: string for the label to examine. |
| 756 perm: a permission that can be restricted (e.g. 'View' or 'Edit'). |
| 757 Defaults to '' to mean 'any'. |
| 758 |
| 759 Returns: |
| 760 True if a given label is a restriction label (of the specified perm) |
| 761 """ |
| 762 if label in _KNOWN_NON_RESTRICTION_LABELS: |
| 763 return False |
| 764 if not perm and label in _KNOWN_RESTRICTION_LABELS: |
| 765 return True |
| 766 |
| 767 prefix = ('restrict-%s-' % perm.lower()) if perm else 'restrict-' |
| 768 is_restrict = label.lower().startswith(prefix) and label.count('-') >= 2 |
| 769 |
| 770 if is_restrict: |
| 771 _KNOWN_RESTRICTION_LABELS.add(label) |
| 772 elif not perm: |
| 773 _KNOWN_NON_RESTRICTION_LABELS.add(label) |
| 774 |
| 775 return is_restrict |
| 776 |
| 777 |
| 778 def HasRestrictions(issue, perm=''): |
| 779 """Return True if the issue has any restrictions (on the specified perm).""" |
| 780 return ( |
| 781 any(IsRestrictLabel(lab, perm=perm) for lab in issue.labels) or |
| 782 any(IsRestrictLabel(lab, perm=perm) for lab in issue.derived_labels)) |
| 783 |
| 784 |
| 785 def GetRestrictions(issue): |
| 786 """Return a list of restriction labels on the given issue.""" |
| 787 if not issue: |
| 788 return [] |
| 789 |
| 790 return [lab.lower() for lab in tracker_bizobj.GetLabels(issue) |
| 791 if IsRestrictLabel(lab)] |
| 792 |
| 793 |
| 794 def CanViewIssue( |
| 795 effective_ids, perms, project, issue, allow_viewing_deleted=False, |
| 796 granted_perms=None): |
| 797 """Checks if user has permission to view an artifact. |
| 798 |
| 799 Args: |
| 800 effective_ids: set of user IDs for the logged in user and any user |
| 801 group memberships. Should be an empty set for anon users. |
| 802 perms: PermissionSet for the user. |
| 803 project: Project PB for the project that contains this issue. |
| 804 issue: Issue PB for the issue being viewed. |
| 805 allow_viewing_deleted: True if the user should be allowed to view |
| 806 deleted artifacts. |
| 807 granted_perms: optional list of strings of permissions that the user is |
| 808 granted only within the scope of one issue, e.g., by being named in |
| 809 a user-type custom field that grants permissions. |
| 810 |
| 811 Returns: |
| 812 True iff the user can view the specified issue. |
| 813 """ |
| 814 if issue.deleted and not allow_viewing_deleted: |
| 815 # No one can view a deleted issue. If the user can undelete, that |
| 816 # goes through the custom 404 page. |
| 817 return False |
| 818 |
| 819 # Check to see if the user can view anything in the project. |
| 820 if not perms.CanUsePerm(VIEW, effective_ids, project, []): |
| 821 return False |
| 822 |
| 823 if not HasRestrictions(issue): |
| 824 return True |
| 825 |
| 826 return CanViewRestrictedIssueInVisibleProject( |
| 827 effective_ids, perms, project, issue, granted_perms=granted_perms) |
| 828 |
| 829 |
| 830 def CanViewRestrictedIssueInVisibleProject( |
| 831 effective_ids, perms, project, issue, granted_perms=None): |
| 832 """Return True if the user can view this issue. Assumes project is OK.""" |
| 833 # The reporter, owner, and CC'd users can always see the issue. |
| 834 # In effect, these fields override artifact restriction labels. |
| 835 if effective_ids: |
| 836 if (issue.reporter_id in effective_ids or |
| 837 tracker_bizobj.GetOwnerId(issue) in effective_ids or |
| 838 not effective_ids.isdisjoint(tracker_bizobj.GetCcIds(issue))): |
| 839 return True |
| 840 |
| 841 # Otherwise, apply the usual permission checking. |
| 842 return CanView( |
| 843 effective_ids, perms, project, GetRestrictions(issue), |
| 844 granted_perms=granted_perms) |
| 845 |
| 846 |
| 847 def CanEditIssue(effective_ids, perms, project, issue, granted_perms=None): |
| 848 """Return True if a user can edit an issue. |
| 849 |
| 850 Args: |
| 851 effective_ids: set of user IDs for the logged in user and any user |
| 852 group memberships. Should be an empty set for anon users. |
| 853 perms: PermissionSet for the user. |
| 854 project: Project PB for the project that contains this issue. |
| 855 issue: Issue PB for the issue being viewed. |
| 856 granted_perms: optional list of strings of permissions that the user is |
| 857 granted only within the scope of one issue, e.g., by being named in |
| 858 a user-type custom field that grants permissions. |
| 859 |
| 860 Returns: |
| 861 True iff the user can edit the specified issue. |
| 862 """ |
| 863 # TODO(jrobbins): We need to actually grant View+EditIssue in most cases. |
| 864 # So, always grant View whenever there is any granted perm. |
| 865 if not CanViewIssue( |
| 866 effective_ids, perms, project, issue, granted_perms=granted_perms): |
| 867 return False |
| 868 |
| 869 # The issue owner can always edit the issue. |
| 870 if effective_ids: |
| 871 if tracker_bizobj.GetOwnerId(issue) in effective_ids: |
| 872 return True |
| 873 |
| 874 # Otherwise, apply the usual permission checking. |
| 875 return perms.CanUsePerm( |
| 876 EDIT_ISSUE, effective_ids, project, GetRestrictions(issue), |
| 877 granted_perms=granted_perms) |
| 878 |
| 879 |
| 880 def CanCommentIssue(effective_ids, perms, project, issue, granted_perms=None): |
| 881 """Return True if a user can comment on an issue.""" |
| 882 |
| 883 return perms.CanUsePerm( |
| 884 ADD_ISSUE_COMMENT, effective_ids, project, |
| 885 GetRestrictions(issue), granted_perms=granted_perms) |
| 886 |
| 887 |
| 888 def CanViewComponentDef(effective_ids, perms, project, component_def): |
| 889 """Return True if a user can view the given component definition.""" |
| 890 if not effective_ids.isdisjoint(component_def.admin_ids): |
| 891 return True # Component admins can view that component. |
| 892 |
| 893 # TODO(jrobbins): check restrictions on the component definition. |
| 894 return perms.CanUsePerm(VIEW, effective_ids, project, []) |
| 895 |
| 896 |
| 897 def CanEditComponentDef(effective_ids, perms, project, component_def, config): |
| 898 """Return True if a user can edit the given component definition.""" |
| 899 if not effective_ids.isdisjoint(component_def.admin_ids): |
| 900 return True # Component admins can edit that component. |
| 901 |
| 902 # Check to see if user is admin of any parent component. |
| 903 parent_components = tracker_bizobj.FindAncestorComponents( |
| 904 config, component_def) |
| 905 for parent in parent_components: |
| 906 if not effective_ids.isdisjoint(parent.admin_ids): |
| 907 return True |
| 908 |
| 909 return perms.CanUsePerm(EDIT_PROJECT, effective_ids, project, []) |
| 910 |
| 911 |
| 912 def CanViewFieldDef(effective_ids, perms, project, field_def): |
| 913 """Return True if a user can view the given field definition.""" |
| 914 if not effective_ids.isdisjoint(field_def.admin_ids): |
| 915 return True # Field admins can view that field. |
| 916 |
| 917 # TODO(jrobbins): check restrictions on the field definition. |
| 918 return perms.CanUsePerm(VIEW, effective_ids, project, []) |
| 919 |
| 920 |
| 921 def CanEditFieldDef(effective_ids, perms, project, field_def): |
| 922 """Return True if a user can edit the given field definition.""" |
| 923 if not effective_ids.isdisjoint(field_def.admin_ids): |
| 924 return True # Field admins can edit that field. |
| 925 |
| 926 return perms.CanUsePerm(EDIT_PROJECT, effective_ids, project, []) |
| 927 |
| 928 |
| 929 def CanViewTemplate(effective_ids, perms, project, template): |
| 930 """Return True if a user can view the given issue template.""" |
| 931 if not effective_ids.isdisjoint(template.admin_ids): |
| 932 return True # template admins can view that template. |
| 933 |
| 934 # Members-only templates are only shown to members, other templates are |
| 935 # shown to any user that is generally allowed to view project content. |
| 936 if template.members_only: |
| 937 return framework_bizobj.UserIsInProject(project, effective_ids) |
| 938 else: |
| 939 return perms.CanUsePerm(VIEW, effective_ids, project, []) |
| 940 |
| 941 |
| 942 def CanEditTemplate(effective_ids, perms, project, template): |
| 943 """Return True if a user can edit the given field definition.""" |
| 944 if not effective_ids.isdisjoint(template.admin_ids): |
| 945 return True # Template admins can edit that template. |
| 946 |
| 947 return perms.CanUsePerm(EDIT_PROJECT, effective_ids, project, []) |
| 948 |
| 949 |
| 950 class Error(Exception): |
| 951 """Base class for errors from this module.""" |
| 952 |
| 953 |
| 954 class PermissionException(Error): |
| 955 """The user is not authorized to make the current request.""" |
| 956 |
| 957 |
| 958 class BannedUserException(Error): |
| 959 """The user has been banned from using our service.""" |
OLD | NEW |