Edgewall Software

source: branches/0.12-stable/tracopt/perm/authz_policy.py

Last change on this file was 14084, checked in by rjollos, 8 weeks ago

0.12.7: Revert [14079].

Whitespace cleanup on 0.12-stable would require modifications to unit and functional tests. The cleanup has already been done on 1.0-stable.

  • Property svn:eol-style set to native
File size: 9.2 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2007-2009 Edgewall Software
4# Copyright (C) 2007 Alec Thomas <alec@swapoff.org>
5# All rights reserved.
6#
7# This software is licensed as described in the file COPYING, which
8# you should have received as part of this distribution. The terms
9# are also available at http://trac.edgewall.org/wiki/TracLicense.
10#
11# This software consists of voluntary contributions made by many
12# individuals. For the exact contribution history, see the revision
13# history and logs, available at http://trac.edgewall.org/log/.
14#
15# Author: Alec Thomas <alec@swapoff.org>
16
17from fnmatch import fnmatch
18from itertools import groupby
19import os
20
21from trac.core import *
22from trac.config import Option
23from trac.perm import PermissionSystem, IPermissionPolicy
24from trac.util.text import to_unicode
25
26ConfigObj = None
27try:
28    from configobj import ConfigObj
29except ImportError:
30    pass
31
32
33class AuthzPolicy(Component):
34    """Permission policy using an authz-like configuration file.
35   
36    Refer to SVN documentation for syntax of the authz file. Groups are
37    supported.
38   
39    As the fine-grained permissions brought by this permission policy are
40    often used in complement of the other pemission policies (like the
41    `DefaultPermissionPolicy`), there's no need to redefine all the
42    permissions here. Only additional rights or restrictions should be added.
43   
44    === Installation ===
45    Note that this plugin requires the `configobj` package:
46   
47        http://www.voidspace.org.uk/python/configobj.html
48   
49    You should be able to install it by doing a simple `easy_install configobj`
50   
51    Enabling this policy requires listing it in `trac.ini:
52    {{{
53    [trac]
54    permission_policies = AuthzPolicy, DefaultPermissionPolicy
55   
56    [authz_policy]
57    authz_file = conf/authzpolicy.conf
58    }}}
59   
60    This means that the `AuthzPolicy` permissions will be checked first, and
61    only if no rule is found will the `DefaultPermissionPolicy` be used.
62   
63   
64    === Configuration ===
65    The `authzpolicy.conf` file is a `.ini` style configuration file.
66   
67     - Each section of the config is a glob pattern used to match against a
68       Trac resource descriptor. These descriptors are in the form:
69       {{{
70       <realm>:<id>@<version>[/<realm>:<id>@<version> ...]
71       }}}
72       Resources are ordered left to right, from parent to child. If any
73       component is inapplicable, `*` is substituted. If the version pattern is
74       not specified explicitely, all versions (`@*`) is added implicitly
75       
76       Example: Match the WikiStart page
77       {{{
78       [wiki:*]
79       [wiki:WikiStart*]
80       [wiki:WikiStart@*]
81       [wiki:WikiStart]
82       }}}
83       
84       Example: Match the attachment `wiki:WikiStart@117/attachment/FOO.JPG@*`
85       on WikiStart
86       {{{
87       [wiki:*]
88       [wiki:WikiStart*]
89       [wiki:WikiStart@*]
90       [wiki:WikiStart@*/attachment/*]
91       [wiki:WikiStart@117/attachment/FOO.JPG]
92       }}}
93   
94     - Sections are checked against the current Trac resource '''IN ORDER''' of
95       appearance in the configuration file. '''ORDER IS CRITICAL'''.
96   
97     - Once a section matches, the current username is matched, '''IN ORDER''',
98       against the keys of the section. If a key is prefixed with a `@`, it is
99       treated as a group. If a key is prefixed with a `!`, the permission is
100       denied rather than granted. The username will match any of 'anonymous',
101       'authenticated', <username> or '*', using normal Trac permission rules.
102   
103    Example configuration:
104    {{{
105    [groups]
106    administrators = athomas
107   
108    [*/attachment:*]
109    * = WIKI_VIEW, TICKET_VIEW
110   
111    [wiki:WikiStart@*]
112    @administrators = WIKI_ADMIN
113    anonymous = WIKI_VIEW
114    * = WIKI_VIEW
115   
116    # Deny access to page templates
117    [wiki:PageTemplates/*]
118    * =
119   
120    # Match everything else
121    [*]
122    @administrators = TRAC_ADMIN
123    anonymous = BROWSER_VIEW, CHANGESET_VIEW, FILE_VIEW, LOG_VIEW,
124        MILESTONE_VIEW, POLL_VIEW, REPORT_SQL_VIEW, REPORT_VIEW, ROADMAP_VIEW,
125        SEARCH_VIEW, TICKET_CREATE, TICKET_MODIFY, TICKET_VIEW, TIMELINE_VIEW,
126        WIKI_CREATE, WIKI_MODIFY, WIKI_VIEW
127    # Give authenticated users some extra permissions
128    authenticated = REPO_SEARCH, XML_RPC
129    }}}
130    """
131    implements(IPermissionPolicy)
132
133    authz_file = Option('authz_policy', 'authz_file', '',
134                        'Location of authz policy configuration file.')
135
136    authz = None
137    authz_mtime = None
138
139    # IPermissionPolicy methods
140   
141    def check_permission(self, action, username, resource, perm):
142        if ConfigObj is None:
143            self.log.error('configobj package not found')
144            return None
145       
146        if self.authz_file and not self.authz_mtime or \
147                os.path.getmtime(self.get_authz_file()) > self.authz_mtime:
148            self.parse_authz()
149        resource_key = self.normalise_resource(resource)
150        self.log.debug('Checking %s on %s', action, resource_key)
151        permissions = self.authz_permissions(resource_key, username)
152        if permissions is None:
153            return None                 # no match, can't decide
154        elif permissions == ['']:
155            return False                # all actions are denied
156
157        # FIXME: expand all permissions once for all
158        ps = PermissionSystem(self.env)
159        for deny, perms in groupby(permissions,
160                                   key=lambda p: p.startswith('!')):
161            if deny and action in ps.expand_actions([p[1:] for p in perms]):
162                return False            # action is explicitly denied
163            elif action in ps.expand_actions(perms):
164                return True            # action is explicitly granted
165
166        return None                    # no match for action, can't decide
167
168    # Internal methods
169
170    def get_authz_file(self):
171        f = self.authz_file
172        return os.path.isabs(f) and f or os.path.join(self.env.path, f)
173
174    def parse_authz(self):
175        self.log.debug('Parsing authz security policy %s',
176                       self.get_authz_file())
177        self.authz = ConfigObj(self.get_authz_file(), encoding='utf8')
178        groups = {}
179        for group, users in self.authz.get('groups', {}).iteritems():
180            if isinstance(users, basestring):
181                users = [users]
182            groups[group] = map(to_unicode, users)
183       
184        self.groups_by_user = {}
185       
186        def add_items(group, items):
187            for item in items:
188                if item.startswith('@'):
189                    add_items(group, groups[item[1:]])
190                else:
191                    self.groups_by_user.setdefault(item, set()).add(group)
192                   
193        for group, users in groups.iteritems():
194            add_items('@' + group, users)
195
196        self.authz_mtime = os.path.getmtime(self.get_authz_file())
197
198    def normalise_resource(self, resource):
199        def flatten(resource):
200            if not resource:
201                return ['*:*@*']
202            if not (resource.realm or resource.id):
203                return ['%s:%s@%s' % (resource.realm or '*',
204                                      resource.id or '*',
205                                      resource.version or '*')]
206            # XXX Due to the mixed functionality in resource we can end up with
207            # ticket, ticket:1, ticket:1@10. This code naively collapses all
208            # subsets of the parent resource into one. eg. ticket:1@10
209            parent = resource.parent
210            while parent and (resource.realm == parent.realm or
211                              (resource.realm == parent.realm and
212                               resource.id == parent.id)):
213                parent = parent.parent
214            if parent:
215                parent = flatten(parent)
216            else:
217                parent = []
218            return parent + ['%s:%s@%s' % (resource.realm or '*',
219                                           resource.id or '*',
220                                           resource.version or '*')]
221        return '/'.join(flatten(resource))
222
223    def authz_permissions(self, resource_key, username):
224        # TODO: Handle permission negation in sections. eg. "if in this
225        # ticket, remove TICKET_MODIFY"
226        if username and username != 'anonymous':
227            valid_users = ['*', 'authenticated', username]
228        else:
229            valid_users = ['*', 'anonymous']
230        for resource_section in [a for a in self.authz.sections
231                                   if a != 'groups']:
232            resource_glob = to_unicode(resource_section)
233            if '@' not in resource_glob:
234                resource_glob += '@*'
235
236            if fnmatch(resource_key, resource_glob):
237                section = self.authz[resource_section]
238                for who, permissions in section.iteritems():
239                    who = to_unicode(who)
240                    if who in valid_users or \
241                            who in self.groups_by_user.get(username, []):
242                        self.log.debug('%s matched section %s for user %s',
243                                       resource_key, resource_glob, username)
244                        if isinstance(permissions, basestring):
245                            return [permissions]
246                        else:
247                            return permissions
248        return None
Note: See TracBrowser for help on using the repository browser.