Merge lp:~cjwatson/launchpad/restore-git-snap-build-macaroon into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18964
Proposed branch: lp:~cjwatson/launchpad/restore-git-snap-build-macaroon
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/refactor-git-code-import-authz-2
Diff against target: 274 lines (+145/-3)
5 files modified
lib/lp/code/model/tests/test_codeimportjob.py (+2/-0)
lib/lp/code/xmlrpc/git.py (+16/-2)
lib/lp/code/xmlrpc/tests/test_git.py (+89/-1)
lib/lp/snappy/model/snapbuild.py (+5/-0)
lib/lp/snappy/tests/test_snapbuild.py (+33/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/restore-git-snap-build-macaroon
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+367204@code.launchpad.net

Commit message

Make GitAPI accept snap-build macaroons again.

Description of the change

I added support for snap-build macaroons recently, and then promptly forgot about it when refactoring code import authorisation, largely because I hadn't explicitly written any tests for it.

In the process of writing these tests, I noticed that I really should have forced snap-build macaroons to be read-only, so I've done that. The mechanism for doing this is pretty ad-hoc, but we can always improve it later.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/model/tests/test_codeimportjob.py'
2--- lib/lp/code/model/tests/test_codeimportjob.py 2019-04-26 13:13:37 +0000
3+++ lib/lp/code/model/tests/test_codeimportjob.py 2019-05-09 16:04:44 +0000
4@@ -1336,6 +1336,8 @@
5 getUtility(ICodeImportJobWorkflow).startJob(job, machine)
6 macaroon = removeSecurityProxy(issuer).issueMacaroon(job)
7 self.assertMacaroonVerifies(
8+ issuer, macaroon, None, require_context=False)
9+ self.assertMacaroonVerifies(
10 issuer, macaroon, job, require_context=False)
11 self.assertMacaroonVerifies(
12 issuer, macaroon, job.code_import.target, require_context=False)
13
14=== modified file 'lib/lp/code/xmlrpc/git.py'
15--- lib/lp/code/xmlrpc/git.py 2019-05-09 16:04:44 +0000
16+++ lib/lp/code/xmlrpc/git.py 2019-05-09 16:04:44 +0000
17@@ -103,6 +103,17 @@
18
19 :param verified: An `IMacaroonVerificationResult`.
20 """
21+ return verified.issuer_name in ("code-import-job", "snap-build")
22+
23+
24+def _can_internal_issuer_write(verified):
25+ """Does this internal-only issuer have write access?
26+
27+ Some macaroons used by internal services are intended for writing to the
28+ repository; others only allow read access.
29+
30+ :param verified: An `IMacaroonVerificationResult`.
31+ """
32 return verified.issuer_name == "code-import-job"
33
34
35@@ -162,7 +173,7 @@
36 # so we can bypass other checks. This is only permitted for
37 # selected macaroon issuers used by internal services.
38 hosting_path = naked_repository.getInternalPath()
39- writable = True
40+ writable = _can_internal_issuer_write(verified)
41 private = naked_repository.private
42
43 # In any other case, the macaroon constrains the permissions of
44@@ -387,7 +398,7 @@
45 verified = self._verifyMacaroon(password)
46 if verified:
47 auth_params = {"macaroon": password}
48- if verified.issuer_name == "code-import-job":
49+ if _is_issuer_internal(verified):
50 auth_params["user"] = LAUNCHPAD_SERVICES
51 return auth_params
52 # Only macaroons are supported for password authentication.
53@@ -432,6 +443,9 @@
54 raise faults.Unauthorized()
55
56 if internal:
57+ if not _can_internal_issuer_write(verified):
58+ raise faults.Unauthorized()
59+
60 # We know that the authentication parameters
61 # specifically grant access to this repository because
62 # we were able to verify the macaroon using the
63
64=== modified file 'lib/lp/code/xmlrpc/tests/test_git.py'
65--- lib/lp/code/xmlrpc/tests/test_git.py 2019-05-09 16:04:44 +0000
66+++ lib/lp/code/xmlrpc/tests/test_git.py 2019-05-09 16:04:44 +0000
67@@ -20,6 +20,7 @@
68 from zope.security.proxy import removeSecurityProxy
69
70 from lp.app.enums import InformationType
71+from lp.buildmaster.enums import BuildStatus
72 from lp.code.enums import (
73 GitGranteeType,
74 GitRepositoryType,
75@@ -38,8 +39,10 @@
76 from lp.code.xmlrpc.git import GitAPI
77 from lp.registry.enums import TeamMembershipPolicy
78 from lp.services.config import config
79+from lp.services.features.testing import FeatureFixture
80 from lp.services.macaroons.interfaces import IMacaroonIssuer
81 from lp.services.webapp.escaping import html_escape
82+from lp.snappy.interfaces.snap import SNAP_TESTING_FLAGS
83 from lp.testing import (
84 admin_logged_in,
85 ANONYMOUS,
86@@ -1035,6 +1038,49 @@
87 code_imports[0].registrant, path, permission="write",
88 macaroon_raw=macaroons[0].serialize())
89
90+ def test_translatePath_private_snap_build(self):
91+ # A builder with a suitable macaroon can read from a repository
92+ # associated with a running private snap build.
93+ self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
94+ self.pushConfig(
95+ "launchpad", internal_macaroon_secret_key="some-secret")
96+ with person_logged_in(self.factory.makePerson()) as owner:
97+ refs = [
98+ self.factory.makeGitRefs(
99+ owner=owner, information_type=InformationType.USERDATA)[0]
100+ for _ in range(2)]
101+ builds = [
102+ self.factory.makeSnapBuild(
103+ requester=owner, owner=owner, git_ref=ref, private=True)
104+ for ref in refs]
105+ issuer = getUtility(IMacaroonIssuer, "snap-build")
106+ macaroons = [
107+ removeSecurityProxy(issuer).issueMacaroon(build)
108+ for build in builds]
109+ repository = refs[0].repository
110+ path = u"/%s" % repository.unique_name
111+ self.assertUnauthorized(
112+ LAUNCHPAD_SERVICES, path, permission="write",
113+ macaroon_raw=macaroons[0].serialize())
114+ removeSecurityProxy(builds[0]).updateStatus(BuildStatus.BUILDING)
115+ self.assertTranslates(
116+ LAUNCHPAD_SERVICES, path, repository, False, permission="read",
117+ macaroon_raw=macaroons[0].serialize(), private=True)
118+ self.assertUnauthorized(
119+ LAUNCHPAD_SERVICES, path, permission="read",
120+ macaroon_raw=macaroons[1].serialize())
121+ self.assertUnauthorized(
122+ LAUNCHPAD_SERVICES, path, permission="read",
123+ macaroon_raw=Macaroon(
124+ location=config.vhost.mainsite.hostname, identifier="another",
125+ key="another-secret").serialize())
126+ self.assertUnauthorized(
127+ LAUNCHPAD_SERVICES, path, permission="read",
128+ macaroon_raw="nonsense")
129+ self.assertUnauthorized(
130+ repository.registrant, path, permission="read",
131+ macaroon_raw=macaroons[0].serialize())
132+
133 def test_notify(self):
134 # The notify call creates a GitRefScanJob.
135 repository = self.factory.makeGitRepository()
136@@ -1088,6 +1134,29 @@
137 self.git_api.authenticateWithPassword("", "nonsense"),
138 faults.Unauthorized)
139
140+ def test_authenticateWithPassword_private_snap_build(self):
141+ self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
142+ self.pushConfig(
143+ "launchpad", internal_macaroon_secret_key="some-secret")
144+ with person_logged_in(self.factory.makePerson()) as owner:
145+ [ref] = self.factory.makeGitRefs(
146+ owner=owner, information_type=InformationType.USERDATA)
147+ build = self.factory.makeSnapBuild(
148+ requester=owner, owner=owner, git_ref=ref, private=True)
149+ issuer = getUtility(IMacaroonIssuer, "snap-build")
150+ macaroon = removeSecurityProxy(issuer).issueMacaroon(build)
151+ self.assertEqual(
152+ {"macaroon": macaroon.serialize(), "user": "+launchpad-services"},
153+ self.git_api.authenticateWithPassword("", macaroon.serialize()))
154+ other_macaroon = Macaroon(identifier="another", key="another-secret")
155+ self.assertIsInstance(
156+ self.git_api.authenticateWithPassword(
157+ "", other_macaroon.serialize()),
158+ faults.Unauthorized)
159+ self.assertIsInstance(
160+ self.git_api.authenticateWithPassword("", "nonsense"),
161+ faults.Unauthorized)
162+
163 def test_checkRefPermissions_code_import(self):
164 # A code import worker with a suitable macaroon has repository owner
165 # privileges on a repository associated with a running code import
166@@ -1157,7 +1226,7 @@
167 ref_path = "refs/heads/master"
168 self.assertHasRefPermissions(
169 LAUNCHPAD_SERVICES, repository, [ref_path], {ref_path: []},
170- macaroon_raw=macaroons[0])
171+ macaroon_raw=macaroons[0].serialize())
172 with celebrity_logged_in("vcs_imports"):
173 getUtility(ICodeImportJobWorkflow).startJob(jobs[0], machine)
174 self.assertHasRefPermissions(
175@@ -1179,6 +1248,25 @@
176 code_imports[0].registrant, repository, [ref_path], {ref_path: []},
177 macaroon_raw=macaroons[0].serialize())
178
179+ def test_checkRefPermissions_private_snap_build(self):
180+ # A builder with a suitable macaroon cannot write to a repository,
181+ # even if it is associated with a running private snap build.
182+ self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
183+ self.pushConfig(
184+ "launchpad", internal_macaroon_secret_key="some-secret")
185+ with person_logged_in(self.factory.makePerson()) as owner:
186+ [ref] = self.factory.makeGitRefs(
187+ owner=owner, information_type=InformationType.USERDATA)
188+ build = self.factory.makeSnapBuild(
189+ requester=owner, owner=owner, git_ref=ref, private=True)
190+ issuer = getUtility(IMacaroonIssuer, "snap-build")
191+ macaroon = removeSecurityProxy(issuer).issueMacaroon(build)
192+ build.updateStatus(BuildStatus.BUILDING)
193+ path = ref.path.encode("UTF-8")
194+ self.assertHasRefPermissions(
195+ LAUNCHPAD_SERVICES, ref.repository, [path], {path: []},
196+ macaroon_raw=macaroon.serialize())
197+
198
199 class TestGitAPISecurity(TestGitAPIMixin, TestCaseWithFactory):
200 """Slow tests for `IGitAPI`.
201
202=== modified file 'lib/lp/snappy/model/snapbuild.py'
203--- lib/lp/snappy/model/snapbuild.py 2019-05-01 15:47:37 +0000
204+++ lib/lp/snappy/model/snapbuild.py 2019-05-09 16:04:44 +0000
205@@ -628,6 +628,11 @@
206 # Circular import.
207 from lp.snappy.model.snap import Snap
208
209+ if context is None:
210+ # We're only verifying that the macaroon could be valid for some
211+ # context.
212+ return True
213+
214 try:
215 build_id = int(caveat_value)
216 except ValueError:
217
218=== modified file 'lib/lp/snappy/tests/test_snapbuild.py'
219--- lib/lp/snappy/tests/test_snapbuild.py 2019-04-25 21:57:30 +0000
220+++ lib/lp/snappy/tests/test_snapbuild.py 2019-05-09 16:04:44 +0000
221@@ -843,6 +843,33 @@
222 macaroon = issuer.issueMacaroon(build)
223 self.assertMacaroonVerifies(issuer, macaroon, ref.repository)
224
225+ def test_verifyMacaroon_good_no_context(self):
226+ [ref] = self.factory.makeGitRefs(
227+ information_type=InformationType.USERDATA)
228+ build = self.factory.makeSnapBuild(
229+ snap=self.factory.makeSnap(git_ref=ref, private=True))
230+ build.updateStatus(BuildStatus.BUILDING)
231+ issuer = removeSecurityProxy(
232+ getUtility(IMacaroonIssuer, "snap-build"))
233+ macaroon = issuer.issueMacaroon(build)
234+ self.assertMacaroonVerifies(
235+ issuer, macaroon, None, require_context=False)
236+ self.assertMacaroonVerifies(
237+ issuer, macaroon, ref.repository, require_context=False)
238+
239+ def test_verifyMacaroon_no_context_but_require_context(self):
240+ [ref] = self.factory.makeGitRefs(
241+ information_type=InformationType.USERDATA)
242+ build = self.factory.makeSnapBuild(
243+ snap=self.factory.makeSnap(git_ref=ref, private=True))
244+ build.updateStatus(BuildStatus.BUILDING)
245+ issuer = removeSecurityProxy(
246+ getUtility(IMacaroonIssuer, "snap-build"))
247+ macaroon = issuer.issueMacaroon(build)
248+ self.assertMacaroonDoesNotVerify(
249+ ["Expected macaroon verification context but got None."],
250+ issuer, macaroon, None)
251+
252 def test_verifyMacaroon_wrong_location(self):
253 [ref] = self.factory.makeGitRefs(
254 information_type=InformationType.USERDATA)
255@@ -856,6 +883,9 @@
256 self.assertMacaroonDoesNotVerify(
257 ["Macaroon has unknown location 'another-location'."],
258 issuer, macaroon, ref.repository)
259+ self.assertMacaroonDoesNotVerify(
260+ ["Macaroon has unknown location 'another-location'."],
261+ issuer, macaroon, ref.repository, require_context=False)
262
263 def test_verifyMacaroon_wrong_key(self):
264 [ref] = self.factory.makeGitRefs(
265@@ -869,6 +899,9 @@
266 location=config.vhost.mainsite.hostname, key="another-secret")
267 self.assertMacaroonDoesNotVerify(
268 ["Signatures do not match"], issuer, macaroon, ref.repository)
269+ self.assertMacaroonDoesNotVerify(
270+ ["Signatures do not match"],
271+ issuer, macaroon, ref.repository, require_context=False)
272
273 def test_verifyMacaroon_refuses_branch(self):
274 branch = self.factory.makeAnyBranch(