Source

trac-ticketlinks / tracopt / perm / authz_policy.py

Full commit
rblank 8461b0d 






















cboos 5252307 
rblank 8461b0d 








rblank b8fa8a9 
































































































rblank 8461b0d 

rblank bf88896 
rblank 8461b0d 








































cboos c940f46 

rblank 8461b0d 
rblank ffe6f85 





rblank 8461b0d 
rblank 3047377 



rblank ffe6f85 
rblank 3047377 


rblank ffe6f85 
rblank 3047377 

rblank 8461b0d 



rblank 85931df 





rblank 8461b0d 



cboos c940f46 


rblank 8461b0d 













cboos c169a82 
rblank 85931df 

rblank 8461b0d 

cboos 5252307 
rblank 8461b0d 







cboos c940f46 

rblank 8461b0d 



# -*- coding: utf-8 -*-
#
# Copyright (C) 2007-2009 Edgewall Software
# Copyright (C) 2007 Alec Thomas <alec@swapoff.org>
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at http://trac.edgewall.org/wiki/TracLicense.
#
# This software consists of voluntary contributions made by many
# individuals. For the exact contribution history, see the revision
# history and logs, available at http://trac.edgewall.org/log/.
#
# Author: Alec Thomas <alec@swapoff.org>

from fnmatch import fnmatch
from itertools import groupby
import os

from trac.core import *
from trac.config import Option
from trac.perm import PermissionSystem, IPermissionPolicy
from trac.util.text import to_unicode

ConfigObj = None
try:
    from configobj import ConfigObj
except ImportError:
    pass


class AuthzPolicy(Component):
    """Permission policy using an authz-like configuration file.
    
    Refer to SVN documentation for syntax of the authz file. Groups are
    supported.
    
    As the fine-grained permissions brought by this permission policy are
    often used in complement of the other pemission policies (like the
    `DefaultPermissionPolicy`), there's no need to redefine all the
    permissions here. Only additional rights or restrictions should be added.
    
    === Installation ===
    Note that this plugin requires the `configobj` package:
    
        http://www.voidspace.org.uk/python/configobj.html
    
    You should be able to install it by doing a simple `easy_install configobj`
    
    Enabling this policy requires listing it in `trac.ini:
    {{{
    [trac]
    permission_policies = AuthzPolicy, DefaultPermissionPolicy
    
    [authz_policy]
    authz_file = conf/authzpolicy.conf
    }}}
    
    This means that the `AuthzPolicy` permissions will be checked first, and
    only if no rule is found will the `DefaultPermissionPolicy` be used.
    
    
    === Configuration ===
    The `authzpolicy.conf` file is a `.ini` style configuration file.
    
     - Each section of the config is a glob pattern used to match against a
       Trac resource descriptor. These descriptors are in the form:
       {{{
       <realm>:<id>@<version>[/<realm>:<id>@<version> ...]
       }}}
       Resources are ordered left to right, from parent to child. If any
       component is inapplicable, `*` is substituted. If the version pattern is
       not specified explicitely, all versions (`@*`) is added implicitly
       
       Example: Match the WikiStart page
       {{{
       [wiki:*]
       [wiki:WikiStart*]
       [wiki:WikiStart@*]
       [wiki:WikiStart]
       }}}
       
       Example: Match the attachment `wiki:WikiStart@117/attachment/FOO.JPG@*`
       on WikiStart
       {{{
       [wiki:*]
       [wiki:WikiStart*]
       [wiki:WikiStart@*]
       [wiki:WikiStart@*/attachment/*]
       [wiki:WikiStart@117/attachment/FOO.JPG]
       }}}
    
     - Sections are checked against the current Trac resource '''IN ORDER''' of
       appearance in the configuration file. '''ORDER IS CRITICAL'''.
    
     - Once a section matches, the current username is matched, '''IN ORDER''',
       against the keys of the section. If a key is prefixed with a `@`, it is
       treated as a group. If a key is prefixed with a `!`, the permission is
       denied rather than granted. The username will match any of 'anonymous',
       'authenticated', <username> or '*', using normal Trac permission rules.
    
    Example configuration:
    {{{
    [groups]
    administrators = athomas
    
    [*/attachment:*]
    * = WIKI_VIEW, TICKET_VIEW
    
    [wiki:WikiStart@*]
    @administrators = WIKI_ADMIN
    anonymous = WIKI_VIEW
    * = WIKI_VIEW
    
    # Deny access to page templates
    [wiki:PageTemplates/*]
    * =
    
    # Match everything else
    [*]
    @administrators = TRAC_ADMIN
    anonymous = BROWSER_VIEW, CHANGESET_VIEW, FILE_VIEW, LOG_VIEW,
        MILESTONE_VIEW, POLL_VIEW, REPORT_SQL_VIEW, REPORT_VIEW, ROADMAP_VIEW,
        SEARCH_VIEW, TICKET_CREATE, TICKET_MODIFY, TICKET_VIEW, TIMELINE_VIEW,
        WIKI_CREATE, WIKI_MODIFY, WIKI_VIEW
    # Give authenticated users some extra permissions
    authenticated = REPO_SEARCH, XML_RPC
    }}}
    """
    implements(IPermissionPolicy)

    authz_file = Option('authz_policy', 'authz_file', '',
                        'Location of authz policy configuration file.')

    authz = None
    authz_mtime = None

    # IPermissionPolicy methods
    
    def check_permission(self, action, username, resource, perm):
        if ConfigObj is None:
            self.log.error('configobj package not found')
            return None
        
        if self.authz_file and not self.authz_mtime or \
                os.path.getmtime(self.get_authz_file()) > self.authz_mtime:
            self.parse_authz()
        resource_key = self.normalise_resource(resource)
        self.log.debug('Checking %s on %s', action, resource_key)
        permissions = self.authz_permissions(resource_key, username)
        if permissions is None:
            return None                 # no match, can't decide
        elif permissions == ['']:
            return False                # all actions are denied

        # FIXME: expand all permissions once for all
        ps = PermissionSystem(self.env)
        for deny, perms in groupby(permissions,
                                   key=lambda p: p.startswith('!')):
            if deny and action in ps.expand_actions([p[1:] for p in perms]):
                return False            # action is explicitly denied
            elif action in ps.expand_actions(perms):
                return True            # action is explicitly granted

        return None                    # no match for action, can't decide

    # Internal methods

    def get_authz_file(self):
        f = self.authz_file
        return os.path.isabs(f) and f or os.path.join(self.env.path, f)

    def parse_authz(self):
        self.log.debug('Parsing authz security policy %s',
                       self.get_authz_file())
        self.authz = ConfigObj(self.get_authz_file())
        groups = {}
        for group, users in self.authz.get('groups', {}).iteritems():
            if isinstance(users, basestring):
                users = [users]
            groups[group] = users
        
        self.groups_by_user = {}
        
        def add_items(group, items):
            for item in items:
                if item.startswith('@'):
                    add_items(group, groups[item[1:]])
                else:
                    self.groups_by_user.setdefault(item, set()).add(group)
                    
        for group, users in groups.iteritems():
            add_items('@' + group, users)

        self.authz_mtime = os.path.getmtime(self.get_authz_file())

    def normalise_resource(self, resource):
        def flatten(resource):
            if not resource:
                return ['*:*@*']
            if not (resource.realm or resource.id):
                return ['%s:%s@%s' % (resource.realm or '*',
                                      resource.id or '*',
                                      resource.version or '*')]
            # XXX Due to the mixed functionality in resource we can end up with
            # ticket, ticket:1, ticket:1@10. This code naively collapses all
            # subsets of the parent resource into one. eg. ticket:1@10
            parent = resource.parent
            while parent and (resource.realm == parent.realm or
                              (resource.realm == parent.realm and
                               resource.id == parent.id)):
                parent = parent.parent
            if parent:
                parent = flatten(parent)
            else:
                parent = []
            return parent + ['%s:%s@%s' % (resource.realm or '*',
                                           resource.id or '*',
                                           resource.version or '*')]
        return '/'.join(flatten(resource))

    def authz_permissions(self, resource_key, username):
        # TODO: Handle permission negation in sections. eg. "if in this
        # ticket, remove TICKET_MODIFY"
        if username and username != 'anonymous':
            valid_users = ['*', 'authenticated', username]
        else:
            valid_users = ['*', 'anonymous']
        for resource_section in [a for a in self.authz.sections
                                 if a != 'groups']:
            resource_glob = to_unicode(resource_section)
            if '@' not in resource_glob:
                resource_glob += '@*'

            if fnmatch(resource_key, resource_glob):
                section = self.authz[resource_section]
                for who, permissions in section.iteritems():
                    if who in valid_users or \
                            who in self.groups_by_user.get(username, []):
                        self.log.debug('%s matched section %s for user %s',
                                       resource_key, resource_glob, username)
                        if isinstance(permissions, basestring):
                            return [permissions]
                        else:
                            return permissions
        return None