Edgewall Software

Ticket #9566: 9566-scoped-repos-r10006.patch

File 9566-scoped-repos-r10006.patch, 13.8 KB (added by rblank, 22 months ago)

Support for scoped repositories.

  • trac/versioncontrol/svn_authz.py

    diff --git a/trac/versioncontrol/svn_authz.py b/trac/versioncontrol/svn_authz.py
    a b  
    4040        path = path[:idx + 1] 
    4141 
    4242 
     43def join(*args): 
     44    args = (arg.strip('/') for arg in args) 
     45    return '/'.join(arg for arg in args if arg) 
     46 
     47 
    4348class ParseError(Exception): 
    4449    """Exception thrown for parse errors in authz files""" 
    4550 
     
    125130 
    126131    authz_module_name = Option('trac', 'authz_module_name', '', 
    127132        """The module prefix used in the `authz_file` for the default 
    128         repository. If left empty, the global sections will be used. 
     133        repository. If left empty, the global section is used. 
    129134        """) 
    130135 
    131136    _mtime = 0 
    132137    _authz = {} 
    133138    _users = set() 
    134139     
     140    _handled_perms = frozenset([('source', 'BROWSER_VIEW'), 
     141                                ('source', 'FILE_VIEW'), 
     142                                ('source', 'LOG_VIEW'), 
     143                                ('changeset', 'CHANGESET_VIEW')]) 
     144 
    135145    # IPermissionPolicy methods 
    136146 
    137147    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'): 
     148        if (resource.realm, action) in self._handled_perms: 
    143149            authz, users = self._get_authz_info() 
    144150            if authz is None: 
    145151                return False 
     152             
     153            if username == 'anonymous': 
     154                usernames = ('$anonymous', '*') 
     155            else: 
     156                usernames = (username, '$authenticated', '*') 
    146157            if resource is None: 
    147158                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('/') 
     159 
     160            rm = RepositoryManager(self.env) 
     161            repos = rm.get_repository(resource.parent.id) 
     162            scope = getattr(repos, 'scope', '') 
     163            modules = [resource.parent.id or self.authz_module_name] 
     164            if modules[0]: 
     165                modules.append('') 
     166 
     167            def check_path(path): 
     168                path = '/' + join(scope, path) 
    153169                if path != '/': 
    154170                    path += '/' 
    155171                 
     
    170186                       if spath.startswith(path) 
    171187                       for user in usernames): 
    172188                    return True 
     189             
     190            if resource.realm == 'source': 
     191                return check_path(resource.id) 
    173192 
    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) 
     193            elif resource.realm == 'changeset': 
    183194                changes = list(repos.get_changeset(resource.id).get_changes()) 
    184                 if not changes: 
     195                if not changes or any(check_path(change[0]) 
     196                                      for change in changes): 
    185197                    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) 
    190198 
    191199    def _get_authz_info(self): 
    192200        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  
    1616import unittest 
    1717 
    1818from trac.resource import Resource 
    19 from trac.test import EnvironmentStub 
     19from trac.test import EnvironmentStub, Mock 
    2020from trac.util import create_file 
     21from trac.versioncontrol.api import RepositoryManager 
    2122from trac.versioncontrol.svn_authz import AuthzSourcePolicy, ParseError, \ 
    2223                                          parse 
    2324 
     
    185186&jekyll = r 
    186187[/aliases_b] 
    187188@alias2 = r 
     189 
     190# Scoped repository 
     191[scoped:/scope/dir1] 
     192joe = r 
     193[scoped:/scope/dir2] 
     194jane = r 
    188195""") 
    189196        self.env = EnvironmentStub(enable=[AuthzSourcePolicy]) 
    190197        self.env.config.set('trac', 'authz_file', self.authz) 
    191198        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 
    192222 
    193223    def tearDown(self): 
    194224        self.env.reset_db() 
    195225        os.remove(self.authz) 
    196226 
    197     def assertPermission(self, result, user, reponame, path): 
     227    def assertPathPerm(self, result, user, reponame, path): 
    198228        """Assert that `user` is granted access `result` to `path` within 
    199229        the repository `reponame`. 
    200230        """ 
     
    204234            check = self.policy.check_permission(perm, user, resource, None) 
    205235            self.assertEqual(result, check) 
    206236         
     237    def assertRevPerm(self, result, user, reponame, rev): 
     238        """Assert that `user` is granted access `result` to `rev` within 
     239        the repository `reponame`. 
     240        """ 
     241        resource = Resource('changeset', rev, 
     242                            parent=Resource('repository', reponame)) 
     243        check = self.policy.check_permission('CHANGESET_VIEW', user, resource, 
     244                                             None) 
     245        self.assertEqual(result, check) 
     246         
    207247    def test_default_permission(self): 
    208248        # By default, permissions are undecided 
    209         self.assertPermission(None, 'joe', '', '/not_defined') 
    210         self.assertPermission(None, 'jane', 'repo', '/not/defined/either') 
     249        self.assertPathPerm(None, 'joe', '', '/not_defined') 
     250        self.assertPathPerm(None, 'jane', 'repo', '/not/defined/either') 
    211251 
    212252    def test_read_write(self): 
    213253        # Allow 'r' and 'rw' entries, deny 'w' and empty entries 
    214         self.assertPermission(True, 'user', '', '/readonly') 
    215         self.assertPermission(True, 'user', '', '/readwrite') 
    216         self.assertPermission(False, 'user', '', '/writeonly') 
    217         self.assertPermission(False, 'user', '', '/empty') 
     254        self.assertPathPerm(True, 'user', '', '/readonly') 
     255        self.assertPathPerm(True, 'user', '', '/readwrite') 
     256        self.assertPathPerm(False, 'user', '', '/writeonly') 
     257        self.assertPathPerm(False, 'user', '', '/empty') 
    218258 
    219259    def test_trailing_slashes(self): 
    220260        # Combinations of trailing slashes in the file and in the path 
    221         self.assertPermission(True, 'user', '', '/trailing_a') 
    222         self.assertPermission(True, 'user', '', '/trailing_a/') 
    223         self.assertPermission(True, 'user', '', '/trailing_b') 
    224         self.assertPermission(True, 'user', '', '/trailing_b/') 
     261        self.assertPathPerm(True, 'user', '', '/trailing_a') 
     262        self.assertPathPerm(True, 'user', '', '/trailing_a/') 
     263        self.assertPathPerm(True, 'user', '', '/trailing_b') 
     264        self.assertPathPerm(True, 'user', '', '/trailing_b/') 
    225265 
    226266    def test_sub_path(self): 
    227267        # Permissions are inherited from containing directories 
    228         self.assertPermission(True, 'user', '', '/sub/path') 
    229         self.assertPermission(True, 'user', '', '/sub/path/test') 
    230         self.assertPermission(True, 'user', '', '/sub/path/other/sub') 
     268        self.assertPathPerm(True, 'user', '', '/sub/path') 
     269        self.assertPathPerm(True, 'user', '', '/sub/path/test') 
     270        self.assertPathPerm(True, 'user', '', '/sub/path/other/sub') 
    231271         
    232272    def test_module_usage(self): 
    233273        # If a module name is specified, the rules are specific to the module 
    234         self.assertPermission(True, 'user', 'module', '/module_a') 
    235         self.assertPermission(None, 'user', 'module', '/module_b') 
     274        self.assertPathPerm(True, 'user', 'module', '/module_a') 
     275        self.assertPathPerm(None, 'user', 'module', '/module_b') 
    236276        # If a module is specified, but the configuration contains a non-module 
    237277        # path, the non-module path can still apply 
    238         self.assertPermission(True, 'user', 'module', '/module_c') 
     278        self.assertPathPerm(True, 'user', 'module', '/module_c') 
    239279        # The module-specific rule takes precedence 
    240         self.assertPermission(False, 'user', 'module', '/module_d') 
     280        self.assertPathPerm(False, 'user', 'module', '/module_d') 
    241281 
    242282    def test_wildcard(self): 
    243283        # The * wildcard matches all users, including anonymous 
    244         self.assertPermission(True, 'anonymous', '', '/wildcard') 
    245         self.assertPermission(True, 'joe', '', '/wildcard') 
    246         self.assertPermission(True, 'jane', '', '/wildcard') 
     284        self.assertPathPerm(True, 'anonymous', '', '/wildcard') 
     285        self.assertPathPerm(True, 'joe', '', '/wildcard') 
     286        self.assertPathPerm(True, 'jane', '', '/wildcard') 
    247287 
    248288    def test_special_tokens(self): 
    249289        # The $anonymous token matches only anonymous users 
    250         self.assertPermission(True, 'anonymous', '', '/special/anonymous') 
    251         self.assertPermission(None, 'user', '', '/special/anonymous') 
     290        self.assertPathPerm(True, 'anonymous', '', '/special/anonymous') 
     291        self.assertPathPerm(None, 'user', '', '/special/anonymous') 
    252292        # The $authenticated token matches all authenticated users 
    253         self.assertPermission(None, 'anonymous', '', '/special/authenticated') 
    254         self.assertPermission(True, 'joe', '', '/special/authenticated') 
    255         self.assertPermission(True, 'jane', '', '/special/authenticated') 
     293        self.assertPathPerm(None, 'anonymous', '', '/special/authenticated') 
     294        self.assertPathPerm(True, 'joe', '', '/special/authenticated') 
     295        self.assertPathPerm(True, 'jane', '', '/special/authenticated') 
    256296 
    257297    def test_groups(self): 
    258298        # Groups are specified in a separate section and used with an @ prefix 
    259         self.assertPermission(True, 'user', '', '/groups_a') 
     299        self.assertPathPerm(True, 'user', '', '/groups_a') 
    260300        # Groups can also be members of other groups 
    261         self.assertPermission(True, 'user', '', '/groups_b') 
     301        self.assertPathPerm(True, 'user', '', '/groups_b') 
    262302        # Groups should not be defined cyclically, but they are still handled 
    263303        # correctly to avoid infinite loops 
    264         self.assertPermission(True, 'user', '', '/cyclic') 
     304        self.assertPathPerm(True, 'user', '', '/cyclic') 
    265305 
    266306    def test_precedence(self): 
    267307        # Module-specific sections take precedence over non-module sections 
    268         self.assertPermission(False, 'user', 'module', '/precedence_a') 
     308        self.assertPathPerm(False, 'user', 'module', '/precedence_a') 
    269309        # The most specific section applies 
    270         self.assertPermission(True, 'user', '', '/precedence_b/sub/test') 
    271         self.assertPermission(False, 'user', '', '/precedence_b/sub') 
    272         self.assertPermission(True, 'user', '', '/precedence_b') 
     310        self.assertPathPerm(True, 'user', '', '/precedence_b/sub/test') 
     311        self.assertPathPerm(False, 'user', '', '/precedence_b/sub') 
     312        self.assertPathPerm(True, 'user', '', '/precedence_b') 
    273313        # Within a section, the first matching rule applies 
    274         self.assertPermission(False, 'user', '', '/precedence_c') 
    275         self.assertPermission(True, 'user', '', '/precedence_d') 
     314        self.assertPathPerm(False, 'user', '', '/precedence_c') 
     315        self.assertPathPerm(True, 'user', '', '/precedence_d') 
    276316 
    277317    def test_aliases(self): 
    278318        # Aliases are specified in a separate section and used with an & prefix 
    279         self.assertPermission(True, 'Mr Hyde', '', '/aliases_a') 
     319        self.assertPathPerm(True, 'Mr Hyde', '', '/aliases_a') 
    280320        # Aliases can also be used in groups 
    281         self.assertPermission(True, 'Mr Hyde', '', '/aliases_b') 
     321        self.assertPathPerm(True, 'Mr Hyde', '', '/aliases_b') 
     322 
     323    def test_scoped_repository(self): 
     324        # Take repository scope into account 
     325        self.assertPathPerm(True, 'joe', 'scoped', '/dir1') 
     326        self.assertPathPerm(None, 'joe', 'scoped', '/dir2') 
     327        self.assertPathPerm(True, 'joe', 'scoped', '/') 
     328        self.assertPathPerm(None, 'jane', 'scoped', '/dir1') 
     329        self.assertPathPerm(True, 'jane', 'scoped', '/dir2') 
     330        self.assertPathPerm(True, 'jane', 'scoped', '/') 
     331     
     332    def test_changesets(self): 
     333        # Changesets are allowed if at least one changed path is allowed, or 
     334        # if the changeset is empty 
     335        self.assertRevPerm(True, 'joe', 'scoped', 123) 
     336        self.assertRevPerm(None, 'joe', 'scoped', 456) 
     337        self.assertRevPerm(True, 'joe', 'scoped', 789) 
     338        self.assertRevPerm(None, 'jane', 'scoped', 123) 
     339        self.assertRevPerm(True, 'jane', 'scoped', 456) 
     340        self.assertRevPerm(True, 'jane', 'scoped', 789) 
     341        self.assertRevPerm(None, 'user', 'scoped', 123) 
     342        self.assertRevPerm(None, 'user', 'scoped', 456) 
     343        self.assertRevPerm(True, 'user', 'scoped', 789) 
    282344 
    283345 
    284346def suite():