Edgewall Software

TracDev/ContextRefactoring: trac_perm.py

File trac_perm.py, 20.9 KB (added by cboos, 14 months ago)

Work in progress snapshot - the modifications to source:trunk/trac/perm.py are mainly to be found near the end of the file, in the PermissionCache class.

Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2003-2005 Edgewall Software
4# Copyright (C) 2003-2004 Jonas Borgström <jonas@edgewall.com>
5# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
6# All rights reserved.
7#
8# This software is licensed as described in the file COPYING, which
9# you should have received as part of this distribution. The terms
10# are also available at http://trac.edgewall.org/wiki/TracLicense.
11#
12# This software consists of voluntary contributions made by many
13# individuals. For the exact contribution history, see the revision
14# history and logs, available at http://trac.edgewall.org/log/.
15#
16# Author: Jonas Borgström <jonas@edgewall.com>
17#         Christopher Lenz <cmlenz@gmx.de>
18
19"""Management of permissions."""
20
21from trac.config import ExtensionOption, OrderedExtensionsOption
22from trac.context import Resource
23from trac.core import *
24from trac.util.compat import set
25from trac.util.translation import _
26
27__all__ = ['IPermissionRequestor', 'IPermissionStore',
28           'IPermissionGroupProvider', 'PermissionError', 'PermissionSystem']
29
30
31class PermissionError(StandardError):
32    """Insufficient permissions to complete the operation"""
33
34    def __init__ (self, action=None, resource=None):
35        StandardError.__init__(self)
36        self.action = action
37        self.resource = resource
38
39    def __str__ (self):
40        if self.action:
41            if self.resource:
42                return _('%(perm)s privileges are required to perform '
43                         'this operation on %(resource)s',
44                         perm=self.action, resource=self.resource)
45            else:
46                return _('%(perm)s privileges are required to perform '
47                         'this operation', perm=self.action)
48        else:
49            return _('Insufficient privileges to perform this operation.')
50
51
52class IPermissionRequestor(Interface):
53    """Extension point interface for components that define actions."""
54
55    def get_permission_actions():
56        """Return a list of actions defined by this component.
57       
58        The items in the list may either be simple strings, or
59        `(string, sequence)` tuples. The latter are considered to be "meta
60        permissions" that group several simple actions under one name for
61        convenience.
62        """
63
64
65class IPermissionStore(Interface):
66    """Extension point interface for components that provide storage and
67    management of permissions."""
68
69    def get_user_permissions(username):
70        """Return all permissions for the user with the specified name.
71       
72        The permissions are returned as a dictionary where the key is the name
73        of the permission, and the value is either `True` for granted
74        permissions or `False` for explicitly denied permissions."""
75
76    def get_users_with_permissions(self, permissions):
77        """Retrieve a list of users that have any of the specified permissions.
78
79        Users are returned as a list of usernames.
80        """
81
82    def get_all_permissions():
83        """Return all permissions for all users.
84
85        The permissions are returned as a list of (subject, action)
86        formatted tuples."""
87
88    def grant_permission(username, action):
89        """Grant a user permission to perform an action."""
90
91    def revoke_permission(username, action):
92        """Revokes the permission of the given user to perform an action."""
93
94
95class IPermissionGroupProvider(Interface):
96    """Extension point interface for components that provide information about
97    user groups.
98    """
99
100    def get_permission_groups(username):
101        """Return a list of names of the groups that the user with the specified
102        name is a member of."""
103
104
105class IPermissionPolicy(Interface):
106    """A security policy provider."""
107
108    def check_permission(username, action, resource):
109        """Check that username can perform action on resource.
110
111        Must return True if action is allowed, False if action is denied, or
112        None if indifferent. If None is returned, the next policy in the chain
113        will be used, and so on."""
114
115
116class DefaultPermissionStore(Component):
117    """Default implementation of permission storage and simple group management.
118   
119    This component uses the `PERMISSION` table in the database to store both
120    permissions and groups.
121    """
122    implements(IPermissionStore)
123
124    group_providers = ExtensionPoint(IPermissionGroupProvider)
125
126    def get_user_permissions(self, username):
127        """Retrieve the permissions for the given user and return them in a
128        dictionary.
129       
130        The permissions are stored in the database as (username, action)
131        records. There's simple support for groups by using lowercase names for
132        the action column: such a record represents a group and not an actual
133        permission, and declares that the user is part of that group.
134        """
135        subjects = set([username])
136        for provider in self.group_providers:
137            subjects.update(provider.get_permission_groups(username))
138
139        actions = set([])
140        db = self.env.get_db_cnx()
141        cursor = db.cursor()
142        cursor.execute("SELECT username,action FROM permission")
143        rows = cursor.fetchall()
144        while True:
145            num_users = len(subjects)
146            num_actions = len(actions)
147            for user, action in rows:
148                if user in subjects:
149                    if action.isupper() and action not in actions:
150                        actions.add(action)
151                    if not action.isupper() and action not in subjects:
152                        # action is actually the name of the permission group
153                        # here
154                        subjects.add(action)
155            if num_users == len(subjects) and num_actions == len(actions):
156                break
157        return list(actions)
158
159    def get_users_with_permissions(self, permissions):
160        """Retrieve a list of users that have any of the specified permissions
161       
162        Users are returned as a list of usernames.
163        """
164        # get_user_permissions() takes care of the magic 'authenticated' group.
165        # The optimized loop we had before didn't.  This is very inefficient,
166        # but it works.
167        db = self.env.get_db_cnx()
168        cursor = db.cursor()
169        result = set()
170        users = set([u[0] for u in self.env.get_known_users()])
171        for user in users:
172            userperms = self.get_user_permissions(user)
173            for group in permissions:
174                if group in userperms:
175                    result.add(user)
176        return list(result)
177
178    def get_all_permissions(self):
179        """Return all permissions for all users.
180
181        The permissions are returned as a list of (subject, action)
182        formatted tuples."""
183        db = self.env.get_db_cnx()
184        cursor = db.cursor()
185        cursor.execute("SELECT username,action FROM permission")
186        return [(row[0], row[1]) for row in cursor]
187
188    def grant_permission(self, username, action):
189        """Grants a user the permission to perform the specified action."""
190        db = self.env.get_db_cnx()
191        cursor = db.cursor()
192        cursor.execute("INSERT INTO permission VALUES (%s, %s)",
193                       (username, action))
194        self.log.info('Granted permission for %s to %s' % (action, username))
195        db.commit()
196
197    def revoke_permission(self, username, action):
198        """Revokes a users' permission to perform the specified action."""
199        db = self.env.get_db_cnx()
200        cursor = db.cursor()
201        cursor.execute("DELETE FROM permission WHERE username=%s AND action=%s",
202                       (username, action))
203        self.log.info('Revoked permission for %s to %s' % (action, username))
204        db.commit()
205
206
207class DefaultPermissionGroupProvider(Component):
208    """Provides the basic builtin permission groups 'anonymous' and
209    'authenticated'."""
210
211    implements(IPermissionGroupProvider)
212
213    def get_permission_groups(self, username):
214        groups = ['anonymous']
215        if username and username != 'anonymous':
216            groups.append('authenticated')
217        return groups
218
219
220class DefaultPermissionPolicy(Component):
221    """Default permission policy using the IPermissionStore system."""
222
223    implements(IPermissionPolicy)
224
225    # IPermissionPolicy methods
226
227    def check_permission(self, username, action, resource):
228        return PermissionSystem(self.env). \
229               get_user_permissions(username).get(action, None)
230
231
232class PermissionSystem(Component):
233    """Sub-system that manages user permissions."""
234
235    implements(IPermissionRequestor)
236
237    requestors = ExtensionPoint(IPermissionRequestor)
238
239    store = ExtensionOption('trac', 'permission_store', IPermissionStore,
240                            'DefaultPermissionStore',
241        """Name of the component implementing `IPermissionStore`, which is used
242        for managing user and group permissions.""")
243
244    policies = OrderedExtensionsOption('trac', 'permission_policies',
245        IPermissionPolicy,
246        'DefaultPermissionPolicy, LegacyAttachmentPolicy',
247        False,
248        """List of components implementing `IPermissionPolicy`, in the order in
249        which they will be applied. These components manage fine-grained access
250        control to Trac resources.
251        Defaults to the DefaultPermissionPolicy (pre-0.11 behavior) and
252        LegacyAttachmentPolicy (map ATTACHMENT_* permissions to realm specific
253        ones)""")
254
255    # Public API
256
257    def grant_permission(self, username, action):
258        """Grant the user with the given name permission to perform to specified
259        action."""
260        if action.isupper() and action not in self.get_actions():
261            raise TracError(_('%(name)s is not a valid action.', name=action))
262
263        self.store.grant_permission(username, action)
264
265    def revoke_permission(self, username, action):
266        """Revokes the permission of the specified user to perform an action."""
267        self.store.revoke_permission(username, action)
268
269    def get_actions(self):
270        actions = []
271        for requestor in self.requestors:
272            for action in requestor.get_permission_actions():
273                if isinstance(action, tuple):
274                    actions.append(action[0])
275                else:
276                    actions.append(action)
277        return actions
278
279    def get_user_permissions(self, username=None):
280        """Return the permissions of the specified user.
281       
282        The return value is a dictionary containing all the actions as keys, and
283        a boolean value. `True` means that the permission is granted, `False`
284        means the permission is denied."""
285        actions = []
286        for requestor in self.requestors:
287            actions += list(requestor.get_permission_actions())
288        permissions = {}
289        if username:
290            # Return all permissions that the given user has
291            meta = {}
292            for action in actions:
293                if isinstance(action, tuple):
294                    name, value = action
295                    meta[name] = value
296            def _expand_meta(action):
297                permissions[action] = True
298                if meta.has_key(action):
299                    [_expand_meta(perm) for perm in meta[action]]
300            for perm in self.store.get_user_permissions(username):
301                _expand_meta(perm)
302        else:
303            # Return all permissions available in the system
304            for action in actions:
305                if isinstance(action, tuple):
306                    permissions[action[0]] = True
307                else:
308                    permissions[action] = True
309        return permissions
310
311    def get_all_permissions(self):
312        """Return all permissions for all users.
313
314        The permissions are returned as a list of (subject, action)
315        formatted tuples."""
316        return self.store.get_all_permissions()
317
318    def get_users_with_permission(self, permission):
319        """Return all users that have the specified permission.
320       
321        Users are returned as a list of user names.
322        """
323        # this should probably be cached
324        parent_map = {}
325        for requestor in self.requestors:
326            for action in requestor.get_permission_actions():
327                for child in action[1]:
328                    parent_map.setdefault(child, []).append(action[0])
329
330        satisfying_perms = {}
331        def _append_with_parents(action):
332            if action in satisfying_perms:
333                return # avoid unneccesary work and infinite loops
334            satisfying_perms[action] = True
335            if action in parent_map:
336                map(_append_with_parents, parent_map[action])
337        _append_with_parents(permission)
338
339        return self.store.get_users_with_permissions(satisfying_perms.keys())
340
341    def expand_actions(self, actions):
342        """Helper method for expanding all meta actions."""
343        meta = {}
344        for requestor in self.requestors:
345            for m in requestor.get_permission_actions():
346                if isinstance(m, tuple):
347                    meta[m[0]] = m[1]
348        expanded_actions = set(actions)
349
350        def expand_action(action):
351            actions = meta.get(action, [])
352            expanded_actions.update(actions)
353            [expand_action(a) for a in actions]
354
355        [expand_action(a) for a in actions]
356        return expanded_actions
357
358    def check_permission(self, action, username=None, resource=None):
359        """Return True if permission to perform action for the given resource
360        is allowed."""
361        if username is None:
362            username = 'anonymous'
363
364        if resource is None:
365            resource = PermissionCache(self.env, username).toplevel()
366
367        for policy in self.policies:
368            decision = policy.check_permission(username, action, resource)
369            if decision is not None:
370                if not decision:
371                    self.log.debug("%s denies %s performing %s on %r" %
372                                   (policy.__class__.__name__, username,
373                                    action, resource))
374                return decision
375        self.log.debug("No policy allowed %s performing %s on %r" %
376                       (username, action, resource))
377        return False
378
379    # IPermissionRequestor methods
380
381    def get_permission_actions(self):
382        """Implement the global `TRAC_ADMIN` meta permission.
383       
384        Implements also the `EMAIL_VIEW` permission which allows for
385        showing email addresses even if `[trac] show_email_addresses`
386        is `false`.
387        """
388        actions = ['EMAIL_VIEW']
389        for requestor in [r for r in self.requestors if r is not self]:
390            for action in requestor.get_permission_actions():
391                if isinstance(action, tuple):
392                    actions.append(action[0])
393                else:
394                    actions.append(action)
395        return [('TRAC_ADMIN', actions), 'EMAIL_VIEW']
396
397
398class PermissionCache(object):
399    """Cache the permissions on Trac resources for a single user.
400
401    Conversely, the `PermissionCache` also offers convenient ways to access
402    authorized Resource objects (''since 0.11'')
403
404    More specifically, when requesting a PermissionCache to create a Resource
405    object in some `realm`, the corresponding `<realm>_VIEW` permission is
406    automatically checked and the `Resource` object will be returned *only*
407    if this permission is granted::
408
409        page = perm('wiki', 'WikiStart')
410        page_version = perm('wiki', 'WikiStart', 31)
411
412    If the 'WIKI_VIEW' permission is missing for the 'WikiStart' page, then
413    `page` will be `None`. If it is prefered to have a `PermissionError`
414    triggered in that case, one could use::
415
416        page = perm.assert_resource('wiki', 'WikiStart')
417        page_version = perm.assert_resource('wiki', 'WikiStart', 31)
418
419    Once a resource is available, additional permissions checks can be
420    performed::
421   
422        'WIKI_MODIFY' in page.perm
423
424    Note that if it is not useful to store the Resource in a variable,
425    the following kind of shortcut can be used::
426   
427        'WIKI_MODIFY' in perm('wiki', 'WikiStart')
428
429    Permissions can nevertheless still be checked without having to specify an
430    explicit resource, in a style similar to the previous versions of Trac::
431
432        'WIKI_MODIFY' in perm
433        perm.require('WIKI_MODIFY')
434
435    Note that the above notations are equivalent to checking the permission
436    on a "toplevel" resource, i.e. a resource corresponding to the whole
437    environment::
438   
439        'WIKI_MODIFY' in perm.toplevel()
440
441    The above should generally be equivalent to checking the permission on the
442    corresponding "realm" resource, i.e. a resource for which only the realm
443    information was specified::
444
445        'WIKI_MODIFY' in perm('wiki')
446
447    """
448
449    __slots__ = ('env', 'username', '_resource', '_cache')
450
451    def __init__(self, env, username=None, resource=None, cache=None):
452        self.env = env
453        self.username = username or 'anonymous'
454        self._resource = resource
455        if cache is None:
456            self._cache = {}
457        else:
458            self._cache = cache
459
460    def copy(self, resource):
461        return PermissionCache(self.env, self.username, resource, self._cache)
462   
463    def _normalize_resource(self, realm_or_resource, id, version):
464        """Helper method used for enforcing the use of Resource instances"""
465        if realm_or_resource is None:
466            return self._resource
467        elif isinstance(realm_or_resource, Resource):
468            return realm_or_resource
469        else:
470            return Resource(self, realm_or_resource, id, version, None, None)
471
472    def has_permission(self, action, realm_or_resource=None, id=None,
473                       version=None):
474        resource = self._normalize_resource(realm_or_resource, id, version)
475        return self._has_permission(action, resource)
476
477    def _has_permission(self, action, resource):
478        key = (self.username, hash(resource), action)
479        try:
480            return self._cache[key]
481        except KeyError:
482            decision = PermissionSystem(self.env) \
483                       .check_permission(action, self.username, resource)
484            self._cache[key] = decision
485            return decision
486
487    __contains__ = has_permission
488       
489    def require(self, action, realm_or_resource=None, id=None, version=None):
490        resource = self._normalize_resource(realm_or_resource, id, version)
491        return self._require(action, resource)
492
493    def _require(self, action, resource):
494        if not self._has_permission(action, resource):
495            raise PermissionError(action, resource)
496    assert_permission = require
497
498    def permissions(self):
499        """Deprecated (but still used by the HDF compatibility layer)"""
500        self.env.log.warning('perm.permissions() is deprecated and '
501                             'is only present for HDF compatibility')
502        perm = PermissionSystem(self.env)
503        actions = perm.get_user_permissions(self.username)
504        return [action for action in actions if action in self]
505
506    # methods for accessing Resource objects
507   
508    def assert_resource(self, realm, id=None, version=None, model=None,
509                        parent=None):
510        """Get a Resource object for the `(realm, id, version)` resource.
511
512        If `version` is `None`, this will designate the latest version of the
513        resource.
514        If `id` is `None`, this resource will stand for the realm as a whole.
515
516        This will raise a `PermissionError` exception when there's no read
517        permission for the resource.
518        """
519        resource = Resource(self, realm, id, version, model, parent)
520        self._require(realm.upper() + '_VIEW', resource)
521        return resource
522
523    def __call__(self, realm, id=None, version=None, model=None, parent=None):
524        """Get a Resource object for the `(realm, id, version)` resource.
525
526        Same as `assert_resource`, except it will return `None` when there's
527        no read permission for the resource.
528        """
529        resource = Resource(self, realm, id, version, model, parent)
530        if self._has_permission(realm.upper() + '_VIEW', resource):
531            return resource
532        else:
533            # NOTE: we can't return `None` here as we must support calls like:
534            #  'WIKI_MODIFY' in req.perm(realm, id)
535            # when the (realm,id) resource is not viewable
536            return ()
537
538    def toplevel(self):
539        """Return a special resource corresponding to the whole environment.
540
541        This call will always succeed.
542        """
543        return Resource(self, '', None, None, None, None)