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