1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 import os
18 from fnmatch import fnmatchcase
19 from itertools import groupby
20
21 from trac.config import ConfigurationError, ParsingError, PathOption, \
22 UnicodeConfigParser
23 from trac.core import Component, implements
24 from trac.perm import IPermissionPolicy, PermissionSystem
25 from trac.util import to_list
26 from trac.util.text import exception_to_unicode
27
28
30 """Permission policy using an authz-like configuration file.
31
32 Refer to SVN documentation for syntax of the authz file. Groups are
33 supported.
34
35 As the fine-grained permissions brought by this permission policy are
36 often used in complement of the other permission policies (like the
37 `DefaultPermissionPolicy`), there's no need to redefine all the
38 permissions here. Only additional rights or restrictions should be added.
39
40 === Installation ===
41 Enabling this policy requires listing it in `trac.ini`::
42
43 {{{
44 [trac]
45 permission_policies = AuthzPolicy, DefaultPermissionPolicy
46
47 [authz_policy]
48 authz_file = conf/authzpolicy.conf
49 }}}
50
51 This means that the `AuthzPolicy` permissions will be checked first, and
52 only if no rule is found will the `DefaultPermissionPolicy` be used.
53
54
55 === Configuration ===
56 The `authzpolicy.conf` file is a `.ini` style configuration file.
57
58 - Each section of the config is a glob pattern used to match against a
59 Trac resource descriptor. These descriptors are in the form::
60
61 {{{
62 <realm>:<id>@<version>[/<realm>:<id>@<version> ...]
63 }}}
64
65 Resources are ordered left to right, from parent to child. If any
66 component is inapplicable, `*` is substituted. If the version pattern is
67 not specified explicitely, all versions (`@*`) is added implicitly
68
69 Example: Match the WikiStart page::
70
71 {{{
72 [wiki:*]
73 [wiki:WikiStart*]
74 [wiki:WikiStart@*]
75 [wiki:WikiStart]
76 }}}
77
78 Example: Match the attachment
79 ``wiki:WikiStart@117/attachment/FOO.JPG@*`` on WikiStart::
80
81 {{{
82 [wiki:*]
83 [wiki:WikiStart*]
84 [wiki:WikiStart@*]
85 [wiki:WikiStart@*/attachment/*]
86 [wiki:WikiStart@117/attachment/FOO.JPG]
87 }}}
88
89 - Sections are checked against the current Trac resource '''IN ORDER''' of
90 appearance in the configuration file. '''ORDER IS CRITICAL'''.
91
92 - Once a section matches, the current username is matched, '''IN ORDER''',
93 against the keys of the section. If a key is prefixed with a `@`, it is
94 treated as a group. If a key is prefixed with a `!`, the permission is
95 denied rather than granted. The username will match any of 'anonymous',
96 'authenticated', <username> or '*', using normal Trac permission rules.
97
98 Example configuration::
99
100 {{{
101 [groups]
102 administrators = athomas
103
104 [*/attachment:*]
105 * = WIKI_VIEW, TICKET_VIEW
106
107 [wiki:WikiStart@*]
108 @administrators = WIKI_ADMIN
109 anonymous = WIKI_VIEW
110 * = WIKI_VIEW
111
112 # Deny access to page templates
113 [wiki:PageTemplates/*]
114 * =
115
116 # Match everything else
117 [*]
118 @administrators = TRAC_ADMIN
119 anonymous = BROWSER_VIEW, CHANGESET_VIEW, FILE_VIEW, LOG_VIEW,
120 MILESTONE_VIEW, POLL_VIEW, REPORT_SQL_VIEW, REPORT_VIEW,
121 ROADMAP_VIEW, SEARCH_VIEW, TICKET_CREATE, TICKET_MODIFY,
122 TICKET_VIEW, TIMELINE_VIEW,
123 WIKI_CREATE, WIKI_MODIFY, WIKI_VIEW
124 # Give authenticated users some extra permissions
125 authenticated = REPO_SEARCH, XML_RPC
126 }}}
127
128 """
129 implements(IPermissionPolicy)
130
131 authz_file = PathOption('authz_policy', 'authz_file', '',
132 "Location of authz policy configuration file. "
133 "Non-absolute paths are relative to the "
134 "Environment `conf` directory.")
135
137 self.authz = None
138 self.authz_mtime = None
139 self.groups_by_user = {}
140
141
142
144 if not self.authz_mtime or \
145 os.path.getmtime(self.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
152 elif permissions == []:
153 return False
154
155
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
161 elif action in ps.expand_actions(perms):
162 return True
163
164 return None
165
166
167
169 self.log.debug("Parsing authz security policy %s",
170 self.authz_file)
171
172 if not self.authz_file:
173 self.log.error("The `[authz_policy] authz_file` configuration "
174 "option in trac.ini is empty or not defined.")
175 raise ConfigurationError()
176 try:
177 self.authz_mtime = os.path.getmtime(self.authz_file)
178 except OSError as e:
179 self.log.error("Error parsing authz permission policy file: %s",
180 exception_to_unicode(e))
181 raise ConfigurationError()
182
183 self.authz = UnicodeConfigParser(ignorecase_option=False)
184 try:
185 self.authz.read(self.authz_file)
186 except ParsingError as e:
187 self.log.error("Error parsing authz permission policy file: %s",
188 exception_to_unicode(e))
189 raise ConfigurationError()
190 groups = {}
191 if self.authz.has_section('groups'):
192 for group, users in self.authz.items('groups'):
193 groups[group] = to_list(users)
194
195 self.groups_by_user = {}
196
197 def add_items(group, items):
198 for item in items:
199 if item.startswith('@'):
200 add_items(group, groups[item[1:]])
201 else:
202 self.groups_by_user.setdefault(item, set()).add(group)
203
204 for group, users in groups.iteritems():
205 add_items('@' + group, users)
206
207 all_actions = set(PermissionSystem(self.env).get_actions())
208 authz_basename = os.path.basename(self.authz_file)
209 for section in self.authz.sections():
210 if section == 'groups':
211 continue
212 for _, actions in self.authz.items(section):
213 for action in to_list(actions):
214 if action.startswith('!'):
215 action = action[1:]
216 if action not in all_actions:
217 self.log.warning("The action %s in the [%s] section "
218 "of %s is not a valid action.",
219 action, section, authz_basename)
220
227
228 def flatten(resource):
229 if not resource:
230 return ['*:*@*']
231 descriptor = to_descriptor(resource)
232 if not resource.realm and resource.id is None:
233 return [descriptor]
234
235
236
237 parent = resource.parent
238 while parent and resource.realm == parent.realm:
239 parent = parent.parent
240 if parent:
241 return flatten(parent) + [descriptor]
242 else:
243 return [descriptor]
244
245 return '/'.join(flatten(resource))
246
248
249
250 if username and username != 'anonymous':
251 valid_users = ['*', 'authenticated', 'anonymous', username]
252 else:
253 valid_users = ['*', 'anonymous']
254 for resource_section in [a for a in self.authz.sections()
255 if a != 'groups']:
256 resource_glob = resource_section
257 if '@' not in resource_glob:
258 resource_glob += '@*'
259
260 if fnmatchcase(resource_key, resource_glob):
261 for who, permissions in self.authz.items(resource_section):
262 permissions = to_list(permissions)
263 if who in valid_users or \
264 who in self.groups_by_user.get(username, []):
265 self.log.debug("%s matched section %s for user %s",
266 resource_key, resource_glob, username)
267 if isinstance(permissions, basestring):
268 return [permissions]
269 else:
270 return permissions
271 return None
272