Package tracopt :: Package perm :: Module authz_policy

Source Code for Module tracopt.perm.authz_policy

  1  # -*- coding: utf-8 -*- 
  2  # 
  3  # Copyright (C) 2007-2020 Edgewall Software 
  4  # Copyright (C) 2007 Alec Thomas <[email protected]> 
  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 <[email protected]> 
 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   
29 -class AuthzPolicy(Component):
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
136 - def __init__(self):
137 self.authz = None 138 self.authz_mtime = None 139 self.groups_by_user = {}
140 141 # IPermissionPolicy methods 142
143 - def check_permission(self, action, username, resource, perm):
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 # 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 parse_authz(self):
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
221 - def normalise_resource(self, resource):
222 def to_descriptor(resource): 223 id = resource.id 224 return '%s:%s@%s' % (resource.realm or '*', 225 id if id is not None else '*', 226 resource.version or '*')
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 # XXX Due to the mixed functionality in resource we can end up with 235 # ticket, ticket:1, ticket:1@10. This code naively collapses all 236 # subsets of the parent resource into one. eg. ticket:1@10 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
247 - def authz_permissions(self, resource_key, username):
248 # TODO: Handle permission negation in sections. eg. "if in this 249 # ticket, remove TICKET_MODIFY" 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