Merge ~cjwatson/launchpad:snap-validate-macaroons-harder into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: da8d0afc19d32d310e83bdd94c94602febc8ee45
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:snap-validate-macaroons-harder
Merge into: launchpad:master
Diff against target: 331 lines (+96/-31)
4 files modified
lib/lp/snappy/browser/tests/test_snap.py (+8/-5)
lib/lp/snappy/interfaces/snap.py (+11/-5)
lib/lp/snappy/model/snap.py (+13/-3)
lib/lp/snappy/tests/test_snap.py (+64/-18)
Reviewer Review Type Date Requested Status
Thiago F. Pappacena (community) Approve
Review via email: mp+389774@code.launchpad.net

Commit message

Validate macaroons in Snap.completeAuthorization

Description of the change

This isn't a full verification (which Launchpad can't do anyway), just a check that the macaroons are well-formed.

To post a comment you must log in.
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/snappy/browser/tests/test_snap.py b/lib/lp/snappy/browser/tests/test_snap.py
2index 0a481dc..37b3cda 100644
3--- a/lib/lp/snappy/browser/tests/test_snap.py
4+++ b/lib/lp/snappy/browser/tests/test_snap.py
5@@ -1,4 +1,4 @@
6-# Copyright 2015-2019 Canonical Ltd. This software is licensed under the
7+# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
8 # GNU Affero General Public License version 3 (see the file LICENSE).
9
10 """Test snap package views."""
11@@ -1187,7 +1187,7 @@ class TestSnapAuthorizeView(BaseTestSnapView):
12 # If the form does not include a discharge macaroon, the "complete"
13 # action fails.
14 with person_logged_in(self.snap.owner):
15- self.snap.store_secrets = {"root": "root"}
16+ self.snap.store_secrets = {"root": Macaroon().serialize()}
17 transaction.commit()
18 form = {"field.actions.complete": "1"}
19 view = create_initialized_view(
20@@ -1203,12 +1203,14 @@ class TestSnapAuthorizeView(BaseTestSnapView):
21 def test_complete_authorization(self):
22 # If the form includes a discharge macaroon, the "complete" action
23 # succeeds and records the new secrets.
24+ root_macaroon = Macaroon()
25+ discharge_macaroon = Macaroon()
26 with person_logged_in(self.snap.owner):
27- self.snap.store_secrets = {"root": "root"}
28+ self.snap.store_secrets = {"root": root_macaroon.serialize()}
29 transaction.commit()
30 form = {
31 "field.actions.complete": "1",
32- "field.discharge_macaroon": "discharge",
33+ "field.discharge_macaroon": discharge_macaroon.serialize(),
34 }
35 view = create_initialized_view(
36 self.snap, "+authorize", form=form, method="POST",
37@@ -1223,7 +1225,8 @@ class TestSnapAuthorizeView(BaseTestSnapView):
38 self.snap.name,
39 view.request.response.notifications[0].message)
40 self.assertEqual(
41- {"root": "root", "discharge": "discharge"},
42+ {"root": root_macaroon.serialize(),
43+ "discharge": discharge_macaroon.serialize()},
44 self.snap.store_secrets)
45
46
47diff --git a/lib/lp/snappy/interfaces/snap.py b/lib/lp/snappy/interfaces/snap.py
48index a6460b7..eca689b 100644
49--- a/lib/lp/snappy/interfaces/snap.py
50+++ b/lib/lp/snappy/interfaces/snap.py
51@@ -1,4 +1,4 @@
52-# Copyright 2015-2019 Canonical Ltd. This software is licensed under the
53+# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
54 # GNU Affero General Public License version 3 (see the file LICENSE).
55
56 """Snap package interfaces."""
57@@ -8,6 +8,7 @@ from __future__ import absolute_import, print_function, unicode_literals
58 __metaclass__ = type
59
60 __all__ = [
61+ 'BadMacaroon',
62 'BadSnapSearchContext',
63 'BadSnapSource',
64 'CannotAuthorizeStoreUploads',
65@@ -28,7 +29,7 @@ __all__ = [
66 'SNAP_SNAPCRAFT_CHANNEL_FEATURE_FLAG',
67 'SNAP_TESTING_FLAGS',
68 'SNAP_WEBHOOKS_FEATURE_FLAG',
69- 'SnapAuthorizationBadMacaroon',
70+ 'SnapAuthorizationBadGeneratedMacaroon',
71 'SnapBuildAlreadyPending',
72 'SnapBuildArchiveOwnerMismatch',
73 'SnapBuildDisallowedArchitecture',
74@@ -237,11 +238,16 @@ class CannotAuthorizeStoreUploads(Exception):
75
76
77 @error_status(http_client.INTERNAL_SERVER_ERROR)
78-class SnapAuthorizationBadMacaroon(Exception):
79+class SnapAuthorizationBadGeneratedMacaroon(Exception):
80 """The macaroon generated to authorize store uploads is unusable."""
81
82
83 @error_status(http_client.BAD_REQUEST)
84+class BadMacaroon(Exception):
85+ """A macaroon supplied by the user is invalid."""
86+
87+
88+@error_status(http_client.BAD_REQUEST)
89 class CannotRequestAutoBuilds(Exception):
90 """Snap package is not configured for automatic builds."""
91
92@@ -597,8 +603,8 @@ class ISnapEdit(IWebhookTarget):
93 :raises BadRequestPackageUploadResponse: if the store returns an
94 error or a response without a macaroon when asked to issue a
95 package_upload macaroon.
96- :raises SnapAuthorizationBadMacaroon: if the package_upload macaroon
97- returned by the store has unsuitable SSO caveats.
98+ :raises SnapAuthorizationBadGeneratedMacaroon: if the package_upload
99+ macaroon returned by the store has unsuitable SSO caveats.
100 :return: The SSO caveat ID from the package_upload macaroon returned
101 by the store. The third-party site should acquire a discharge
102 macaroon for this caveat using OpenID and then call
103diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py
104index 4860fad..aa147ef 100644
105--- a/lib/lp/snappy/model/snap.py
106+++ b/lib/lp/snappy/model/snap.py
107@@ -139,6 +139,7 @@ from lp.services.webhooks.interfaces import IWebhookSet
108 from lp.services.webhooks.model import WebhookTargetMixin
109 from lp.snappy.adapters.buildarch import determine_architectures_to_build
110 from lp.snappy.interfaces.snap import (
111+ BadMacaroon,
112 BadSnapSearchContext,
113 BadSnapSource,
114 CannotAuthorizeStoreUploads,
115@@ -154,7 +155,7 @@ from lp.snappy.interfaces.snap import (
116 NoSourceForSnap,
117 NoSuchSnap,
118 SNAP_PRIVATE_FEATURE_FLAG,
119- SnapAuthorizationBadMacaroon,
120+ SnapAuthorizationBadGeneratedMacaroon,
121 SnapBuildAlreadyPending,
122 SnapBuildArchiveOwnerMismatch,
123 SnapBuildDisallowedArchitecture,
124@@ -588,9 +589,10 @@ class Snap(Storm, WebhookTargetMixin):
125 # in some other service, since the user can't do anything about it
126 # and it should show up in our OOPS reports.
127 if not sso_caveats:
128- raise SnapAuthorizationBadMacaroon("Macaroon has no SSO caveats")
129+ raise SnapAuthorizationBadGeneratedMacaroon(
130+ "Macaroon has no SSO caveats")
131 elif len(sso_caveats) > 1:
132- raise SnapAuthorizationBadMacaroon(
133+ raise SnapAuthorizationBadGeneratedMacaroon(
134 "Macaroon has multiple SSO caveats")
135 self.store_secrets = {'root': root_macaroon_raw}
136 return sso_caveats[0].caveat_id
137@@ -599,6 +601,10 @@ class Snap(Storm, WebhookTargetMixin):
138 discharge_macaroon=None):
139 """See `ISnap`."""
140 if root_macaroon is not None:
141+ try:
142+ Macaroon.deserialize(root_macaroon)
143+ except Exception:
144+ raise BadMacaroon("root_macaroon is invalid.")
145 self.store_secrets = {"root": root_macaroon}
146 else:
147 if self.store_secrets is None or "root" not in self.store_secrets:
148@@ -606,6 +612,10 @@ class Snap(Storm, WebhookTargetMixin):
149 "beginAuthorization must be called before "
150 "completeAuthorization.")
151 if discharge_macaroon is not None:
152+ try:
153+ Macaroon.deserialize(discharge_macaroon)
154+ except Exception:
155+ raise BadMacaroon("discharge_macaroon is invalid.")
156 container = getUtility(IEncryptedContainer, "snap-store-secrets")
157 if container.can_encrypt:
158 self.store_secrets["discharge_encrypted"] = (
159diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py
160index 0b712fe..8c87c8a 100644
161--- a/lib/lp/snappy/tests/test_snap.py
162+++ b/lib/lp/snappy/tests/test_snap.py
163@@ -3287,20 +3287,23 @@ class TestSnapWebservice(TestCaseWithFactory):
164 def test_completeAuthorization(self):
165 with admin_logged_in():
166 snappy_series = self.factory.makeSnappySeries()
167+ root_macaroon = Macaroon()
168+ discharge_macaroon = Macaroon()
169 snap = self.factory.makeSnap(
170 registrant=self.person, store_upload=True,
171 store_series=snappy_series,
172 store_name=self.factory.getUniqueUnicode(),
173- store_secrets={"root": "dummy-root"})
174+ store_secrets={"root": root_macaroon.serialize()})
175 snap_url = api_url(snap)
176 logout()
177 response = self.webservice.named_post(
178 snap_url, "completeAuthorization",
179- discharge_macaroon="dummy-discharge")
180+ discharge_macaroon=discharge_macaroon.serialize())
181 self.assertEqual(200, response.status)
182 with person_logged_in(self.person):
183 self.assertEqual(
184- {"root": "dummy-root", "discharge": "dummy-discharge"},
185+ {"root": root_macaroon.serialize(),
186+ "discharge": discharge_macaroon.serialize()},
187 snap.store_secrets)
188
189 def test_completeAuthorization_without_beginAuthorization(self):
190@@ -3312,22 +3315,26 @@ class TestSnapWebservice(TestCaseWithFactory):
191 store_name=self.factory.getUniqueUnicode())
192 snap_url = api_url(snap)
193 logout()
194+ discharge_macaroon = Macaroon()
195 response = self.webservice.named_post(
196 snap_url, "completeAuthorization",
197- discharge_macaroon="dummy-discharge")
198- self.assertEqual(400, response.status)
199- self.assertEqual(
200- "beginAuthorization must be called before completeAuthorization.",
201- response.body)
202+ discharge_macaroon=discharge_macaroon.serialize())
203+ self.assertThat(response, MatchesStructure.byEquality(
204+ status=400,
205+ body=(
206+ b"beginAuthorization must be called before "
207+ b"completeAuthorization.")))
208
209 def test_completeAuthorization_unauthorized(self):
210 with admin_logged_in():
211 snappy_series = self.factory.makeSnappySeries()
212+ root_macaroon = Macaroon()
213+ discharge_macaroon = Macaroon()
214 snap = self.factory.makeSnap(
215 registrant=self.person, store_upload=True,
216 store_series=snappy_series,
217 store_name=self.factory.getUniqueUnicode(),
218- store_secrets={"root": "dummy-root"})
219+ store_secrets={"root": root_macaroon.serialize()})
220 snap_url = api_url(snap)
221 other_person = self.factory.makePerson()
222 other_webservice = webservice_for_person(
223@@ -3335,7 +3342,7 @@ class TestSnapWebservice(TestCaseWithFactory):
224 other_webservice.default_api_version = "devel"
225 response = other_webservice.named_post(
226 snap_url, "completeAuthorization",
227- discharge_macaroon="dummy-discharge")
228+ discharge_macaroon=discharge_macaroon.serialize())
229 self.assertEqual(401, response.status)
230
231 def test_completeAuthorization_both_macaroons(self):
232@@ -3349,13 +3356,17 @@ class TestSnapWebservice(TestCaseWithFactory):
233 store_name=self.factory.getUniqueUnicode())
234 snap_url = api_url(snap)
235 logout()
236+ root_macaroon = Macaroon()
237+ discharge_macaroon = Macaroon()
238 response = self.webservice.named_post(
239 snap_url, "completeAuthorization",
240- root_macaroon="dummy-root", discharge_macaroon="dummy-discharge")
241+ root_macaroon=root_macaroon.serialize(),
242+ discharge_macaroon=discharge_macaroon.serialize())
243 self.assertEqual(200, response.status)
244 with person_logged_in(self.person):
245 self.assertEqual(
246- {"root": "dummy-root", "discharge": "dummy-discharge"},
247+ {"root": root_macaroon.serialize(),
248+ "discharge": discharge_macaroon.serialize()},
249 snap.store_secrets)
250
251 def test_completeAuthorization_only_root_macaroon(self):
252@@ -3370,11 +3381,44 @@ class TestSnapWebservice(TestCaseWithFactory):
253 store_name=self.factory.getUniqueUnicode())
254 snap_url = api_url(snap)
255 logout()
256+ root_macaroon = Macaroon()
257 response = self.webservice.named_post(
258- snap_url, "completeAuthorization", root_macaroon="dummy-root")
259+ snap_url, "completeAuthorization",
260+ root_macaroon=root_macaroon.serialize())
261 self.assertEqual(200, response.status)
262 with person_logged_in(self.person):
263- self.assertEqual({"root": "dummy-root"}, snap.store_secrets)
264+ self.assertEqual(
265+ {"root": root_macaroon.serialize()}, snap.store_secrets)
266+
267+ def test_completeAuthorization_malformed_root_macaroon(self):
268+ with admin_logged_in():
269+ snappy_series = self.factory.makeSnappySeries()
270+ snap = self.factory.makeSnap(
271+ registrant=self.person, store_upload=True,
272+ store_series=snappy_series,
273+ store_name=self.factory.getUniqueUnicode())
274+ snap_url = api_url(snap)
275+ logout()
276+ response = self.webservice.named_post(
277+ snap_url, "completeAuthorization", root_macaroon="nonsense")
278+ self.assertThat(response, MatchesStructure.byEquality(
279+ status=400, body=b"root_macaroon is invalid."))
280+
281+ def test_completeAuthorization_malformed_discharge_macaroon(self):
282+ with admin_logged_in():
283+ snappy_series = self.factory.makeSnappySeries()
284+ snap = self.factory.makeSnap(
285+ registrant=self.person, store_upload=True,
286+ store_series=snappy_series,
287+ store_name=self.factory.getUniqueUnicode())
288+ snap_url = api_url(snap)
289+ logout()
290+ response = self.webservice.named_post(
291+ snap_url, "completeAuthorization",
292+ root_macaroon=Macaroon().serialize(),
293+ discharge_macaroon="nonsense")
294+ self.assertThat(response, MatchesStructure.byEquality(
295+ status=400, body=b"discharge_macaroon is invalid."))
296
297 def test_completeAuthorization_encrypted(self):
298 private_key = PrivateKey.generate()
299@@ -3384,16 +3428,18 @@ class TestSnapWebservice(TestCaseWithFactory):
300 bytes(private_key.public_key)).decode("UTF-8"))
301 with admin_logged_in():
302 snappy_series = self.factory.makeSnappySeries()
303+ root_macaroon = Macaroon()
304+ discharge_macaroon = Macaroon()
305 snap = self.factory.makeSnap(
306 registrant=self.person, store_upload=True,
307 store_series=snappy_series,
308 store_name=self.factory.getUniqueUnicode(),
309- store_secrets={"root": "dummy-root"})
310+ store_secrets={"root": root_macaroon.serialize()})
311 snap_url = api_url(snap)
312 logout()
313 response = self.webservice.named_post(
314 snap_url, "completeAuthorization",
315- discharge_macaroon="dummy-discharge")
316+ discharge_macaroon=discharge_macaroon.serialize())
317 self.assertEqual(200, response.status)
318 self.pushConfig(
319 "snappy",
320@@ -3402,9 +3448,9 @@ class TestSnapWebservice(TestCaseWithFactory):
321 container = getUtility(IEncryptedContainer, "snap-store-secrets")
322 with person_logged_in(self.person):
323 self.assertThat(snap.store_secrets, MatchesDict({
324- "root": Equals("dummy-root"),
325+ "root": Equals(root_macaroon.serialize()),
326 "discharge_encrypted": AfterPreprocessing(
327- container.decrypt, Equals("dummy-discharge")),
328+ container.decrypt, Equals(discharge_macaroon.serialize())),
329 }))
330
331 def makeBuildableDistroArchSeries(self, **kwargs):

Subscribers

People subscribed via source and target branches

to status/vote changes: