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
diff --git a/lib/lp/snappy/browser/tests/test_snap.py b/lib/lp/snappy/browser/tests/test_snap.py
index 0a481dc..37b3cda 100644
--- a/lib/lp/snappy/browser/tests/test_snap.py
+++ b/lib/lp/snappy/browser/tests/test_snap.py
@@ -1,4 +1,4 @@
1# Copyright 2015-2019 Canonical Ltd. This software is licensed under the1# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Test snap package views."""4"""Test snap package views."""
@@ -1187,7 +1187,7 @@ class TestSnapAuthorizeView(BaseTestSnapView):
1187 # If the form does not include a discharge macaroon, the "complete"1187 # If the form does not include a discharge macaroon, the "complete"
1188 # action fails.1188 # action fails.
1189 with person_logged_in(self.snap.owner):1189 with person_logged_in(self.snap.owner):
1190 self.snap.store_secrets = {"root": "root"}1190 self.snap.store_secrets = {"root": Macaroon().serialize()}
1191 transaction.commit()1191 transaction.commit()
1192 form = {"field.actions.complete": "1"}1192 form = {"field.actions.complete": "1"}
1193 view = create_initialized_view(1193 view = create_initialized_view(
@@ -1203,12 +1203,14 @@ class TestSnapAuthorizeView(BaseTestSnapView):
1203 def test_complete_authorization(self):1203 def test_complete_authorization(self):
1204 # If the form includes a discharge macaroon, the "complete" action1204 # If the form includes a discharge macaroon, the "complete" action
1205 # succeeds and records the new secrets.1205 # succeeds and records the new secrets.
1206 root_macaroon = Macaroon()
1207 discharge_macaroon = Macaroon()
1206 with person_logged_in(self.snap.owner):1208 with person_logged_in(self.snap.owner):
1207 self.snap.store_secrets = {"root": "root"}1209 self.snap.store_secrets = {"root": root_macaroon.serialize()}
1208 transaction.commit()1210 transaction.commit()
1209 form = {1211 form = {
1210 "field.actions.complete": "1",1212 "field.actions.complete": "1",
1211 "field.discharge_macaroon": "discharge",1213 "field.discharge_macaroon": discharge_macaroon.serialize(),
1212 }1214 }
1213 view = create_initialized_view(1215 view = create_initialized_view(
1214 self.snap, "+authorize", form=form, method="POST",1216 self.snap, "+authorize", form=form, method="POST",
@@ -1223,7 +1225,8 @@ class TestSnapAuthorizeView(BaseTestSnapView):
1223 self.snap.name,1225 self.snap.name,
1224 view.request.response.notifications[0].message)1226 view.request.response.notifications[0].message)
1225 self.assertEqual(1227 self.assertEqual(
1226 {"root": "root", "discharge": "discharge"},1228 {"root": root_macaroon.serialize(),
1229 "discharge": discharge_macaroon.serialize()},
1227 self.snap.store_secrets)1230 self.snap.store_secrets)
12281231
12291232
diff --git a/lib/lp/snappy/interfaces/snap.py b/lib/lp/snappy/interfaces/snap.py
index a6460b7..eca689b 100644
--- a/lib/lp/snappy/interfaces/snap.py
+++ b/lib/lp/snappy/interfaces/snap.py
@@ -1,4 +1,4 @@
1# Copyright 2015-2019 Canonical Ltd. This software is licensed under the1# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Snap package interfaces."""4"""Snap package interfaces."""
@@ -8,6 +8,7 @@ from __future__ import absolute_import, print_function, unicode_literals
8__metaclass__ = type8__metaclass__ = type
99
10__all__ = [10__all__ = [
11 'BadMacaroon',
11 'BadSnapSearchContext',12 'BadSnapSearchContext',
12 'BadSnapSource',13 'BadSnapSource',
13 'CannotAuthorizeStoreUploads',14 'CannotAuthorizeStoreUploads',
@@ -28,7 +29,7 @@ __all__ = [
28 'SNAP_SNAPCRAFT_CHANNEL_FEATURE_FLAG',29 'SNAP_SNAPCRAFT_CHANNEL_FEATURE_FLAG',
29 'SNAP_TESTING_FLAGS',30 'SNAP_TESTING_FLAGS',
30 'SNAP_WEBHOOKS_FEATURE_FLAG',31 'SNAP_WEBHOOKS_FEATURE_FLAG',
31 'SnapAuthorizationBadMacaroon',32 'SnapAuthorizationBadGeneratedMacaroon',
32 'SnapBuildAlreadyPending',33 'SnapBuildAlreadyPending',
33 'SnapBuildArchiveOwnerMismatch',34 'SnapBuildArchiveOwnerMismatch',
34 'SnapBuildDisallowedArchitecture',35 'SnapBuildDisallowedArchitecture',
@@ -237,11 +238,16 @@ class CannotAuthorizeStoreUploads(Exception):
237238
238239
239@error_status(http_client.INTERNAL_SERVER_ERROR)240@error_status(http_client.INTERNAL_SERVER_ERROR)
240class SnapAuthorizationBadMacaroon(Exception):241class SnapAuthorizationBadGeneratedMacaroon(Exception):
241 """The macaroon generated to authorize store uploads is unusable."""242 """The macaroon generated to authorize store uploads is unusable."""
242243
243244
244@error_status(http_client.BAD_REQUEST)245@error_status(http_client.BAD_REQUEST)
246class BadMacaroon(Exception):
247 """A macaroon supplied by the user is invalid."""
248
249
250@error_status(http_client.BAD_REQUEST)
245class CannotRequestAutoBuilds(Exception):251class CannotRequestAutoBuilds(Exception):
246 """Snap package is not configured for automatic builds."""252 """Snap package is not configured for automatic builds."""
247253
@@ -597,8 +603,8 @@ class ISnapEdit(IWebhookTarget):
597 :raises BadRequestPackageUploadResponse: if the store returns an603 :raises BadRequestPackageUploadResponse: if the store returns an
598 error or a response without a macaroon when asked to issue a604 error or a response without a macaroon when asked to issue a
599 package_upload macaroon.605 package_upload macaroon.
600 :raises SnapAuthorizationBadMacaroon: if the package_upload macaroon606 :raises SnapAuthorizationBadGeneratedMacaroon: if the package_upload
601 returned by the store has unsuitable SSO caveats.607 macaroon returned by the store has unsuitable SSO caveats.
602 :return: The SSO caveat ID from the package_upload macaroon returned608 :return: The SSO caveat ID from the package_upload macaroon returned
603 by the store. The third-party site should acquire a discharge609 by the store. The third-party site should acquire a discharge
604 macaroon for this caveat using OpenID and then call610 macaroon for this caveat using OpenID and then call
diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py
index 4860fad..aa147ef 100644
--- a/lib/lp/snappy/model/snap.py
+++ b/lib/lp/snappy/model/snap.py
@@ -139,6 +139,7 @@ from lp.services.webhooks.interfaces import IWebhookSet
139from lp.services.webhooks.model import WebhookTargetMixin139from lp.services.webhooks.model import WebhookTargetMixin
140from lp.snappy.adapters.buildarch import determine_architectures_to_build140from lp.snappy.adapters.buildarch import determine_architectures_to_build
141from lp.snappy.interfaces.snap import (141from lp.snappy.interfaces.snap import (
142 BadMacaroon,
142 BadSnapSearchContext,143 BadSnapSearchContext,
143 BadSnapSource,144 BadSnapSource,
144 CannotAuthorizeStoreUploads,145 CannotAuthorizeStoreUploads,
@@ -154,7 +155,7 @@ from lp.snappy.interfaces.snap import (
154 NoSourceForSnap,155 NoSourceForSnap,
155 NoSuchSnap,156 NoSuchSnap,
156 SNAP_PRIVATE_FEATURE_FLAG,157 SNAP_PRIVATE_FEATURE_FLAG,
157 SnapAuthorizationBadMacaroon,158 SnapAuthorizationBadGeneratedMacaroon,
158 SnapBuildAlreadyPending,159 SnapBuildAlreadyPending,
159 SnapBuildArchiveOwnerMismatch,160 SnapBuildArchiveOwnerMismatch,
160 SnapBuildDisallowedArchitecture,161 SnapBuildDisallowedArchitecture,
@@ -588,9 +589,10 @@ class Snap(Storm, WebhookTargetMixin):
588 # in some other service, since the user can't do anything about it589 # in some other service, since the user can't do anything about it
589 # and it should show up in our OOPS reports.590 # and it should show up in our OOPS reports.
590 if not sso_caveats:591 if not sso_caveats:
591 raise SnapAuthorizationBadMacaroon("Macaroon has no SSO caveats")592 raise SnapAuthorizationBadGeneratedMacaroon(
593 "Macaroon has no SSO caveats")
592 elif len(sso_caveats) > 1:594 elif len(sso_caveats) > 1:
593 raise SnapAuthorizationBadMacaroon(595 raise SnapAuthorizationBadGeneratedMacaroon(
594 "Macaroon has multiple SSO caveats")596 "Macaroon has multiple SSO caveats")
595 self.store_secrets = {'root': root_macaroon_raw}597 self.store_secrets = {'root': root_macaroon_raw}
596 return sso_caveats[0].caveat_id598 return sso_caveats[0].caveat_id
@@ -599,6 +601,10 @@ class Snap(Storm, WebhookTargetMixin):
599 discharge_macaroon=None):601 discharge_macaroon=None):
600 """See `ISnap`."""602 """See `ISnap`."""
601 if root_macaroon is not None:603 if root_macaroon is not None:
604 try:
605 Macaroon.deserialize(root_macaroon)
606 except Exception:
607 raise BadMacaroon("root_macaroon is invalid.")
602 self.store_secrets = {"root": root_macaroon}608 self.store_secrets = {"root": root_macaroon}
603 else:609 else:
604 if self.store_secrets is None or "root" not in self.store_secrets:610 if self.store_secrets is None or "root" not in self.store_secrets:
@@ -606,6 +612,10 @@ class Snap(Storm, WebhookTargetMixin):
606 "beginAuthorization must be called before "612 "beginAuthorization must be called before "
607 "completeAuthorization.")613 "completeAuthorization.")
608 if discharge_macaroon is not None:614 if discharge_macaroon is not None:
615 try:
616 Macaroon.deserialize(discharge_macaroon)
617 except Exception:
618 raise BadMacaroon("discharge_macaroon is invalid.")
609 container = getUtility(IEncryptedContainer, "snap-store-secrets")619 container = getUtility(IEncryptedContainer, "snap-store-secrets")
610 if container.can_encrypt:620 if container.can_encrypt:
611 self.store_secrets["discharge_encrypted"] = (621 self.store_secrets["discharge_encrypted"] = (
diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py
index 0b712fe..8c87c8a 100644
--- a/lib/lp/snappy/tests/test_snap.py
+++ b/lib/lp/snappy/tests/test_snap.py
@@ -3287,20 +3287,23 @@ class TestSnapWebservice(TestCaseWithFactory):
3287 def test_completeAuthorization(self):3287 def test_completeAuthorization(self):
3288 with admin_logged_in():3288 with admin_logged_in():
3289 snappy_series = self.factory.makeSnappySeries()3289 snappy_series = self.factory.makeSnappySeries()
3290 root_macaroon = Macaroon()
3291 discharge_macaroon = Macaroon()
3290 snap = self.factory.makeSnap(3292 snap = self.factory.makeSnap(
3291 registrant=self.person, store_upload=True,3293 registrant=self.person, store_upload=True,
3292 store_series=snappy_series,3294 store_series=snappy_series,
3293 store_name=self.factory.getUniqueUnicode(),3295 store_name=self.factory.getUniqueUnicode(),
3294 store_secrets={"root": "dummy-root"})3296 store_secrets={"root": root_macaroon.serialize()})
3295 snap_url = api_url(snap)3297 snap_url = api_url(snap)
3296 logout()3298 logout()
3297 response = self.webservice.named_post(3299 response = self.webservice.named_post(
3298 snap_url, "completeAuthorization",3300 snap_url, "completeAuthorization",
3299 discharge_macaroon="dummy-discharge")3301 discharge_macaroon=discharge_macaroon.serialize())
3300 self.assertEqual(200, response.status)3302 self.assertEqual(200, response.status)
3301 with person_logged_in(self.person):3303 with person_logged_in(self.person):
3302 self.assertEqual(3304 self.assertEqual(
3303 {"root": "dummy-root", "discharge": "dummy-discharge"},3305 {"root": root_macaroon.serialize(),
3306 "discharge": discharge_macaroon.serialize()},
3304 snap.store_secrets)3307 snap.store_secrets)
33053308
3306 def test_completeAuthorization_without_beginAuthorization(self):3309 def test_completeAuthorization_without_beginAuthorization(self):
@@ -3312,22 +3315,26 @@ class TestSnapWebservice(TestCaseWithFactory):
3312 store_name=self.factory.getUniqueUnicode())3315 store_name=self.factory.getUniqueUnicode())
3313 snap_url = api_url(snap)3316 snap_url = api_url(snap)
3314 logout()3317 logout()
3318 discharge_macaroon = Macaroon()
3315 response = self.webservice.named_post(3319 response = self.webservice.named_post(
3316 snap_url, "completeAuthorization",3320 snap_url, "completeAuthorization",
3317 discharge_macaroon="dummy-discharge")3321 discharge_macaroon=discharge_macaroon.serialize())
3318 self.assertEqual(400, response.status)3322 self.assertThat(response, MatchesStructure.byEquality(
3319 self.assertEqual(3323 status=400,
3320 "beginAuthorization must be called before completeAuthorization.",3324 body=(
3321 response.body)3325 b"beginAuthorization must be called before "
3326 b"completeAuthorization.")))
33223327
3323 def test_completeAuthorization_unauthorized(self):3328 def test_completeAuthorization_unauthorized(self):
3324 with admin_logged_in():3329 with admin_logged_in():
3325 snappy_series = self.factory.makeSnappySeries()3330 snappy_series = self.factory.makeSnappySeries()
3331 root_macaroon = Macaroon()
3332 discharge_macaroon = Macaroon()
3326 snap = self.factory.makeSnap(3333 snap = self.factory.makeSnap(
3327 registrant=self.person, store_upload=True,3334 registrant=self.person, store_upload=True,
3328 store_series=snappy_series,3335 store_series=snappy_series,
3329 store_name=self.factory.getUniqueUnicode(),3336 store_name=self.factory.getUniqueUnicode(),
3330 store_secrets={"root": "dummy-root"})3337 store_secrets={"root": root_macaroon.serialize()})
3331 snap_url = api_url(snap)3338 snap_url = api_url(snap)
3332 other_person = self.factory.makePerson()3339 other_person = self.factory.makePerson()
3333 other_webservice = webservice_for_person(3340 other_webservice = webservice_for_person(
@@ -3335,7 +3342,7 @@ class TestSnapWebservice(TestCaseWithFactory):
3335 other_webservice.default_api_version = "devel"3342 other_webservice.default_api_version = "devel"
3336 response = other_webservice.named_post(3343 response = other_webservice.named_post(
3337 snap_url, "completeAuthorization",3344 snap_url, "completeAuthorization",
3338 discharge_macaroon="dummy-discharge")3345 discharge_macaroon=discharge_macaroon.serialize())
3339 self.assertEqual(401, response.status)3346 self.assertEqual(401, response.status)
33403347
3341 def test_completeAuthorization_both_macaroons(self):3348 def test_completeAuthorization_both_macaroons(self):
@@ -3349,13 +3356,17 @@ class TestSnapWebservice(TestCaseWithFactory):
3349 store_name=self.factory.getUniqueUnicode())3356 store_name=self.factory.getUniqueUnicode())
3350 snap_url = api_url(snap)3357 snap_url = api_url(snap)
3351 logout()3358 logout()
3359 root_macaroon = Macaroon()
3360 discharge_macaroon = Macaroon()
3352 response = self.webservice.named_post(3361 response = self.webservice.named_post(
3353 snap_url, "completeAuthorization",3362 snap_url, "completeAuthorization",
3354 root_macaroon="dummy-root", discharge_macaroon="dummy-discharge")3363 root_macaroon=root_macaroon.serialize(),
3364 discharge_macaroon=discharge_macaroon.serialize())
3355 self.assertEqual(200, response.status)3365 self.assertEqual(200, response.status)
3356 with person_logged_in(self.person):3366 with person_logged_in(self.person):
3357 self.assertEqual(3367 self.assertEqual(
3358 {"root": "dummy-root", "discharge": "dummy-discharge"},3368 {"root": root_macaroon.serialize(),
3369 "discharge": discharge_macaroon.serialize()},
3359 snap.store_secrets)3370 snap.store_secrets)
33603371
3361 def test_completeAuthorization_only_root_macaroon(self):3372 def test_completeAuthorization_only_root_macaroon(self):
@@ -3370,11 +3381,44 @@ class TestSnapWebservice(TestCaseWithFactory):
3370 store_name=self.factory.getUniqueUnicode())3381 store_name=self.factory.getUniqueUnicode())
3371 snap_url = api_url(snap)3382 snap_url = api_url(snap)
3372 logout()3383 logout()
3384 root_macaroon = Macaroon()
3373 response = self.webservice.named_post(3385 response = self.webservice.named_post(
3374 snap_url, "completeAuthorization", root_macaroon="dummy-root")3386 snap_url, "completeAuthorization",
3387 root_macaroon=root_macaroon.serialize())
3375 self.assertEqual(200, response.status)3388 self.assertEqual(200, response.status)
3376 with person_logged_in(self.person):3389 with person_logged_in(self.person):
3377 self.assertEqual({"root": "dummy-root"}, snap.store_secrets)3390 self.assertEqual(
3391 {"root": root_macaroon.serialize()}, snap.store_secrets)
3392
3393 def test_completeAuthorization_malformed_root_macaroon(self):
3394 with admin_logged_in():
3395 snappy_series = self.factory.makeSnappySeries()
3396 snap = self.factory.makeSnap(
3397 registrant=self.person, store_upload=True,
3398 store_series=snappy_series,
3399 store_name=self.factory.getUniqueUnicode())
3400 snap_url = api_url(snap)
3401 logout()
3402 response = self.webservice.named_post(
3403 snap_url, "completeAuthorization", root_macaroon="nonsense")
3404 self.assertThat(response, MatchesStructure.byEquality(
3405 status=400, body=b"root_macaroon is invalid."))
3406
3407 def test_completeAuthorization_malformed_discharge_macaroon(self):
3408 with admin_logged_in():
3409 snappy_series = self.factory.makeSnappySeries()
3410 snap = self.factory.makeSnap(
3411 registrant=self.person, store_upload=True,
3412 store_series=snappy_series,
3413 store_name=self.factory.getUniqueUnicode())
3414 snap_url = api_url(snap)
3415 logout()
3416 response = self.webservice.named_post(
3417 snap_url, "completeAuthorization",
3418 root_macaroon=Macaroon().serialize(),
3419 discharge_macaroon="nonsense")
3420 self.assertThat(response, MatchesStructure.byEquality(
3421 status=400, body=b"discharge_macaroon is invalid."))
33783422
3379 def test_completeAuthorization_encrypted(self):3423 def test_completeAuthorization_encrypted(self):
3380 private_key = PrivateKey.generate()3424 private_key = PrivateKey.generate()
@@ -3384,16 +3428,18 @@ class TestSnapWebservice(TestCaseWithFactory):
3384 bytes(private_key.public_key)).decode("UTF-8"))3428 bytes(private_key.public_key)).decode("UTF-8"))
3385 with admin_logged_in():3429 with admin_logged_in():
3386 snappy_series = self.factory.makeSnappySeries()3430 snappy_series = self.factory.makeSnappySeries()
3431 root_macaroon = Macaroon()
3432 discharge_macaroon = Macaroon()
3387 snap = self.factory.makeSnap(3433 snap = self.factory.makeSnap(
3388 registrant=self.person, store_upload=True,3434 registrant=self.person, store_upload=True,
3389 store_series=snappy_series,3435 store_series=snappy_series,
3390 store_name=self.factory.getUniqueUnicode(),3436 store_name=self.factory.getUniqueUnicode(),
3391 store_secrets={"root": "dummy-root"})3437 store_secrets={"root": root_macaroon.serialize()})
3392 snap_url = api_url(snap)3438 snap_url = api_url(snap)
3393 logout()3439 logout()
3394 response = self.webservice.named_post(3440 response = self.webservice.named_post(
3395 snap_url, "completeAuthorization",3441 snap_url, "completeAuthorization",
3396 discharge_macaroon="dummy-discharge")3442 discharge_macaroon=discharge_macaroon.serialize())
3397 self.assertEqual(200, response.status)3443 self.assertEqual(200, response.status)
3398 self.pushConfig(3444 self.pushConfig(
3399 "snappy",3445 "snappy",
@@ -3402,9 +3448,9 @@ class TestSnapWebservice(TestCaseWithFactory):
3402 container = getUtility(IEncryptedContainer, "snap-store-secrets")3448 container = getUtility(IEncryptedContainer, "snap-store-secrets")
3403 with person_logged_in(self.person):3449 with person_logged_in(self.person):
3404 self.assertThat(snap.store_secrets, MatchesDict({3450 self.assertThat(snap.store_secrets, MatchesDict({
3405 "root": Equals("dummy-root"),3451 "root": Equals(root_macaroon.serialize()),
3406 "discharge_encrypted": AfterPreprocessing(3452 "discharge_encrypted": AfterPreprocessing(
3407 container.decrypt, Equals("dummy-discharge")),3453 container.decrypt, Equals(discharge_macaroon.serialize())),
3408 }))3454 }))
34093455
3410 def makeBuildableDistroArchSeries(self, **kwargs):3456 def makeBuildableDistroArchSeries(self, **kwargs):

Subscribers

People subscribed via source and target branches

to status/vote changes: