Edgewall Software

source: branches/0.11-stable/sample-plugins/permissions/authz_policy.py

Last change on this file was 7889, checked in by jonas, 3 years ago

0.11-stable: Updated copyright year.

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