1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 from __future__ import with_statement
20
21 import csv
22 import os
23
24 from trac.admin import AdminCommandError, IAdminCommandProvider, get_dir_list
25 from trac.cache import cached
26 from trac.config import ExtensionOption, OrderedExtensionsOption
27 from trac.core import *
28 from trac.resource import Resource, get_resource_name
29 from trac.util import file_or_std
30 from trac.util.datefmt import time_now
31 from trac.util.text import path_to_unicode, print_table, printout, \
32 stream_encoding, to_unicode, wrap
33 from trac.util.translation import _, N_
34
35 __all__ = ['IPermissionRequestor', 'IPermissionStore', 'IPermissionPolicy',
36 'IPermissionGroupProvider', 'PermissionError', 'PermissionSystem']
40 """Insufficient permissions to perform the operation.
41
42 :since 1.0.5: the `msg` attribute is deprecated and will be removed in
43 1.3.1. Use the `message` property instead.
44 """
45
46 title = N_("Forbidden")
47
48 - def __init__(self, action=None, resource=None, env=None, msg=None):
49 self.action = action
50 self.resource = resource
51 self.env = env
52 if self.action:
53 if self.resource:
54 msg = _("%(perm)s privileges are required to perform "
55 "this operation on %(resource)s. You don't have the "
56 "required permissions.",
57 perm=self.action,
58 resource=get_resource_name(self.env, self.resource))
59 else:
60 msg = _("%(perm)s privileges are required to perform this "
61 "operation. You don't have the required "
62 "permissions.", perm=self.action)
63 elif msg is None:
64 msg = _("Insufficient privileges to perform this operation.")
65 self.msg = msg
66 super(PermissionError, self).__init__(msg)
67
68 @property
71
74 """Extension point interface for components that define actions."""
75
77 """Return a list of actions defined by this component.
78
79 The items in the list may either be simple strings, or
80 `(string, sequence)` tuples. The latter are considered to be "meta
81 permissions" that group several simple actions under one name for
82 convenience, adding to it if another component already defined that
83 name.
84 """
85
88 """Extension point interface for components that provide storage and
89 management of permissions."""
90
92 """Return all permissions for the user with the specified name.
93
94 The permissions are returned as a dictionary where the key is the name
95 of the permission, and the value is either `True` for granted
96 permissions or `False` for explicitly denied permissions."""
97
99 """Retrieve a list of users that have any of the specified permissions.
100
101 Users are returned as a list of usernames.
102 """
103
105 """Return all permissions for all users.
106
107 The permissions are returned as a list of (subject, action)
108 formatted tuples."""
109
111 """Grant a user permission to perform an action."""
112
114 """Revokes the permission of the given user to perform an action."""
115
118 """Extension point interface for components that provide information about
119 user groups.
120 """
121
123 """Return a list of names of the groups that the user with the specified
124 name is a member of."""
125
128 """A security policy provider used for fine grained permission checks."""
129
131 """Check that the action can be performed by username on the resource
132
133 :param action: the name of the permission
134 :param username: the username string or 'anonymous' if there's no
135 authenticated user
136 :param resource: the resource on which the check applies.
137 Will be `None`, if the check is a global one and
138 not made on a resource in particular
139 :param perm: the permission cache for that username and resource,
140 which can be used for doing secondary checks on other
141 permissions. Care must be taken to avoid recursion.
142
143 :return: `True` if action is allowed, `False` if action is denied,
144 or `None` if indifferent. If `None` is returned, the next
145 policy in the chain will be used, and so on.
146
147 Note that when checking a permission on a realm resource (i.e. when
148 `.id` is `None`), this usually corresponds to some preliminary check
149 done before making a fine-grained check on some resource.
150 Therefore the `IPermissionPolicy` should be conservative and return:
151
152 * `True` if the action *can* be allowed for some resources in
153 that realm. Later, for specific resource, the policy will be able
154 to return `True` (allow), `False` (deny) or `None` (don't decide).
155 * `None` if the action *can not* be performed for *some* resources.
156 This corresponds to situation where the policy is only interested
157 in returning `False` or `None` on specific resources.
158 * `False` if the action *can not* be performed for *any* resource in
159 that realm (that's a very strong decision as that will usually
160 prevent any fine-grained check to even happen).
161
162 Note that performing permission checks on realm resources may seem
163 redundant for now as the action name itself contains the realm, but
164 this will probably change in the future (e.g. `'VIEW' in ...`).
165 """
166
169 """Default implementation of permission storage and group management.
170
171 This component uses the `permission` table in the database to store both
172 permissions and groups.
173 """
174 implements(IPermissionStore)
175
176 group_providers = ExtensionPoint(IPermissionGroupProvider)
177
179 """Retrieve the permissions for the given user and return them in a
180 dictionary.
181
182 The permissions are stored in the database as (username, action)
183 records. There's simple support for groups by using lowercase names for
184 the action column: such a record represents a group and not an actual
185 permission, and declares that the user is part of that group.
186 """
187 subjects = set([username])
188 for provider in self.group_providers:
189 subjects.update(provider.get_permission_groups(username) or [])
190
191 actions = set()
192 perms = self._all_permissions
193 while True:
194 num_users = len(subjects)
195 num_actions = len(actions)
196 for user, action in perms:
197 if user in subjects:
198 if action.isupper() and action not in actions:
199 actions.add(action)
200 if not action.isupper() and action not in subjects:
201
202
203 subjects.add(action)
204 if num_users == len(subjects) and num_actions == len(actions):
205 break
206 return list(actions)
207
209 """Retrieve a list of users that have any of the specified permissions
210
211 Users are returned as a list of usernames.
212 """
213
214
215
216 result = set()
217 users = set([u[0] for u in self.env.get_known_users()])
218 for user in users:
219 userperms = self.get_user_permissions(user)
220 for group in permissions:
221 if group in userperms:
222 result.add(user)
223 return list(result)
224
226 """Return all permissions for all users.
227
228 The permissions are returned as a list of (subject, action)
229 formatted tuples."""
230 return self._all_permissions
231
232
233 @cached
237
239 """Grants a user the permission to perform the specified action."""
240 self.env.db_transaction("INSERT INTO permission VALUES (%s, %s)",
241 (username, action))
242 self.log.info("Granted permission for %s to %s", action, username)
243
244
245 del self._all_permissions
246
248 """Revokes a users' permission to perform the specified action."""
249 self.env.db_transaction(
250 "DELETE FROM permission WHERE username=%s AND action=%s",
251 (username, action))
252 self.log.info("Revoked permission for %s to %s", action, username)
253
254
255 del self._all_permissions
256
259 """Permission group provider providing the basic builtin permission groups
260 'anonymous' and 'authenticated'."""
261
262 required = True
263
264 implements(IPermissionGroupProvider)
265
267 groups = ['anonymous']
268 if username and username != 'anonymous':
269 groups.append('authenticated')
270 return groups
271
306
310 """Permission management sub-system."""
311
312 required = True
313
314 implements(IPermissionRequestor)
315
316 requestors = ExtensionPoint(IPermissionRequestor)
317
318 store = ExtensionOption('trac', 'permission_store', IPermissionStore,
319 'DefaultPermissionStore',
320 """Name of the component implementing `IPermissionStore`, which is used
321 for managing user and group permissions.""")
322
323 policies = OrderedExtensionsOption('trac', 'permission_policies',
324 IPermissionPolicy,
325 'DefaultPermissionPolicy, LegacyAttachmentPolicy',
326 False,
327 """List of components implementing `IPermissionPolicy`, in the order in
328 which they will be applied. These components manage fine-grained access
329 control to Trac resources.
330 Defaults to the DefaultPermissionPolicy (pre-0.11 behavior) and
331 LegacyAttachmentPolicy (map ATTACHMENT_* permissions to realm specific
332 ones)""")
333
334
335 CACHE_EXPIRY = 5
336
337 CACHE_REAP_TIME = 60
338
340 self.permission_cache = {}
341 self.last_reap = time_now()
342
343
344
346 """Grant the user with the given name permission to perform to specified
347 action."""
348 if action.isupper() and action not in self.get_actions():
349 raise TracError(_('%(name)s is not a valid action.', name=action))
350
351 self.store.grant_permission(username, action)
352
356
358 """Get all actions from permission requestors as a `dict`.
359
360 The keys are the action names. The values are the additional actions
361 granted by each action. For simple actions, this is an empty list.
362 For meta actions, this is the list of actions covered by the action.
363
364 :since 1.0.17: added `skip` argument.
365 """
366 actions = {}
367 for requestor in self.requestors:
368 if requestor is skip:
369 continue
370 for action in requestor.get_permission_actions() or []:
371 if isinstance(action, tuple):
372 actions.setdefault(action[0], []).extend(action[1])
373 else:
374 actions.setdefault(action, [])
375 return actions
376
378 """Get a list of all actions defined by permission requestors."""
379 actions = set()
380 for requestor in self.requestors:
381 if requestor is skip:
382 continue
383 for action in requestor.get_permission_actions() or []:
384 if isinstance(action, tuple):
385 actions.add(action[0])
386 else:
387 actions.add(action)
388 return list(actions)
389
391 """Return the permissions of the specified user.
392
393 The return value is a dictionary containing all the actions granted to
394 the user mapped to `True`. If an action is missing as a key, or has
395 `False` as a value, permission is denied."""
396 if not username:
397
398 return dict.fromkeys(self.get_actions(), True)
399
400
401 actions = self.get_actions_dict()
402 permissions = {}
403 def expand_meta(action):
404 if action not in permissions:
405 permissions[action] = True
406 for a in actions.get(action, ()):
407 expand_meta(a)
408 for perm in self.store.get_user_permissions(username) or []:
409 expand_meta(perm)
410 return permissions
411
413 """Return all permissions for all users.
414
415 The permissions are returned as a list of (subject, action)
416 formatted tuples."""
417 return self.store.get_all_permissions() or []
418
420 """Return all users that have the specified permission.
421
422 Users are returned as a list of user names.
423 """
424 now = time_now()
425 if now - self.last_reap > self.CACHE_REAP_TIME:
426 self.permission_cache = {}
427 self.last_reap = now
428 timestamp, permissions = self.permission_cache.get(permission,
429 (0, None))
430 if now - timestamp <= self.CACHE_EXPIRY:
431 return permissions
432
433 parent_map = {}
434 for parent, children in self.get_actions_dict().iteritems():
435 for child in children:
436 parent_map.setdefault(child, set()).add(parent)
437
438 satisfying_perms = set()
439 def append_with_parents(action):
440 if action not in satisfying_perms:
441 satisfying_perms.add(action)
442 for action in parent_map.get(action, ()):
443 append_with_parents(action)
444 append_with_parents(permission)
445
446 perms = self.store.get_users_with_permissions(satisfying_perms) or []
447 self.permission_cache[permission] = (now, perms)
448 return perms
449
451 """Helper method for expanding all meta actions."""
452 all_actions = self.get_actions_dict()
453 expanded_actions = set()
454 def expand_action(action):
455 if action not in expanded_actions:
456 expanded_actions.add(action)
457 for a in all_actions.get(action, ()):
458 expand_action(a)
459 for a in actions:
460 expand_action(a)
461 return expanded_actions
462
464 """Return True if permission to perform action for the given resource
465 is allowed."""
466 if username is None:
467 username = 'anonymous'
468 if resource and resource.realm is None:
469 resource = None
470 for policy in self.policies:
471 decision = policy.check_permission(action, username, resource,
472 perm)
473 if decision is not None:
474 self.log.debug("%s %s %s performing %s on %r",
475 policy.__class__.__name__,
476 'allows' if decision else 'denies',
477 username, action, resource)
478 return decision
479 self.log.debug("No policy allowed %s performing %s on %r",
480 username, action, resource)
481 return False
482
483
484
486 """Implement the global `TRAC_ADMIN` meta permission.
487
488 Implements also the `EMAIL_VIEW` permission which allows for
489 showing email addresses even if `[trac] show_email_addresses`
490 is `false`.
491 """
492 actions = self.get_actions(skip=self)
493 actions.append('EMAIL_VIEW')
494 return [('TRAC_ADMIN', actions), 'EMAIL_VIEW']
495
498 """Cache that maintains the permissions of a single user.
499
500 Permissions are usually checked using the following syntax:
501
502 'WIKI_MODIFY' in perm
503
504 One can also apply more fine grained permission checks and
505 specify a specific resource for which the permission should be available:
506
507 'WIKI_MODIFY' in perm('wiki', 'WikiStart')
508
509 If there's already a `page` object available, the check is simply:
510
511 'WIKI_MODIFY' in perm(page.resource)
512
513 If instead of a check, one wants to assert that a given permission is
514 available, the following form should be used:
515
516 perm.require('WIKI_MODIFY')
517
518 or
519
520 perm('wiki', 'WikiStart').require('WIKI_MODIFY')
521
522 or
523
524 perm(page.resource).require('WIKI_MODIFY')
525
526 When using `require`, a `PermissionError` exception is raised if the
527 permission is missing.
528 """
529
530 __slots__ = ('env', 'username', '_resource', '_cache')
531
532 - def __init__(self, env, username=None, resource=None, cache=None,
533 groups=None):
540
542 if realm_or_resource:
543 return Resource(realm_or_resource, id, version)
544 else:
545 return self._resource
546
547 - def __call__(self, realm_or_resource, id=False, version=False):
548 """Convenience function for using thus:
549 'WIKI_VIEW' in perm(context)
550 or
551 'WIKI_VIEW' in perm(realm, id, version)
552 or
553 'WIKI_VIEW' in perm(resource)
554
555 """
556 resource = Resource(realm_or_resource, id, version)
557 if resource and self._resource and resource == self._resource:
558 return self
559 else:
560 return PermissionCache(self.env, self.username, resource,
561 self._cache)
562
563 - def has_permission(self, action, realm_or_resource=None, id=False,
564 version=False):
567
569 key = (self.username, hash(resource), action)
570 cached = self._cache.get(key)
571 if cached:
572 cache_decision, cache_resource = cached
573 if resource == cache_resource:
574 return cache_decision
575 perm = self
576 if resource is not self._resource:
577 perm = PermissionCache(self.env, self.username, resource,
578 self._cache)
579 decision = PermissionSystem(self.env). \
580 check_permission(action, perm.username, resource, perm)
581 self._cache[key] = (decision, resource)
582 return decision
583
584 __contains__ = has_permission
585
586 - def require(self, action, realm_or_resource=None, id=False, version=False,
587 message=None):
594 assert_permission = require
595
597 """Deprecated (but still used by the HDF compatibility layer)"""
598 self.env.log.warning("perm.permissions() is deprecated and "
599 "is only present for HDF compatibility")
600 perm = PermissionSystem(self.env)
601 actions = perm.get_user_permissions(self.username)
602 return [action for action in actions if action in self]
603
606 """trac-admin command provider for permission system administration."""
607
608 implements(IAdminCommandProvider)
609
610
611
613 yield ('permission list', '[user]',
614 'List permission rules',
615 self._complete_list, self._do_list)
616 yield ('permission add', '<user> <action> [action] [...]',
617 'Add a new permission rule',
618 self._complete_add, self._do_add)
619 yield ('permission remove', '<user> <action> [action] [...]',
620 'Remove a permission rule',
621 self._complete_remove, self._do_remove)
622 yield ('permission export', '[file]',
623 'Export permission rules to a file or stdout as CSV',
624 self._complete_import_export, self._do_export)
625 yield ('permission import', '[file]',
626 'Import permission rules from a file or stdin as CSV',
627 self._complete_import_export, self._do_import)
628
632
637
641
648
654
658
679
680 - def _do_add(self, user, *actions):
681 permsys = PermissionSystem(self.env)
682 if user.isupper():
683 raise AdminCommandError(_('All upper-cased tokens are reserved '
684 'for permission names'))
685 for action in actions:
686 try:
687 permsys.grant_permission(user, action)
688 except self.env.db_exc.IntegrityError:
689 printout(_("The user %(user)s already has permission "
690 "%(action)s.", user=user, action=action))
691
693 permsys = PermissionSystem(self.env)
694 rows = permsys.get_all_permissions()
695 for action in actions:
696 found = False
697 for u, a in rows:
698 if user in (u, '*') and action in (a, '*'):
699 permsys.revoke_permission(u, a)
700 found = True
701 if not found:
702 if user in self.get_user_list() and \
703 action in permsys.get_user_permissions(user):
704 msg = _("Cannot remove permission %(action)s for user "
705 "%(user)s. The permission is granted through "
706 "a meta-permission or group.", action=action,
707 user=user)
708 else:
709 msg = _("Cannot remove permission %(action)s for user "
710 "%(user)s. The user has not been granted the "
711 "permission.", action=action, user=user)
712 raise AdminCommandError(msg)
713
730
732 permsys = PermissionSystem(self.env)
733 try:
734 with file_or_std(filename, 'rb') as f:
735 encoding = stream_encoding(f)
736 linesep = os.linesep if filename else '\n'
737 reader = csv.reader(f, lineterminator=linesep)
738 for row in reader:
739 if len(row) < 2:
740 raise AdminCommandError(
741 _("Invalid row %(line)d. Expected <user>, "
742 "<action>, [action], [...]",
743 line=reader.line_num))
744 user = to_unicode(row[0], encoding)
745 actions = [to_unicode(action, encoding)
746 for action in row[1:]]
747 if user.isupper():
748 raise AdminCommandError(
749 _("Invalid user %(user)s on line %(line)d: All "
750 "upper-cased tokens are reserved for permission "
751 "names.", user=user, line=reader.line_num))
752 old_actions = self.get_user_perms(user)
753 for action in set(actions) - set(old_actions):
754 permsys.grant_permission(user, action)
755 except csv.Error, e:
756 raise AdminCommandError(
757 _("Cannot import from %(filename)s line %(line)d: %(error)s ",
758 filename=path_to_unicode(filename or 'stdin'),
759 line=reader.line_num, error=e))
760 except IOError, e:
761 raise AdminCommandError(
762 _("Cannot import from %(filename)s: %(error)s",
763 filename=path_to_unicode(filename or 'stdin'),
764 error=e.strerror))
765