Edgewall Software

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

File 9566-scoped-repos-2-r10006.patch, 15.5 KB (added by rblank, 21 months ago)

Handle coarse permissions

  • 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([(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 
    135149    # IPermissionPolicy methods 
    136150 
    137151    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: 
    143154            authz, users = self._get_authz_info() 
    144155            if authz is None: 
    145156                return False 
     157             
     158            if username == 'anonymous': 
     159                usernames = ('$anonymous', '*') 
     160            else: 
     161                usernames = (username, '$authenticated', '*') 
    146162            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) 
    153174                if path != '/': 
    154175                    path += '/' 
    155176                 
     
    170191                       if spath.startswith(path) 
    171192                       for user in usernames): 
    172193                    return True 
     194             
     195            if realm == 'source': 
     196                return check_path(resource.id) 
    173197 
    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': 
    183199                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): 
    185202                    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) 
    190203 
    191204    def _get_authz_info(self): 
    192205        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=None, path=None): 
    198228        """Assert that `user` is granted access `result` to `path` within 
    199229        the repository `reponame`. 
    200230        """ 
    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)) 
    203235        for perm in ('BROWSER_VIEW', 'FILE_VIEW', 'LOG_VIEW'): 
    204236            check = self.policy.check_permission(perm, user, resource, None) 
    205237            self.assertEqual(result, check) 
    206238         
     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] 
     261joe = r 
     262denied = 
     263[/otherpath] 
     264jane = 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 
    207278    def test_default_permission(self): 
    208279        # By default, permissions are undecided 
    209         self.assertPermission(None, 'joe', '', '/not_defined') 
    210         self.assertPermission(None, 'jane', 'repo', '/not/defined/either') 
     280        self.assertPathPerm(None, 'joe', '', '/not_defined') 
     281        self.assertPathPerm(None, 'jane', 'repo', '/not/defined/either') 
    211282 
    212283    def test_read_write(self): 
    213284        # 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') 
     285        self.assertPathPerm(True, 'user', '', '/readonly') 
     286        self.assertPathPerm(True, 'user', '', '/readwrite') 
     287        self.assertPathPerm(False, 'user', '', '/writeonly') 
     288        self.assertPathPerm(False, 'user', '', '/empty') 
    218289 
    219290    def test_trailing_slashes(self): 
    220291        # 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/') 
     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/') 
    225296 
    226297    def test_sub_path(self): 
    227298        # 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') 
     299        self.assertPathPerm(True, 'user', '', '/sub/path') 
     300        self.assertPathPerm(True, 'user', '', '/sub/path/test') 
     301        self.assertPathPerm(True, 'user', '', '/sub/path/other/sub') 
    231302         
    232303    def test_module_usage(self): 
    233304        # 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') 
     305        self.assertPathPerm(True, 'user', 'module', '/module_a') 
     306        self.assertPathPerm(None, 'user', 'module', '/module_b') 
    236307        # If a module is specified, but the configuration contains a non-module 
    237308        # path, the non-module path can still apply 
    238         self.assertPermission(True, 'user', 'module', '/module_c') 
     309        self.assertPathPerm(True, 'user', 'module', '/module_c') 
    239310        # The module-specific rule takes precedence 
    240         self.assertPermission(False, 'user', 'module', '/module_d') 
     311        self.assertPathPerm(False, 'user', 'module', '/module_d') 
    241312 
    242313    def test_wildcard(self): 
    243314        # 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') 
     315        self.assertPathPerm(True, 'anonymous', '', '/wildcard') 
     316        self.assertPathPerm(True, 'joe', '', '/wildcard') 
     317        self.assertPathPerm(True, 'jane', '', '/wildcard') 
    247318 
    248319    def test_special_tokens(self): 
    249320        # The $anonymous token matches only anonymous users 
    250         self.assertPermission(True, 'anonymous', '', '/special/anonymous') 
    251         self.assertPermission(None, 'user', '', '/special/anonymous') 
     321        self.assertPathPerm(True, 'anonymous', '', '/special/anonymous') 
     322        self.assertPathPerm(None, 'user', '', '/special/anonymous') 
    252323        # 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') 
     324        self.assertPathPerm(None, 'anonymous', '', '/special/authenticated') 
     325        self.assertPathPerm(True, 'joe', '', '/special/authenticated') 
     326        self.assertPathPerm(True, 'jane', '', '/special/authenticated') 
    256327 
    257328    def test_groups(self): 
    258329        # Groups are specified in a separate section and used with an @ prefix 
    259         self.assertPermission(True, 'user', '', '/groups_a') 
     330        self.assertPathPerm(True, 'user', '', '/groups_a') 
    260331        # Groups can also be members of other groups 
    261         self.assertPermission(True, 'user', '', '/groups_b') 
     332        self.assertPathPerm(True, 'user', '', '/groups_b') 
    262333        # Groups should not be defined cyclically, but they are still handled 
    263334        # correctly to avoid infinite loops 
    264         self.assertPermission(True, 'user', '', '/cyclic') 
     335        self.assertPathPerm(True, 'user', '', '/cyclic') 
    265336 
    266337    def test_precedence(self): 
    267338        # Module-specific sections take precedence over non-module sections 
    268         self.assertPermission(False, 'user', 'module', '/precedence_a') 
     339        self.assertPathPerm(False, 'user', 'module', '/precedence_a') 
    269340        # 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') 
     341        self.assertPathPerm(True, 'user', '', '/precedence_b/sub/test') 
     342        self.assertPathPerm(False, 'user', '', '/precedence_b/sub') 
     343        self.assertPathPerm(True, 'user', '', '/precedence_b') 
    273344        # Within a section, the first matching rule applies 
    274         self.assertPermission(False, 'user', '', '/precedence_c') 
    275         self.assertPermission(True, 'user', '', '/precedence_d') 
     345        self.assertPathPerm(False, 'user', '', '/precedence_c') 
     346        self.assertPathPerm(True, 'user', '', '/precedence_d') 
    276347 
    277348    def test_aliases(self): 
    278349        # Aliases are specified in a separate section and used with an & prefix 
    279         self.assertPermission(True, 'Mr Hyde', '', '/aliases_a') 
     350        self.assertPathPerm(True, 'Mr Hyde', '', '/aliases_a') 
    280351        # 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) 
    282375 
    283376 
    284377def suite():