← Back to team overview

launchpad-reviewers team mailing list archive

Re: [Merge] lp:~twom/launchpad/rework-git-permissions-for-shadowing into lp:launchpad

 

Review: Needs Fixing



Diff comments:

> 
> === modified file 'lib/lp/code/interfaces/gitrule.py'
> --- lib/lp/code/interfaces/gitrule.py	2018-10-16 15:29:37 +0000
> +++ lib/lp/code/interfaces/gitrule.py	2018-10-18 13:49:45 +0000
> @@ -71,6 +71,16 @@
>  
>      grants = Attribute("The access grants for this rule.")
>  
> +    def findRuleGrantsByGrantee(grantee):
> +        """Find the grants for a grantee applied to this repository.
> +
> +        :param grantee: The Person affected
> +        """
> +
> +    def findRuleGrantsForRepositoryOwner():
> +        """Find the grants of type REPOSITORY_OWNER applied to this repository.
> +        """

s/this repository/this rule/ on both these docstrings, if you're leaving them here.  (But see below; I think it might be better to move this back to IGitRepository.)

> +
>  
>  class IGitRuleEditableAttributes(Interface):
>      """`IGitRule` attributes that can be edited.
> 
> === modified file 'lib/lp/code/xmlrpc/git.py'
> --- lib/lp/code/xmlrpc/git.py	2018-10-17 10:09:33 +0000
> +++ lib/lp/code/xmlrpc/git.py	2018-10-18 13:49:45 +0000
> @@ -414,3 +418,70 @@
>              self._listRefRules,
>              translated_path,
>              )
> +
> +    def make_regex(self, pattern):
> +        return re.compile(self.glob_to_re(pattern.rstrip(b'\n')))
> +
> +    def glob_to_re(self, s):
> +        """Convert a glob to a regular expression.
> +
> +        The only wildcard supported is "*", to match any path segment.

This is now any number of characters, rather than just any path segment.

However, should we consider just using the standard library's `fnmatch` instead?  That's what I'd specified before I realised that turnip had its own implementation, and I think it probably still makes sense: it's easy enough to explain, it's a bit more useful, and all its metacharacters are invalid in ref names.

> +        """
> +        return b'^%s\Z' % (
> +            b''.join(b'.*' if c == b'*' else re.escape(c) for c in s))
> +
> +    def _find_matching_rules(self, repository, ref_path):
> +        """Find all matching rules for a given ref path"""
> +

Extra blank line.

> +        matching_rules = []
> +        for rule in repository.rules:
> +            regex = self.make_regex(rule.ref_pattern)
> +            if regex.match(ref_path):
> +                matching_rules.append(rule)
> +        return matching_rules
> +
> +    def _checkRefPermissions(self, requester, translated_path, ref_paths):
> +        repository = removeSecurityProxy(
> +            getUtility(IGitLookup).getByHostingPath(translated_path))
> +        is_owner = requester.inTeam(repository.owner)
> +        result = {}
> +        for ref in ref_paths:
> +            matching_rules = self._find_matching_rules(repository, ref)
> +            if is_owner and not matching_rules:
> +                result[ref] = ['create', 'push', 'force_push']
> +                continue
> +            seen_grantees = []

This should be a set, so that membership checks are cheap.

> +            union_permissions = set()
> +            for rule in matching_rules:
> +                grants = rule.findRuleGrantsByGrantee(requester)
> +                if is_owner:
> +                    owner_grants = rule.findRuleGrantsForRepositoryOwner()
> +                    grants = grants.union(owner_grants)

This is potentially quite a lot of database queries: it's one or two per matching rule per ref being pushed.  Instead, could you collect all the grants across the whole repository up-front, organise them into a dict indexed by rule, and look them up in that dict as you go?  The DB indexes are arranged with the intent of making this an efficient operation.

> +
> +                for grant in grants:
> +                    if (grant.grantee, grant.grantee_type) in seen_grantees:
> +                        continue
> +                    permissions = self._buildPermissions(grant)
> +                    union_permissions.update(permissions)
> +                    seen_grantees.append(
> +                        (grant.grantee, grant.grantee_type)
> +                    )

Could be on one line.

> +
> +                owner_grantees = any(x[1] == GitGranteeType.REPOSITORY_OWNER
> +                                     for x in seen_grantees)
> +                if is_owner and not owner_grantees:

if is_owner and (None, GitGranteeType.REPOSITORY_OWNER) not in seen_grantees:

> +                    union_permissions.update(['create', 'push'])
> +
> +            sorted_permissions = self._sortPermissions(union_permissions)
> +            result[ref] = sorted_permissions
> +        return result
> +
> +    def checkRefPermissions(self, translated_path, ref_paths, auth_params):
> +        """ See `IGitAPI`"""
> +        requester_id = auth_params.get("uid")
> +        return run_with_login(
> +            requester_id,
> +            self._checkRefPermissions,
> +            translated_path,
> +            ref_paths
> +        )
> 
> === modified file 'lib/lp/code/xmlrpc/tests/test_git.py'
> --- lib/lp/code/xmlrpc/tests/test_git.py	2018-10-17 10:09:33 +0000
> +++ lib/lp/code/xmlrpc/tests/test_git.py	2018-10-18 13:49:45 +0000
> @@ -669,7 +669,7 @@
>                  }),
>              ]))
>  
> -    def test_listRefRules_grantee_example_one(self):
> +    def _make_example_repository(self):

It might be worth making this table-driven, especially once you add the second set of examples.

>          user_a = self.factory.makePerson()
>          user_b = self.factory.makePerson()
>          user_c = self.factory.makePerson()
> @@ -710,247 +714,106 @@
>          self.factory.makeGitRuleGrant(
>              rule=rule, grantee=stable_team, can_create=True)
>  
> -        results = self.git_api.listRefRules(
> +        return user_a, user_b, user_c, stable_team, next_team, repository
> +
> +    def test_listRefRules_grantee_example_one(self):

Please rename all these tests to `test_checkRefPermissions_...` to avoid confusion.

> +        test_ref_paths = [
> +            'refs/heads/stable/next', 'refs/heads/stable/protected',
> +            'refs/heads/stable/foo', 'refs/heads/archived/foo',
> +            'refs/heads/foo/next', 'refs/heads/unprotected',
> +            'refs/tags/1.0',
> +        ]

`test_ref_paths` is common to each set of examples for this set of rules, so could you factor this out (maybe add it to the already large pile of stuff returned from `_make_example_repository`)?

> +        #test_ref_paths = ['refs/heads/stable/next']

Don't leave commented code lying around.

> +        user_a, _, _, _, _, repository = self._make_example_repository()
> +
> +        results = self.git_api.checkRefPermissions(
>              repository.getInternalPath(),
> +            test_ref_paths,
>              {'uid': user_a.id})
>  
> -        self.assertThat(results, MatchesListwise([
> -            MatchesDict(
> -                {'ref_pattern': Equals('refs/heads/stable/next'),
> -                 'permissions': Equals(['push', 'force_push']),
> -                }),
> -            MatchesDict(
> -                {'ref_pattern': Equals('refs/heads/archived/*'),
> -                 'permissions': Equals([]),
> -                }),
> -            MatchesDict(
> -                {'ref_pattern': Equals('refs/heads/stable/*'),
> -                 'permissions': Equals(['create', 'push']),
> -                }),
> -            MatchesDict(
> -                {'ref_pattern': Equals('refs/heads/*/next'),
> -                 'permissions': Equals(['create', 'push']),
> -                }),
> -            MatchesDict(
> -                {'ref_pattern': Equals('refs/tags/*'),
> -                 'permissions': Equals(['create']),
> -                }),
> -            MatchesDict(
> -                {'ref_pattern': Equals('*'),
> -                 'permissions': Equals(['create', 'push', 'force_push']),
> -                }),
> -            ]))
> +        self.assertThat(results, MatchesDict({
> +            'refs/heads/stable/next': Equals(['push', 'force_push']),
> +            'refs/heads/stable/protected': Equals(['create', 'push']),
> +            'refs/heads/stable/foo': Equals(['create', 'push']),
> +            'refs/heads/archived/foo': Equals([]),
> +            'refs/heads/foo/next': Equals(['create', 'push']),
> +            'refs/heads/unprotected': Equals(['create', 'push', 'force_push']),
> +            'refs/tags/1.0': Equals(['create']),
> +        }))
>  
>      def test_listRefRules_grantee_example_two(self):
> -        user_a = self.factory.makePerson()
> -        user_b = self.factory.makePerson()
> -        user_c = self.factory.makePerson()
> -        stable_team = self.factory.makeTeam(members=[user_a, user_b])
> -        next_team = self.factory.makeTeam(members=[user_b, user_c])
> -
> -        repository = removeSecurityProxy(
> -            self.factory.makeGitRepository(owner=user_a))
> -
> -        rule = self.factory.makeGitRule(
> -            repository, ref_pattern=u'refs/heads/stable/next')
> -        self.factory.makeGitRuleGrant(
> -            rule=rule, grantee=user_a, can_force_push=True)
> -
> -        rule = self.factory.makeGitRule(
> -            repository, ref_pattern=u'refs/heads/archived/*')
> -        self.factory.makeGitRuleGrant(
> -            rule=rule, grantee=user_a)
> -        self.factory.makeGitRuleGrant(
> -            rule=rule, grantee=user_b, can_create=True)
> -
> -        rule = self.factory.makeGitRule(
> -            repository, ref_pattern=u'refs/heads/stable/*')
> -        self.factory.makeGitRuleGrant(
> -            rule=rule, grantee=stable_team, can_push=True)
> -
> -        rule = self.factory.makeGitRule(
> -            repository, ref_pattern=u'refs/heads/*/next')
> -        self.factory.makeGitRuleGrant(
> -            rule=rule, grantee=next_team, can_force_push=True)
> -
> -        rule = self.factory.makeGitRule(
> -            repository, ref_pattern=u'refs/tags/*')
> -        self.factory.makeGitRuleGrant(
> -            rule=rule, grantee=user_a, can_create=True)
> -        self.factory.makeGitRuleGrant(
> -            rule=rule, grantee=stable_team, can_create=True)
> -
> -        results = self.git_api.listRefRules(
> +
> +        test_ref_paths = [
> +            'refs/heads/stable/next', 'refs/heads/stable/protected',
> +            'refs/heads/stable/foo', 'refs/heads/archived/foo',
> +            'refs/heads/foo/next', 'refs/heads/unprotected',
> +            'refs/tags/1.0',
> +        ]
> +        _, user_b, _, _, _, repository = self._make_example_repository()
> +
> +        results = self.git_api.checkRefPermissions(
>              repository.getInternalPath(),
> +            test_ref_paths,
>              {'uid': user_b.id})
>  
> -        self.assertThat(results, MatchesListwise([
> -            MatchesDict(
> -                {'ref_pattern': Equals('refs/heads/stable/next'),
> -                 'permissions': Equals([]),
> -                }),
> -            MatchesDict(
> -                {'ref_pattern': Equals('refs/heads/archived/*'),
> -                 'permissions': Equals(['create']),
> -                }),
> -            MatchesDict(
> -                {'ref_pattern': Equals('refs/heads/stable/*'),
> -                 'permissions': Equals(['push']),
> -                }),
> -            MatchesDict(
> -                {'ref_pattern': Equals('refs/heads/*/next'),
> -                 'permissions': Equals(['push', 'force_push']),
> -                }),
> -            MatchesDict(
> -                {'ref_pattern': Equals('refs/tags/*'),
> -                 'permissions': Equals(['create']),
> -                }),
> -            MatchesDict(
> -                {'ref_pattern': Equals('*'),
> -                 'permissions': Equals([]),
> -                }),
> -            ]))
> +        self.assertThat(results, MatchesDict({
> +            'refs/heads/stable/next': Equals(['push', 'force_push']),
> +            'refs/heads/stable/protected': Equals([]),
> +            'refs/heads/stable/foo': Equals(['push']),
> +            'refs/heads/archived/foo': Equals(['create']),
> +            'refs/heads/foo/next': Equals(['push', 'force_push']),
> +            'refs/heads/unprotected': Equals([]),
> +            'refs/tags/1.0': Equals(['create']),
> +        }))
>  
>      def test_listRefRules_grantee_example_three(self):
> -        user_a = self.factory.makePerson()
> -        user_b = self.factory.makePerson()
> -        user_c = self.factory.makePerson()
> -        stable_team = self.factory.makeTeam(members=[user_a, user_b])
> -        next_team = self.factory.makeTeam(members=[user_b, user_c])
> -
> -        repository = removeSecurityProxy(
> -            self.factory.makeGitRepository(owner=user_a))
> -
> -        rule = self.factory.makeGitRule(
> -            repository, ref_pattern=u'refs/heads/stable/next')
> -        self.factory.makeGitRuleGrant(
> -            rule=rule, grantee=user_a, can_force_push=True)
> -
> -        rule = self.factory.makeGitRule(
> -            repository, ref_pattern=u'refs/heads/archived/*')
> -        self.factory.makeGitRuleGrant(
> -            rule=rule, grantee=user_a)
> -        self.factory.makeGitRuleGrant(
> -            rule=rule, grantee=user_b, can_create=True)
> -
> -        rule = self.factory.makeGitRule(
> -            repository, ref_pattern=u'refs/heads/stable/*')
> -        self.factory.makeGitRuleGrant(
> -            rule=rule, grantee=stable_team, can_push=True)
> -
> -        rule = self.factory.makeGitRule(
> -            repository, ref_pattern=u'refs/heads/*/next')
> -        self.factory.makeGitRuleGrant(
> -            rule=rule, grantee=next_team, can_force_push=True)
> -
> -        rule = self.factory.makeGitRule(
> -            repository, ref_pattern=u'refs/tags/*')
> -        self.factory.makeGitRuleGrant(
> -            rule=rule, grantee=user_a, can_create=True)
> -        self.factory.makeGitRuleGrant(
> -            rule=rule, grantee=stable_team, can_create=True)
> -
> -        results = self.git_api.listRefRules(
> +        test_ref_paths = [
> +            'refs/heads/stable/next', 'refs/heads/stable/protected',
> +            'refs/heads/stable/foo', 'refs/heads/archived/foo',
> +            'refs/heads/foo/next', 'refs/heads/unprotected',
> +            'refs/tags/1.0',
> +        ]
> +        _, _, user_c, _, _, repository = self._make_example_repository()
> +
> +        results = self.git_api.checkRefPermissions(
>              repository.getInternalPath(),
> +            test_ref_paths,
>              {'uid': user_c.id})
>  
> -        self.assertThat(results, MatchesListwise([
> -            MatchesDict(
> -                {'ref_pattern': Equals('refs/heads/stable/next'),
> -                 'permissions': Equals([]),
> -                }),
> -            MatchesDict(
> -                {'ref_pattern': Equals('refs/heads/archived/*'),
> -                 'permissions': Equals([]),
> -                }),
> -            MatchesDict(
> -                {'ref_pattern': Equals('refs/heads/stable/*'),
> -                 'permissions': Equals([]),
> -                }),
> -            MatchesDict(
> -                {'ref_pattern': Equals('refs/heads/*/next'),
> -                 'permissions': Equals(['push', 'force_push']),
> -                }),
> -            MatchesDict(
> -                {'ref_pattern': Equals('refs/tags/*'),
> -                 'permissions': Equals([]),
> -                }),
> -            MatchesDict(
> -                {'ref_pattern': Equals('*'),
> -                 'permissions': Equals([]),
> -                }),
> -            ]))
> +        self.assertThat(results, MatchesDict({
> +            'refs/heads/stable/next': Equals(['push', 'force_push']),
> +            'refs/heads/stable/protected': Equals([]),
> +            'refs/heads/stable/foo': Equals([]),
> +            'refs/heads/archived/foo': Equals([]),
> +            'refs/heads/foo/next': Equals(['push', 'force_push']),
> +            'refs/heads/unprotected': Equals([]),
> +            'refs/tags/1.0': Equals([]),
> +        }))
>  
>      def test_listRefRules_grantee_example_four(self):
> -        user_a = self.factory.makePerson()
> -        user_b = self.factory.makePerson()
> -        user_c = self.factory.makePerson()
>          user_d = self.factory.makePerson()
> -        stable_team = self.factory.makeTeam(members=[user_a, user_b])
> -        next_team = self.factory.makeTeam(members=[user_b, user_c])
> -
> -        repository = removeSecurityProxy(
> -            self.factory.makeGitRepository(owner=user_a))
> -
> -        rule = self.factory.makeGitRule(
> -            repository, ref_pattern=u'refs/heads/stable/next')
> -        self.factory.makeGitRuleGrant(
> -            rule=rule, grantee=user_a, can_force_push=True)
> -
> -        rule = self.factory.makeGitRule(
> -            repository, ref_pattern=u'refs/heads/archived/*')
> -        self.factory.makeGitRuleGrant(
> -            rule=rule, grantee=user_a)
> -        self.factory.makeGitRuleGrant(
> -            rule=rule, grantee=user_b, can_create=True)
> -
> -        rule = self.factory.makeGitRule(
> -            repository, ref_pattern=u'refs/heads/stable/*')
> -        self.factory.makeGitRuleGrant(
> -            rule=rule, grantee=stable_team, can_push=True)
> -
> -        rule = self.factory.makeGitRule(
> -            repository, ref_pattern=u'refs/heads/*/next')
> -        self.factory.makeGitRuleGrant(
> -            rule=rule, grantee=next_team, can_force_push=True)
> -
> -        rule = self.factory.makeGitRule(
> -            repository, ref_pattern=u'refs/tags/*')
> -        self.factory.makeGitRuleGrant(
> -            rule=rule, grantee=user_a, can_create=True)
> -        self.factory.makeGitRuleGrant(
> -            rule=rule, grantee=stable_team, can_create=True)
> -
> -        results = self.git_api.listRefRules(
> +        test_ref_paths = [
> +            'refs/heads/stable/next', 'refs/heads/stable/protected',
> +            'refs/heads/stable/foo', 'refs/heads/archived/foo',
> +            'refs/heads/foo/next', 'refs/heads/unprotected',
> +            'refs/tags/1.0',
> +        ]
> +        _, _, user_c, _, _, repository = self._make_example_repository()
> +
> +        results = self.git_api.checkRefPermissions(
>              repository.getInternalPath(),
> +            test_ref_paths,
>              {'uid': user_d.id})
>  
> -        self.assertThat(results, MatchesListwise([
> -            MatchesDict(
> -                {'ref_pattern': Equals('refs/heads/stable/next'),
> -                 'permissions': Equals([]),
> -                }),
> -            MatchesDict(
> -                {'ref_pattern': Equals('refs/heads/archived/*'),
> -                 'permissions': Equals([]),
> -                }),
> -            MatchesDict(
> -                {'ref_pattern': Equals('refs/heads/stable/*'),
> -                 'permissions': Equals([]),
> -                }),
> -            MatchesDict(
> -                {'ref_pattern': Equals('refs/heads/*/next'),
> -                 'permissions': Equals([]),
> -                }),
> -            MatchesDict(
> -                {'ref_pattern': Equals('refs/tags/*'),
> -                 'permissions': Equals([]),
> -                }),
> -            MatchesDict(
> -                {'ref_pattern': Equals('*'),
> -                 'permissions': Equals([]),
> -                }),
> -            ]))
> +        self.assertThat(results, MatchesDict({
> +            'refs/heads/stable/next': Equals([]),
> +            'refs/heads/stable/protected': Equals([]),
> +            'refs/heads/stable/foo': Equals([]),
> +            'refs/heads/archived/foo': Equals([]),
> +            'refs/heads/foo/next': Equals([]),
> +            'refs/heads/unprotected': Equals([]),
> +            'refs/tags/1.0': Equals([]),
> +        }))

Could you also add the other example (under "Alternatively:") in the spec?  It tests some things that I'm not completely sure this one does: at any rate it's all been sufficiently subtle that I'd rather have them all.

>  
>  
>  class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory):


-- 
https://code.launchpad.net/~twom/launchpad/rework-git-permissions-for-shadowing/+merge/357091
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.


References