Merge lp:~cjwatson/launchpad/git-checkRefPermissions-macaroon into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18810
Proposed branch: lp:~cjwatson/launchpad/git-checkRefPermissions-macaroon
Merge into: lp:launchpad
Diff against target: 208 lines (+129/-12)
2 files modified
lib/lp/code/xmlrpc/git.py (+27/-8)
lib/lp/code/xmlrpc/tests/test_git.py (+102/-4)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-checkRefPermissions-macaroon
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+357732@code.launchpad.net

Commit message

Make GitAPI.checkRefPermissions work for pushes from code import workers.

Description of the change

See e.g. https://oops.canonical.com/oops/?oopsid=OOPS-030d238eea8c79e6d52e0205deafd603.

I've tried to follow the structure of translatePath and similar here.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

<wgrant> I need to go now but the diff looks good
<wgrant> Feel free to self approve
<wgrant> Checked in loggerhead

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/xmlrpc/git.py'
2--- lib/lp/code/xmlrpc/git.py 2018-10-23 16:36:25 +0000
3+++ lib/lp/code/xmlrpc/git.py 2018-10-24 01:17:40 +0000
4@@ -366,18 +366,35 @@
5 matching_rules.append(rule)
6 return matching_rules
7
8- def _checkRefPermissions(self, requester, translated_path, ref_paths):
9+ def _checkRefPermissions(self, requester, translated_path, ref_paths,
10+ auth_params):
11+ if requester == LAUNCHPAD_ANONYMOUS:
12+ requester = None
13 repository = removeSecurityProxy(
14 getUtility(IGitLookup).getByHostingPath(translated_path))
15- is_owner = requester.inTeam(repository.owner)
16 result = {}
17
18 grants_for_user = defaultdict(list)
19- grants = repository.findRuleGrantsByGrantee(requester)
20- if is_owner:
21- owner_grants = repository.findRuleGrantsByGrantee(
22+ macaroon_raw = auth_params.get("macaroon")
23+ if (macaroon_raw is not None and
24+ self._verifyMacaroon(macaroon_raw, repository)):
25+ # The authentication parameters grant access as an anonymous
26+ # repository owner.
27+ # For the time being, this only works for code imports.
28+ assert repository.repository_type == GitRepositoryType.IMPORTED
29+ is_owner = True
30+ grants = repository.findRuleGrantsByGrantee(
31 GitGranteeType.REPOSITORY_OWNER)
32- grants = grants.union(owner_grants)
33+ elif requester is None:
34+ is_owner = False
35+ grants = []
36+ else:
37+ is_owner = requester.inTeam(repository.owner)
38+ grants = repository.findRuleGrantsByGrantee(requester)
39+ if is_owner:
40+ owner_grants = repository.findRuleGrantsByGrantee(
41+ GitGranteeType.REPOSITORY_OWNER)
42+ grants = grants.union(owner_grants)
43 for grant in grants:
44 grants_for_user[grant.rule].append(grant)
45
46@@ -407,9 +424,11 @@
47 def checkRefPermissions(self, translated_path, ref_paths, auth_params):
48 """ See `IGitAPI`"""
49 requester_id = auth_params.get("uid")
50+ if requester_id is None:
51+ requester_id = LAUNCHPAD_ANONYMOUS
52 return run_with_login(
53 requester_id,
54 self._checkRefPermissions,
55 translated_path,
56- ref_paths
57- )
58+ ref_paths,
59+ auth_params)
60
61=== modified file 'lib/lp/code/xmlrpc/tests/test_git.py'
62--- lib/lp/code/xmlrpc/tests/test_git.py 2018-10-23 16:36:45 +0000
63+++ lib/lp/code/xmlrpc/tests/test_git.py 2018-10-24 01:17:40 +0000
64@@ -1,4 +1,4 @@
65-# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
66+# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
67 # GNU Affero General Public License version 3 (see the file LICENSE).
68
69 """Tests for the internal Git API."""
70@@ -8,7 +8,6 @@
71 from pymacaroons import Macaroon
72 from testtools.matchers import (
73 Equals,
74- MatchesListwise,
75 MatchesDict,
76 )
77 from zope.component import getUtility
78@@ -187,6 +186,20 @@
79 {"clone_from": cloned_from.getInternalPath()},
80 self.hosting_fixture.create.extract_kwargs()[0])
81
82+ def assertHasRefPermissions(self, requester, repository, ref_paths,
83+ permissions, macaroon_raw=None):
84+ if requester is not None:
85+ requester = requester.id
86+ auth_params = {"uid": requester}
87+ if macaroon_raw is not None:
88+ auth_params["macaroon"] = macaroon_raw
89+ translated_path = removeSecurityProxy(repository).getInternalPath()
90+ results = self.git_api.checkRefPermissions(
91+ translated_path, ref_paths, auth_params)
92+ self.assertThat(results, MatchesDict({
93+ ref_path: Equals(ref_permissions)
94+ for ref_path, ref_permissions in permissions.items()}))
95+
96 def test_translatePath_private_repository(self):
97 requester = self.factory.makePerson()
98 repository = removeSecurityProxy(
99@@ -893,7 +906,8 @@
100 removeSecurityProxy(issuer).issueMacaroon(job) for job in jobs]
101 path = u"/%s" % code_imports[0].git_repository.unique_name
102 self.assertPermissionDenied(
103- None, path, permission="write", macaroon_raw=macaroons[0])
104+ None, path, permission="write",
105+ macaroon_raw=macaroons[0].serialize())
106 with celebrity_logged_in("vcs_imports"):
107 getUtility(ICodeImportJobWorkflow).startJob(jobs[0], machine)
108 self.assertTranslates(
109@@ -931,7 +945,8 @@
110 removeSecurityProxy(issuer).issueMacaroon(job) for job in jobs]
111 path = u"/%s" % code_imports[0].git_repository.unique_name
112 self.assertPermissionDenied(
113- None, path, permission="write", macaroon_raw=macaroons[0])
114+ None, path, permission="write",
115+ macaroon_raw=macaroons[0].serialize())
116 with celebrity_logged_in("vcs_imports"):
117 getUtility(ICodeImportJobWorkflow).startJob(jobs[0], machine)
118 self.assertTranslates(
119@@ -1003,6 +1018,89 @@
120 self.git_api.authenticateWithPassword("", "nonsense"),
121 faults.Unauthorized)
122
123+ def test_checkRefPermissions_code_import(self):
124+ # A code import worker with a suitable macaroon has repository owner
125+ # privileges on a repository associated with a running code import
126+ # job.
127+ self.pushConfig("codeimport", macaroon_secret_key="some-secret")
128+ machine = self.factory.makeCodeImportMachine(set_online=True)
129+ code_imports = [
130+ self.factory.makeCodeImport(
131+ target_rcs_type=TargetRevisionControlSystems.GIT)
132+ for _ in range(2)]
133+ with celebrity_logged_in("vcs_imports"):
134+ jobs = [
135+ self.factory.makeCodeImportJob(code_import=code_import)
136+ for code_import in code_imports]
137+ issuer = getUtility(IMacaroonIssuer, "code-import-job")
138+ macaroons = [
139+ removeSecurityProxy(issuer).issueMacaroon(job) for job in jobs]
140+ repository = code_imports[0].git_repository
141+ ref_path = "refs/heads/master"
142+ self.assertHasRefPermissions(
143+ None, repository, [ref_path], {ref_path: []},
144+ macaroon_raw=macaroons[0].serialize())
145+ with celebrity_logged_in("vcs_imports"):
146+ getUtility(ICodeImportJobWorkflow).startJob(jobs[0], machine)
147+ self.assertHasRefPermissions(
148+ None, repository, [ref_path],
149+ {ref_path: ["create", "push", "force_push"]},
150+ macaroon_raw=macaroons[0].serialize())
151+ self.assertHasRefPermissions(
152+ None, repository, [ref_path], {ref_path: []},
153+ macaroon_raw=macaroons[1].serialize())
154+ self.assertHasRefPermissions(
155+ None, repository, [ref_path], {ref_path: []},
156+ macaroon_raw=Macaroon(
157+ location=config.vhost.mainsite.hostname, identifier="another",
158+ key="another-secret").serialize())
159+ self.assertHasRefPermissions(
160+ None, repository, [ref_path], {ref_path: []},
161+ macaroon_raw="nonsense")
162+
163+ def test_checkRefPermissions_private_code_import(self):
164+ # A code import worker with a suitable macaroon has repository owner
165+ # privileges on a repository associated with a running private code
166+ # import job.
167+ self.pushConfig("codeimport", macaroon_secret_key="some-secret")
168+ machine = self.factory.makeCodeImportMachine(set_online=True)
169+ code_imports = [
170+ self.factory.makeCodeImport(
171+ target_rcs_type=TargetRevisionControlSystems.GIT)
172+ for _ in range(2)]
173+ private_repository = code_imports[0].git_repository
174+ removeSecurityProxy(private_repository).transitionToInformationType(
175+ InformationType.PRIVATESECURITY, private_repository.owner)
176+ with celebrity_logged_in("vcs_imports"):
177+ jobs = [
178+ self.factory.makeCodeImportJob(code_import=code_import)
179+ for code_import in code_imports]
180+ issuer = getUtility(IMacaroonIssuer, "code-import-job")
181+ macaroons = [
182+ removeSecurityProxy(issuer).issueMacaroon(job) for job in jobs]
183+ repository = code_imports[0].git_repository
184+ ref_path = "refs/heads/master"
185+ self.assertHasRefPermissions(
186+ None, repository, [ref_path], {ref_path: []},
187+ macaroon_raw=macaroons[0])
188+ with celebrity_logged_in("vcs_imports"):
189+ getUtility(ICodeImportJobWorkflow).startJob(jobs[0], machine)
190+ self.assertHasRefPermissions(
191+ None, repository, [ref_path],
192+ {ref_path: ["create", "push", "force_push"]},
193+ macaroon_raw=macaroons[0].serialize())
194+ self.assertHasRefPermissions(
195+ None, repository, [ref_path], {ref_path: []},
196+ macaroon_raw=macaroons[1].serialize())
197+ self.assertHasRefPermissions(
198+ None, repository, [ref_path], {ref_path: []},
199+ macaroon_raw=Macaroon(
200+ location=config.vhost.mainsite.hostname, identifier="another",
201+ key="another-secret").serialize())
202+ self.assertHasRefPermissions(
203+ None, repository, [ref_path], {ref_path: []},
204+ macaroon_raw="nonsense")
205+
206
207 class TestGitAPISecurity(TestGitAPIMixin, TestCaseWithFactory):
208 """Slow tests for `IGitAPI`.