Edgewall Software

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

Last change on this file was 17305, checked in by Ryan J Ollos, 3 years ago

1.0.20dev: Update copyright

[skip ci]

Refs #9567.

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