Merge ~cjwatson/launchpad:snap-validate-macaroons-harder into launchpad:master
- Git
- lp:~cjwatson/launchpad
- snap-validate-macaroons-harder
- Merge into 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) |
Related bugs: |
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.completeAu
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.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/snappy/browser/tests/test_snap.py b/lib/lp/snappy/browser/tests/test_snap.py |
2 | index 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 | |
47 | diff --git a/lib/lp/snappy/interfaces/snap.py b/lib/lp/snappy/interfaces/snap.py |
48 | index 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 |
103 | diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py |
104 | index 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"] = ( |
159 | diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py |
160 | index 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): |
LGTM