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