Edgewall Software

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

Last change on this file was 14084, checked in by Ryan J Ollos, 9 years 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.