Ticket #9566: 9566-scoped-repos-2-r10006.patch
| File 9566-scoped-repos-2-r10006.patch, 15.5 KB (added by rblank, 21 months ago) |
|---|
-
trac/versioncontrol/svn_authz.py
diff --git a/trac/versioncontrol/svn_authz.py b/trac/versioncontrol/svn_authz.py
a b 40 40 path = path[:idx + 1] 41 41 42 42 43 def join(*args): 44 args = (arg.strip('/') for arg in args) 45 return '/'.join(arg for arg in args if arg) 46 47 43 48 class ParseError(Exception): 44 49 """Exception thrown for parse errors in authz files""" 45 50 … … 125 130 126 131 authz_module_name = Option('trac', 'authz_module_name', '', 127 132 """The module prefix used in the `authz_file` for the default 128 repository. If left empty, the global section s will beused.133 repository. If left empty, the global section is used. 129 134 """) 130 135 131 136 _mtime = 0 132 137 _authz = {} 133 138 _users = set() 134 139 140 _handled_perms = frozenset([(None, 'BROWSER_VIEW'), 141 (None, 'CHANGESET_VIEW'), 142 (None, 'FILE_VIEW'), 143 (None, 'LOG_VIEW'), 144 ('source', 'BROWSER_VIEW'), 145 ('source', 'FILE_VIEW'), 146 ('source', 'LOG_VIEW'), 147 ('changeset', 'CHANGESET_VIEW')]) 148 135 149 # IPermissionPolicy methods 136 150 137 151 def check_permission(self, action, username, resource, perm): 138 if username == 'anonymous': 139 usernames = ('$anonymous', '*') 140 else: 141 usernames = (username, '$authenticated', '*') 142 if action in ('BROWSER_VIEW', 'FILE_VIEW', 'LOG_VIEW'): 152 realm = resource and resource.realm or None 153 if (realm, action) in self._handled_perms: 143 154 authz, users = self._get_authz_info() 144 155 if authz is None: 145 156 return False 157 158 if username == 'anonymous': 159 usernames = ('$anonymous', '*') 160 else: 161 usernames = (username, '$authenticated', '*') 146 162 if resource is None: 147 return bool(users & set(usernames)) 148 if resource.realm == 'source': 149 modules = [resource.parent.id or self.authz_module_name] 150 if modules[0]: 151 modules.append('') 152 path = '/' + resource.id.strip('/') 163 return users & set(usernames) and True or None 164 165 rm = RepositoryManager(self.env) 166 repos = rm.get_repository(resource.parent.id) 167 scope = getattr(repos, 'scope', '') 168 modules = [resource.parent.id or self.authz_module_name] 169 if modules[0]: 170 modules.append('') 171 172 def check_path(path): 173 path = '/' + join(scope, path) 153 174 if path != '/': 154 175 path += '/' 155 176 … … 170 191 if spath.startswith(path) 171 192 for user in usernames): 172 193 return True 194 195 if realm == 'source': 196 return check_path(resource.id) 173 197 174 elif action == 'CHANGESET_VIEW': 175 authz, users = self._get_authz_info() 176 if authz is None: 177 return False 178 if resource is None: 179 return bool(users & set(usernames)) 180 if resource.realm == 'changeset': 181 rm = RepositoryManager(self.env) 182 repos = rm.get_repository(resource.parent.id) 198 elif realm == 'changeset': 183 199 changes = list(repos.get_changeset(resource.id).get_changes()) 184 if not changes: 200 if not changes or any(check_path(change[0]) 201 for change in changes): 185 202 return True 186 source = Resource('source', version=resource.id,187 parent=resource.parent)188 return any('FILE_VIEW' in perm(source(id=change[0]))189 for change in changes)190 203 191 204 def _get_authz_info(self): 192 205 try: -
trac/versioncontrol/tests/svn_authz.py
diff --git a/trac/versioncontrol/tests/svn_authz.py b/trac/versioncontrol/tests/svn_authz.py
a b 16 16 import unittest 17 17 18 18 from trac.resource import Resource 19 from trac.test import EnvironmentStub 19 from trac.test import EnvironmentStub, Mock 20 20 from trac.util import create_file 21 from trac.versioncontrol.api import RepositoryManager 21 22 from trac.versioncontrol.svn_authz import AuthzSourcePolicy, ParseError, \ 22 23 parse 23 24 … … 185 186 &jekyll = r 186 187 [/aliases_b] 187 188 @alias2 = r 189 190 # Scoped repository 191 [scoped:/scope/dir1] 192 joe = r 193 [scoped:/scope/dir2] 194 jane = r 188 195 """) 189 196 self.env = EnvironmentStub(enable=[AuthzSourcePolicy]) 190 197 self.env.config.set('trac', 'authz_file', self.authz) 191 198 self.policy = AuthzSourcePolicy(self.env) 199 200 # Monkey-subclass RepositoryManager to serve mock repositories 201 rm = RepositoryManager(self.env) 202 203 class TestRepositoryManager(rm.__class__): 204 def get_repository(self, reponame): 205 if reponame == 'scoped': 206 def get_changeset(rev): 207 if rev == 123: 208 def get_changes(): 209 yield ('/dir1/file',) 210 elif rev == 456: 211 def get_changes(): 212 yield ('/dir2/file',) 213 else: 214 def get_changes(): 215 return iter([]) 216 return Mock(get_changes=get_changes) 217 return Mock(scope='/scope', 218 get_changeset=get_changeset) 219 return Mock() 220 221 rm.__class__ = TestRepositoryManager 192 222 193 223 def tearDown(self): 194 224 self.env.reset_db() 195 225 os.remove(self.authz) 196 226 197 def assertP ermission(self, result, user, reponame, path):227 def assertPathPerm(self, result, user, reponame=None, path=None): 198 228 """Assert that `user` is granted access `result` to `path` within 199 229 the repository `reponame`. 200 230 """ 201 resource = Resource('source', path, 202 parent=Resource('repository', reponame)) 231 resource = None 232 if reponame is not None: 233 resource = Resource('source', path, 234 parent=Resource('repository', reponame)) 203 235 for perm in ('BROWSER_VIEW', 'FILE_VIEW', 'LOG_VIEW'): 204 236 check = self.policy.check_permission(perm, user, resource, None) 205 237 self.assertEqual(result, check) 206 238 239 def assertRevPerm(self, result, user, reponame=None, rev=None): 240 """Assert that `user` is granted access `result` to `rev` within 241 the repository `reponame`. 242 """ 243 resource = None 244 if reponame is not None: 245 resource = Resource('changeset', rev, 246 parent=Resource('repository', reponame)) 247 check = self.policy.check_permission('CHANGESET_VIEW', user, resource, 248 None) 249 self.assertEqual(result, check) 250 251 def test_coarse_permissions(self): 252 # Granted to all due to wildcard 253 self.assertPathPerm(True, 'unknown') 254 self.assertPathPerm(True, 'joe') 255 self.assertRevPerm(True, 'unknown') 256 self.assertRevPerm(True, 'joe') 257 # Granted if at least one fine permission is granted 258 self.policy._mtime = 0 259 create_file(self.authz, """\ 260 [/somepath] 261 joe = r 262 denied = 263 [/otherpath] 264 jane = r 265 $anonymous = r 266 """) 267 self.assertPathPerm(None, 'unknown') 268 self.assertRevPerm(None, 'unknown') 269 self.assertPathPerm(None, 'denied') 270 self.assertRevPerm(None, 'denied') 271 self.assertPathPerm(True, 'joe') 272 self.assertRevPerm(True, 'joe') 273 self.assertPathPerm(True, 'jane') 274 self.assertRevPerm(True, 'jane') 275 self.assertPathPerm(True, 'anonymous') 276 self.assertRevPerm(True, 'anonymous') 277 207 278 def test_default_permission(self): 208 279 # By default, permissions are undecided 209 self.assertP ermission(None, 'joe', '', '/not_defined')210 self.assertP ermission(None, 'jane', 'repo', '/not/defined/either')280 self.assertPathPerm(None, 'joe', '', '/not_defined') 281 self.assertPathPerm(None, 'jane', 'repo', '/not/defined/either') 211 282 212 283 def test_read_write(self): 213 284 # Allow 'r' and 'rw' entries, deny 'w' and empty entries 214 self.assertP ermission(True, 'user', '', '/readonly')215 self.assertP ermission(True, 'user', '', '/readwrite')216 self.assertP ermission(False, 'user', '', '/writeonly')217 self.assertP ermission(False, 'user', '', '/empty')285 self.assertPathPerm(True, 'user', '', '/readonly') 286 self.assertPathPerm(True, 'user', '', '/readwrite') 287 self.assertPathPerm(False, 'user', '', '/writeonly') 288 self.assertPathPerm(False, 'user', '', '/empty') 218 289 219 290 def test_trailing_slashes(self): 220 291 # Combinations of trailing slashes in the file and in the path 221 self.assertP ermission(True, 'user', '', '/trailing_a')222 self.assertP ermission(True, 'user', '', '/trailing_a/')223 self.assertP ermission(True, 'user', '', '/trailing_b')224 self.assertP ermission(True, 'user', '', '/trailing_b/')292 self.assertPathPerm(True, 'user', '', '/trailing_a') 293 self.assertPathPerm(True, 'user', '', '/trailing_a/') 294 self.assertPathPerm(True, 'user', '', '/trailing_b') 295 self.assertPathPerm(True, 'user', '', '/trailing_b/') 225 296 226 297 def test_sub_path(self): 227 298 # Permissions are inherited from containing directories 228 self.assertP ermission(True, 'user', '', '/sub/path')229 self.assertP ermission(True, 'user', '', '/sub/path/test')230 self.assertP ermission(True, 'user', '', '/sub/path/other/sub')299 self.assertPathPerm(True, 'user', '', '/sub/path') 300 self.assertPathPerm(True, 'user', '', '/sub/path/test') 301 self.assertPathPerm(True, 'user', '', '/sub/path/other/sub') 231 302 232 303 def test_module_usage(self): 233 304 # If a module name is specified, the rules are specific to the module 234 self.assertP ermission(True, 'user', 'module', '/module_a')235 self.assertP ermission(None, 'user', 'module', '/module_b')305 self.assertPathPerm(True, 'user', 'module', '/module_a') 306 self.assertPathPerm(None, 'user', 'module', '/module_b') 236 307 # If a module is specified, but the configuration contains a non-module 237 308 # path, the non-module path can still apply 238 self.assertP ermission(True, 'user', 'module', '/module_c')309 self.assertPathPerm(True, 'user', 'module', '/module_c') 239 310 # The module-specific rule takes precedence 240 self.assertP ermission(False, 'user', 'module', '/module_d')311 self.assertPathPerm(False, 'user', 'module', '/module_d') 241 312 242 313 def test_wildcard(self): 243 314 # The * wildcard matches all users, including anonymous 244 self.assertP ermission(True, 'anonymous', '', '/wildcard')245 self.assertP ermission(True, 'joe', '', '/wildcard')246 self.assertP ermission(True, 'jane', '', '/wildcard')315 self.assertPathPerm(True, 'anonymous', '', '/wildcard') 316 self.assertPathPerm(True, 'joe', '', '/wildcard') 317 self.assertPathPerm(True, 'jane', '', '/wildcard') 247 318 248 319 def test_special_tokens(self): 249 320 # The $anonymous token matches only anonymous users 250 self.assertP ermission(True, 'anonymous', '', '/special/anonymous')251 self.assertP ermission(None, 'user', '', '/special/anonymous')321 self.assertPathPerm(True, 'anonymous', '', '/special/anonymous') 322 self.assertPathPerm(None, 'user', '', '/special/anonymous') 252 323 # The $authenticated token matches all authenticated users 253 self.assertP ermission(None, 'anonymous', '', '/special/authenticated')254 self.assertP ermission(True, 'joe', '', '/special/authenticated')255 self.assertP ermission(True, 'jane', '', '/special/authenticated')324 self.assertPathPerm(None, 'anonymous', '', '/special/authenticated') 325 self.assertPathPerm(True, 'joe', '', '/special/authenticated') 326 self.assertPathPerm(True, 'jane', '', '/special/authenticated') 256 327 257 328 def test_groups(self): 258 329 # Groups are specified in a separate section and used with an @ prefix 259 self.assertP ermission(True, 'user', '', '/groups_a')330 self.assertPathPerm(True, 'user', '', '/groups_a') 260 331 # Groups can also be members of other groups 261 self.assertP ermission(True, 'user', '', '/groups_b')332 self.assertPathPerm(True, 'user', '', '/groups_b') 262 333 # Groups should not be defined cyclically, but they are still handled 263 334 # correctly to avoid infinite loops 264 self.assertP ermission(True, 'user', '', '/cyclic')335 self.assertPathPerm(True, 'user', '', '/cyclic') 265 336 266 337 def test_precedence(self): 267 338 # Module-specific sections take precedence over non-module sections 268 self.assertP ermission(False, 'user', 'module', '/precedence_a')339 self.assertPathPerm(False, 'user', 'module', '/precedence_a') 269 340 # The most specific section applies 270 self.assertP ermission(True, 'user', '', '/precedence_b/sub/test')271 self.assertP ermission(False, 'user', '', '/precedence_b/sub')272 self.assertP ermission(True, 'user', '', '/precedence_b')341 self.assertPathPerm(True, 'user', '', '/precedence_b/sub/test') 342 self.assertPathPerm(False, 'user', '', '/precedence_b/sub') 343 self.assertPathPerm(True, 'user', '', '/precedence_b') 273 344 # Within a section, the first matching rule applies 274 self.assertP ermission(False, 'user', '', '/precedence_c')275 self.assertP ermission(True, 'user', '', '/precedence_d')345 self.assertPathPerm(False, 'user', '', '/precedence_c') 346 self.assertPathPerm(True, 'user', '', '/precedence_d') 276 347 277 348 def test_aliases(self): 278 349 # Aliases are specified in a separate section and used with an & prefix 279 self.assertP ermission(True, 'Mr Hyde', '', '/aliases_a')350 self.assertPathPerm(True, 'Mr Hyde', '', '/aliases_a') 280 351 # Aliases can also be used in groups 281 self.assertPermission(True, 'Mr Hyde', '', '/aliases_b') 352 self.assertPathPerm(True, 'Mr Hyde', '', '/aliases_b') 353 354 def test_scoped_repository(self): 355 # Take repository scope into account 356 self.assertPathPerm(True, 'joe', 'scoped', '/dir1') 357 self.assertPathPerm(None, 'joe', 'scoped', '/dir2') 358 self.assertPathPerm(True, 'joe', 'scoped', '/') 359 self.assertPathPerm(None, 'jane', 'scoped', '/dir1') 360 self.assertPathPerm(True, 'jane', 'scoped', '/dir2') 361 self.assertPathPerm(True, 'jane', 'scoped', '/') 362 363 def test_changesets(self): 364 # Changesets are allowed if at least one changed path is allowed, or 365 # if the changeset is empty 366 self.assertRevPerm(True, 'joe', 'scoped', 123) 367 self.assertRevPerm(None, 'joe', 'scoped', 456) 368 self.assertRevPerm(True, 'joe', 'scoped', 789) 369 self.assertRevPerm(None, 'jane', 'scoped', 123) 370 self.assertRevPerm(True, 'jane', 'scoped', 456) 371 self.assertRevPerm(True, 'jane', 'scoped', 789) 372 self.assertRevPerm(None, 'user', 'scoped', 123) 373 self.assertRevPerm(None, 'user', 'scoped', 456) 374 self.assertRevPerm(True, 'user', 'scoped', 789) 282 375 283 376 284 377 def suite():
