← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Tom Wardill has proposed merging lp:~twom/launchpad/rework-git-permissions-for-shadowing into lp:launchpad.

Commit message:
Rework git branch permissions to improve shadowing for multiple grants

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~twom/launchpad/rework-git-permissions-for-shadowing/+merge/357091
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~twom/launchpad/rework-git-permissions-for-shadowing into lp:launchpad.
=== modified file 'lib/lp/code/interfaces/gitapi.py'
--- lib/lp/code/interfaces/gitapi.py	2018-09-28 13:53:41 +0000
+++ lib/lp/code/interfaces/gitapi.py	2018-10-18 13:49:45 +0000
@@ -73,3 +73,10 @@
 
         :returns: A list of rules for the user in the specified repository
         """
+
+    def checkRefPermissions(self, repository, ref_paths, user):
+        """Return a list of ref rules for a `user` in a `repository` that
+        match the input refs.
+
+        :returns: A list of rules for the user in the specified repository
+        """

=== modified file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py	2018-10-16 10:10:10 +0000
+++ lib/lp/code/interfaces/gitrepository.py	2018-10-18 13:49:45 +0000
@@ -748,16 +748,6 @@
         :param user: The `IPerson` who is moving the 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.
-        """
-
     @export_read_operation()
     @operation_for_version("devel")
     def canBeDeleted():

=== 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.
+        """
+
 
 class IGitRuleEditableAttributes(Interface):
     """`IGitRule` attributes that can be edited.

=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py	2018-10-16 10:10:10 +0000
+++ lib/lp/code/model/gitrepository.py	2018-10-18 13:49:45 +0000
@@ -72,7 +72,6 @@
 from lp.app.interfaces.services import IService
 from lp.code.enums import (
     BranchMergeProposalStatus,
-    GitGranteeType,
     GitObjectType,
     GitRepositoryType,
     )
@@ -1194,20 +1193,6 @@
         return Store.of(self).find(
             GitRuleGrant, GitRuleGrant.repository_id == self.id)
 
-    def findRuleGrantsByGrantee(self, grantee):
-        """See `IGitRepository`."""
-        clauses = [
-            GitRuleGrant.grantee_type == GitGranteeType.PERSON,
-            TeamParticipation.person == grantee,
-            GitRuleGrant.grantee == TeamParticipation.teamID
-            ]
-        return self.grants.find(*clauses).config(distinct=True)
-
-    def findRuleGrantsForRepositoryOwner(self):
-        """See `IGitRepository`."""
-        return self.grants.find(
-            GitRuleGrant.grantee_type == GitGranteeType.REPOSITORY_OWNER)
-
     def getActivity(self, changed_after=None):
         """See `IGitRepository`."""
         clauses = [GitActivity.repository_id == self.id]

=== modified file 'lib/lp/code/model/gitrule.py'
--- lib/lp/code/model/gitrule.py	2018-10-16 15:29:37 +0000
+++ lib/lp/code/model/gitrule.py	2018-10-18 13:49:45 +0000
@@ -54,6 +54,7 @@
     validate_person,
     validate_public_person,
     )
+from lp.registry.model.teammembership import TeamParticipation
 from lp.services.database.constants import (
     DEFAULT,
     UTC_NOW,
@@ -127,6 +128,20 @@
         return Store.of(self).find(
             GitRuleGrant, GitRuleGrant.rule_id == self.id)
 
+    def findRuleGrantsByGrantee(self, grantee):
+        """See `IGitRule`."""
+        clauses = [
+            GitRuleGrant.grantee_type == GitGranteeType.PERSON,
+            TeamParticipation.person == grantee,
+            GitRuleGrant.grantee == TeamParticipation.teamID
+            ]
+        return self.grants.find(*clauses).config(distinct=True)
+
+    def findRuleGrantsForRepositoryOwner(self):
+        """See `IGitRule`."""
+        return self.grants.find(
+            GitRuleGrant.grantee_type == GitGranteeType.REPOSITORY_OWNER)
+
     def addGrant(self, grantee, grantor, can_create=False, can_push=False,
                  can_force_push=False):
         """See `IGitRule`."""

=== 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
@@ -8,6 +8,7 @@
     'GitAPI',
     ]
 
+import re
 import sys
 
 from pymacaroons import Macaroon
@@ -24,7 +25,10 @@
 
 from lp.app.errors import NameLookupFailed
 from lp.app.validators import LaunchpadValidationError
-from lp.code.enums import GitRepositoryType
+from lp.code.enums import (
+    GitGranteeType,
+    GitRepositoryType,
+    )
 from lp.code.errors import (
     GitRepositoryCreationException,
     GitRepositoryCreationFault,
@@ -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.
+        """
+        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"""
+
+        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 = []
+            union_permissions = set()
+            for rule in matching_rules:
+                grants = rule.findRuleGrantsByGrantee(requester)
+                if is_owner:
+                    owner_grants = rule.findRuleGrantsForRepositoryOwner()
+                    grants = grants.union(owner_grants)
+
+                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)
+                    )
+
+                owner_grantees = any(x[1] == GitGranteeType.REPOSITORY_OWNER
+                                     for x in seen_grantees)
+                if is_owner and not owner_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):
         user_a = self.factory.makePerson()
         user_b = self.factory.makePerson()
         user_c = self.factory.makePerson()
@@ -686,6 +686,10 @@
             can_force_push=True)
 
         rule = self.factory.makeGitRule(
+            repository, ref_pattern=u'refs/heads/stable/protected')
+        self.factory.makeGitRuleGrant(rule=rule, grantee=stable_team)
+
+        rule = self.factory.makeGitRule(
             repository, ref_pattern=u'refs/heads/archived/*')
         self.factory.makeGitRuleGrant(
             rule=rule, grantee=GitGranteeType.REPOSITORY_OWNER)
@@ -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):
+        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 = ['refs/heads/stable/next']
+        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([]),
+        }))
 
 
 class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory):


Follow ups