Merge lp:~cjwatson/launchpad/git-permissions-non-unicode-refs into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18830
Proposed branch: lp:~cjwatson/launchpad/git-permissions-non-unicode-refs
Merge into: lp:launchpad
Diff against target: 180 lines (+100/-12)
4 files modified
lib/lp/code/interfaces/gitrepository.py (+2/-1)
lib/lp/code/model/gitrepository.py (+5/-1)
lib/lp/code/xmlrpc/git.py (+24/-5)
lib/lp/code/xmlrpc/tests/test_git.py (+69/-5)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-permissions-non-unicode-refs
Reviewer Review Type Date Requested Status
Tom Wardill (community) Approve
Launchpad code reviewers Pending
Review via email: mp+359088@code.launchpad.net

Commit message

Begin converting GitAPI.checkRefPermissions to accept ref paths as bytes.

Description of the change

Git ref paths may not be valid UTF-8, so we need to treat them as bytes. We can't deal perfectly with non-UTF-8 refs - they won't get scanned and so won't show up in the webservice API or the web UI - but we can at least allow them to round-trip through Launchpad at the git level. I believe we tested that a while back and it was fine, but then we forgot about it during the permissions work so it regressed.

I have most of a turnip branch to deal with the other end of this, but the webapp needs to support it first.

To post a comment you must log in.
Revision history for this message
Tom Wardill (twom) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/interfaces/gitrepository.py'
2--- lib/lp/code/interfaces/gitrepository.py 2018-11-09 22:06:43 +0000
3+++ lib/lp/code/interfaces/gitrepository.py 2018-11-21 01:09:21 +0000
4@@ -824,7 +824,8 @@
5 :param person: An `IPerson` to check, or
6 `GitGranteeType.REPOSITORY_OWNER` to check an anonymous
7 repository owner.
8- :param ref_paths: An iterable of reference paths.
9+ :param ref_paths: An iterable of reference paths (each of which may
10+ be either bytes or text).
11 :return: A dict mapping reference paths to sets of
12 `GitPermissionType`, corresponding to the requested person's
13 effective permissions on each of the requested references.
14
15=== modified file 'lib/lp/code/model/gitrepository.py'
16--- lib/lp/code/model/gitrepository.py 2018-11-09 22:06:43 +0000
17+++ lib/lp/code/model/gitrepository.py 2018-11-21 01:09:21 +0000
18@@ -1311,8 +1311,12 @@
19 grants_for_user[grant.rule].append(grant)
20
21 for ref_path in ref_paths:
22+ encoded_ref_path = (
23+ ref_path if isinstance(ref_path, bytes)
24+ else ref_path.encode("UTF-8"))
25 matching_rules = [
26- rule for rule in rules if fnmatch(ref_path, rule.ref_pattern)]
27+ rule for rule in rules if
28+ fnmatch(encoded_ref_path, rule.ref_pattern.encode("UTF-8"))]
29 if is_owner and not matching_rules:
30 # If there are no matching rules, then the repository owner
31 # can do anything.
32
33=== modified file 'lib/lp/code/xmlrpc/git.py'
34--- lib/lp/code/xmlrpc/git.py 2018-10-25 17:52:43 +0000
35+++ lib/lp/code/xmlrpc/git.py 2018-11-21 01:09:21 +0000
36@@ -11,6 +11,7 @@
37 import sys
38
39 from pymacaroons import Macaroon
40+from six.moves import xmlrpc_client
41 from storm.store import Store
42 import transaction
43 from zope.component import (
44@@ -364,11 +365,29 @@
45 assert repository.repository_type == GitRepositoryType.IMPORTED
46 requester = GitGranteeType.REPOSITORY_OWNER
47
48- return {
49- ref_path: self._renderPermissions(permissions)
50- for ref_path, permissions in repository.checkRefPermissions(
51- requester, ref_paths).items()
52- }
53+ if all(isinstance(ref_path, xmlrpc_client.Binary)
54+ for ref_path in ref_paths):
55+ # New protocol: caller sends paths as bytes; Launchpad returns a
56+ # list of (path, permissions) tuples. (XML-RPC doesn't support
57+ # dict keys being bytes.)
58+ ref_paths = [ref_path.data for ref_path in ref_paths]
59+ return [
60+ (xmlrpc_client.Binary(ref_path),
61+ self._renderPermissions(permissions))
62+ for ref_path, permissions in repository.checkRefPermissions(
63+ requester, ref_paths).items()
64+ ]
65+ else:
66+ # Old protocol: caller sends paths as text; Launchpad returns a
67+ # dict of {path: permissions}.
68+ # XXX cjwatson 2018-11-21: Remove this once turnip has migrated
69+ # to the new protocol. git ref paths are not required to be
70+ # valid UTF-8.
71+ return {
72+ ref_path: self._renderPermissions(permissions)
73+ for ref_path, permissions in repository.checkRefPermissions(
74+ requester, ref_paths).items()
75+ }
76
77 def checkRefPermissions(self, translated_path, ref_paths, auth_params):
78 """ See `IGitAPI`"""
79
80=== modified file 'lib/lp/code/xmlrpc/tests/test_git.py'
81--- lib/lp/code/xmlrpc/tests/test_git.py 2018-10-24 00:42:39 +0000
82+++ lib/lp/code/xmlrpc/tests/test_git.py 2018-11-21 01:09:21 +0000
83@@ -6,9 +6,15 @@
84 __metaclass__ = type
85
86 from pymacaroons import Macaroon
87+from six.moves import xmlrpc_client
88 from testtools.matchers import (
89 Equals,
90+ IsInstance,
91+ MatchesAll,
92 MatchesDict,
93+ MatchesListwise,
94+ MatchesSetwise,
95+ MatchesStructure,
96 )
97 from zope.component import getUtility
98 from zope.security.proxy import removeSecurityProxy
99@@ -194,11 +200,25 @@
100 if macaroon_raw is not None:
101 auth_params["macaroon"] = macaroon_raw
102 translated_path = removeSecurityProxy(repository).getInternalPath()
103- results = self.git_api.checkRefPermissions(
104- translated_path, ref_paths, auth_params)
105- self.assertThat(results, MatchesDict({
106- ref_path: Equals(ref_permissions)
107- for ref_path, ref_permissions in permissions.items()}))
108+ if all(isinstance(ref_path, bytes) for ref_path in ref_paths):
109+ ref_paths = [
110+ xmlrpc_client.Binary(ref_path) for ref_path in ref_paths]
111+ results = self.git_api.checkRefPermissions(
112+ translated_path, ref_paths, auth_params)
113+ self.assertThat(results, MatchesSetwise(*(
114+ MatchesListwise([
115+ MatchesAll(
116+ IsInstance(xmlrpc_client.Binary),
117+ MatchesStructure.byEquality(data=ref_path)),
118+ Equals(ref_permissions),
119+ ])
120+ for ref_path, ref_permissions in permissions.items())))
121+ else:
122+ results = self.git_api.checkRefPermissions(
123+ translated_path, ref_paths, auth_params)
124+ self.assertThat(results, MatchesDict({
125+ ref_path: Equals(ref_permissions)
126+ for ref_path, ref_permissions in permissions.items()}))
127
128 def test_translatePath_private_repository(self):
129 requester = self.factory.makePerson()
130@@ -519,6 +539,50 @@
131 'refs/other': Equals([]),
132 }))
133
134+ def test_checkRefPermissions_bytes(self):
135+ owner = self.factory.makePerson()
136+ grantee = self.factory.makePerson()
137+ no_privileges = self.factory.makePerson()
138+ repository = removeSecurityProxy(
139+ self.factory.makeGitRepository(owner=owner))
140+ self.factory.makeGitRuleGrant(
141+ repository=repository, ref_pattern=u"refs/heads/next/*",
142+ grantee=grantee, can_push=True)
143+ paths = [
144+ # Properly-encoded UTF-8.
145+ u"refs/heads/next/\N{BLACK HEART SUIT}".encode("UTF-8"),
146+ # Non-UTF-8. (git does not require any particular encoding for
147+ # ref paths; non-UTF-8 ones won't work well everywhere, but it's
148+ # at least possible to round-trip them through Launchpad.)
149+ b"refs/heads/next/\x80",
150+ ]
151+
152+ self.assertHasRefPermissions(
153+ grantee, repository, paths, {path: ["push"] for path in paths})
154+ login(ANONYMOUS)
155+ self.assertHasRefPermissions(
156+ no_privileges, repository, paths, {path: [] for path in paths})
157+
158+ def test_checkRefPermissions_unicode(self):
159+ # Actual Unicode ref paths work too.
160+ # XXX cjwatson 2018-11-21: Remove this when the transition to the
161+ # new protocol is complete.
162+ owner = self.factory.makePerson()
163+ grantee = self.factory.makePerson()
164+ no_privileges = self.factory.makePerson()
165+ repository = removeSecurityProxy(
166+ self.factory.makeGitRepository(owner=owner))
167+ self.factory.makeGitRuleGrant(
168+ repository=repository, ref_pattern=u"refs/heads/next/*",
169+ grantee=grantee, can_push=True)
170+ path = u"refs/heads/next/\N{SNOWMAN}"
171+
172+ self.assertHasRefPermissions(
173+ grantee, repository, [path], {path: ["push"]})
174+ login(ANONYMOUS)
175+ self.assertHasRefPermissions(
176+ no_privileges, repository, [path], {path: []})
177+
178
179 class TestGitAPI(TestGitAPIMixin, TestCaseWithFactory):
180 """Tests for the implementation of `IGitAPI`."""