launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #21285
[Merge] lp:~cjwatson/launchpad/snap-store-exports into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-store-exports into lp:launchpad.
Commit message:
Provide parts of Snap:+authorize on the API as Snap.beginAuthorization. Export various store-related properties on Snap, and store_* arguments to SnapSet.new.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snap-store-exports/+merge/312854
build.snapcraft.io needs this in order to be able to create store-uploaded snaps using the API. The ability to control the URL that the authorization workflow redirects to on success lets us steer the user back to build.snapcraft.io when they're finished.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-store-exports into lp:launchpad.
=== modified file 'lib/lp/snappy/browser/snap.py'
--- lib/lp/snappy/browser/snap.py 2016-12-02 12:53:41 +0000
+++ lib/lp/snappy/browser/snap.py 2016-12-08 18:08:19 +0000
@@ -16,15 +16,11 @@
'SnapView',
]
-from urllib import urlencode
-from urlparse import urlsplit
-
from lazr.restful.fields import Reference
from lazr.restful.interface import (
copy_field,
use_template,
)
-from pymacaroons import Macaroon
import yaml
from zope.component import getUtility
from zope.error.interfaces import IErrorReportingUtility
@@ -60,8 +56,8 @@
from lp.registry.enums import VCSType
from lp.registry.interfaces.pocket import PackagePublishingPocket
from lp.services.features import getFeatureFlag
+from lp.services.fields import URIField
from lp.services.helpers import english_list
-from lp.services.openid.adapters.openid import CurrentOpenIDEndPoint
from lp.services.propertycache import cachedproperty
from lp.services.scripts import log
from lp.services.webapp import (
@@ -74,7 +70,6 @@
NavigationMenu,
stepthrough,
structured,
- urlappend,
)
from lp.services.webapp.authorization import check_permission
from lp.services.webapp.breadcrumb import (
@@ -84,6 +79,7 @@
from lp.services.webhooks.browser import WebhookTargetNavigationMixin
from lp.snappy.browser.widgets.snaparchive import SnapArchiveWidget
from lp.snappy.interfaces.snap import (
+ CannotAuthorizeStoreUploads,
ISnap,
ISnapSet,
NoSuchSnap,
@@ -98,7 +94,6 @@
)
from lp.snappy.interfaces.snapstoreclient import (
BadRequestPackageUploadResponse,
- ISnapStoreClient,
)
from lp.soyuz.browser.archive import EnableProcessorsMixin
from lp.soyuz.browser.build import get_build_by_id_str
@@ -782,6 +777,8 @@
discharge_macaroon = TextLine(
title=u'Serialized discharge macaroon', required=True)
+ success_url = URIField(
+ title=u'URL to redirect to on success', allowed_schemes=['https'])
render_context = False
@@ -791,55 +788,15 @@
def cancel_url(self):
return canonical_url(self.context)
- @staticmethod
- def extractSSOCaveat(macaroon):
- locations = [
- urlsplit(root).netloc
- for root in CurrentOpenIDEndPoint.getAllRootURLs()]
- sso_caveats = [
- c for c in macaroon.third_party_caveats()
- if c.location in locations]
- # We must have exactly one SSO caveat; more than one should never be
- # required and could be an attempt to substitute weaker caveats. We
- # might as well OOPS here, even though the cause of this is probably
- # in some other service, since the user can't do anything about it
- # and it should show up in our OOPS reports.
- if not sso_caveats:
- raise SnapAuthorizationException("Macaroon has no SSO caveats")
- elif len(sso_caveats) > 1:
- raise SnapAuthorizationException(
- "Macaroon has multiple SSO caveats")
- return sso_caveats[0]
-
@classmethod
def requestAuthorization(cls, snap, request):
"""Begin the process of authorizing uploads of a snap package."""
- if snap.store_series is None:
- request.response.addInfoNotification(
- _(u'Cannot authorize uploads of a snap package with no '
- u'store series.'))
- request.response.redirect(canonical_url(snap))
- return
- if snap.store_name is None:
- request.response.addInfoNotification(
- _(u'Cannot authorize uploads of a snap package with no '
- u'store name.'))
- request.response.redirect(canonical_url(snap))
- return
- snap_store_client = getUtility(ISnapStoreClient)
- root_macaroon_raw = snap_store_client.requestPackageUploadPermission(
- snap.store_series, snap.store_name)
- sso_caveat = cls.extractSSOCaveat(
- Macaroon.deserialize(root_macaroon_raw))
- snap.store_secrets = {'root': root_macaroon_raw}
- base_url = canonical_url(snap, view_name='+authorize')
- login_url = urlappend(base_url, '+login')
- login_url += '?%s' % urlencode([
- ('macaroon_caveat_id', sso_caveat.caveat_id),
- ('discharge_macaroon_action', 'field.actions.complete'),
- ('discharge_macaroon_field', 'field.discharge_macaroon'),
- ])
- return login_url
+ try:
+ return snap.beginAuthorization()
+ except CannotAuthorizeStoreUploads as e:
+ request.response.addInfoNotification(unicode(e))
+ request.response.redirect(canonical_url(snap))
+ return
@action('Begin authorization', name='begin')
def begin_action(self, action, data):
@@ -854,6 +811,11 @@
_(u'Uploads of %(snap)s to the store were not authorized.'),
snap=self.context.name))
return
+ success_url = data.get('success_url')
+ # XXX cjwatson 2016-12-08: Drop this once all appservers have been
+ # upgraded to always pass success_url.
+ if success_url is None:
+ success_url = canonical_url(self.context)
# We have to set a whole new dict here to avoid problems with
# security proxies.
new_store_secrets = dict(self.context.store_secrets)
@@ -862,7 +824,7 @@
self.request.response.addInfoNotification(structured(
_(u'Uploads of %(snap)s to the store are now authorized.'),
snap=self.context.name))
- self.request.response.redirect(canonical_url(self.context))
+ self.request.response.redirect(success_url)
@property
def adapters(self):
=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
--- lib/lp/snappy/browser/tests/test_snap.py 2016-12-02 12:53:41 +0000
+++ lib/lp/snappy/browser/tests/test_snap.py 2016-12-08 18:08:19 +0000
@@ -11,6 +11,7 @@
)
import json
import re
+from urllib import quote_plus
from urllib2 import HTTPError
from urlparse import (
parse_qs,
@@ -52,7 +53,6 @@
from lp.services.webapp.servers import LaunchpadTestRequest
from lp.snappy.browser.snap import (
SnapAdminView,
- SnapAuthorizeView,
SnapEditView,
SnapView,
)
@@ -433,6 +433,7 @@
"discharge_macaroon_action": ["field.actions.complete"],
"discharge_macaroon_field": ["field.discharge_macaroon"],
"macaroon_caveat_id": ["dummy"],
+ "field.success_url": [canonical_url(snap, rootsite="code")],
}
self.assertEqual(expected_args, parse_qs(parsed_location[3]))
@@ -973,6 +974,7 @@
"discharge_macaroon_action": ["field.actions.complete"],
"discharge_macaroon_field": ["field.discharge_macaroon"],
"macaroon_caveat_id": ["dummy"],
+ "field.success_url": [canonical_url(snap)],
}
self.assertEqual(expected_args, parse_qs(parsed_location[3]))
@@ -996,54 +998,6 @@
store_series=self.snappyseries,
store_name=self.factory.getUniqueUnicode())
- def assertRequestsAuthorization(self, snap, func, *args, **kwargs):
- owner = snap.owner
- root_macaroon = Macaroon()
- root_macaroon.add_third_party_caveat(
- urlsplit(config.launchpad.openid_provider_root).netloc, '',
- 'dummy')
- root_macaroon_raw = root_macaroon.serialize()
-
- @all_requests
- def handler(url, request):
- self.request = request
- return {
- "status_code": 200,
- "content": {"macaroon": root_macaroon_raw},
- }
-
- self.pushConfig("snappy", store_url="http://sca.example/")
- with HTTMock(handler):
- ret = func(*args, **kwargs)
- self.assertThat(self.request, MatchesStructure.byEquality(
- url="http://sca.example/dev/api/acl/", method="POST"))
- with person_logged_in(owner):
- expected_body = {
- "packages": [{
- "name": snap.store_name,
- "series": snap.store_series.name,
- }],
- "permissions": ["package_upload"],
- }
- self.assertEqual(expected_body, json.loads(self.request.body))
- self.assertEqual({"root": root_macaroon_raw}, snap.store_secrets)
- return ret
-
- def test_requestAuthorization(self):
- def request_authorization():
- with person_logged_in(self.snap.owner):
- return SnapAuthorizeView.requestAuthorization(
- self.snap, LaunchpadTestRequest())
-
- login_url = self.assertRequestsAuthorization(
- self.snap, request_authorization)
- self.assertEqual(
- canonical_url(self.snap) +
- "/+authorize/+login?macaroon_caveat_id=dummy&"
- "discharge_macaroon_action=field.actions.complete&"
- "discharge_macaroon_field=field.discharge_macaroon",
- login_url)
-
def test_unauthorized(self):
# A user without edit access cannot authorize snap package uploads.
self.useFixture(FakeLogger())
@@ -1057,20 +1011,46 @@
# to begin authorization. This allows (re-)authorizing uploads of
# an existing snap package without having to edit it.
snap_url = canonical_url(self.snap)
-
- def begin_authorization():
+ owner = self.snap.owner
+ root_macaroon = Macaroon()
+ root_macaroon.add_third_party_caveat(
+ urlsplit(config.launchpad.openid_provider_root).netloc, '',
+ 'dummy')
+ root_macaroon_raw = root_macaroon.serialize()
+
+ @all_requests
+ def handler(url, request):
+ self.request = request
+ return {
+ "status_code": 200,
+ "content": {"macaroon": root_macaroon_raw},
+ }
+
+ self.pushConfig("snappy", store_url="http://sca.example/")
+ with HTTMock(handler):
browser = self.getNonRedirectingBrowser(
url=snap_url + "/+authorize", user=self.snap.owner)
- return self.assertRaises(
+ redirection = self.assertRaises(
HTTPError, browser.getControl("Begin authorization").click)
-
- redirection = self.assertRequestsAuthorization(
- self.snap, begin_authorization)
+ self.assertThat(self.request, MatchesStructure.byEquality(
+ url="http://sca.example/dev/api/acl/", method="POST"))
+ with person_logged_in(owner):
+ expected_body = {
+ "packages": [{
+ "name": self.snap.store_name,
+ "series": self.snap.store_series.name,
+ }],
+ "permissions": ["package_upload"],
+ }
+ self.assertEqual(expected_body, json.loads(self.request.body))
+ self.assertEqual(
+ {"root": root_macaroon_raw}, self.snap.store_secrets)
self.assertEqual(303, redirection.code)
self.assertEqual(
snap_url + "/+authorize/+login?macaroon_caveat_id=dummy&"
"discharge_macaroon_action=field.actions.complete&"
- "discharge_macaroon_field=field.discharge_macaroon",
+ "discharge_macaroon_field=field.discharge_macaroon&"
+ "field.success_url=" + quote_plus(snap_url),
redirection.hdrs["Location"])
def test_complete_authorization_missing_discharge_macaroon(self):
@@ -1116,6 +1096,32 @@
{"root": "root", "discharge": "discharge"},
self.snap.store_secrets)
+ def test_complete_authorization_with_success_url(self):
+ # The "complete" action honours the provided success_url.
+ with person_logged_in(self.snap.owner):
+ self.snap.store_secrets = {"root": "root"}
+ transaction.commit()
+ form = {
+ "field.actions.complete": "1",
+ "field.discharge_macaroon": "discharge",
+ "field.success_url": "https://example.org/",
+ }
+ view = create_initialized_view(
+ self.snap, "+authorize", form=form, method="POST",
+ principal=self.snap.owner)
+ self.assertEqual("", view())
+ self.assertEqual(302, view.request.response.getStatus())
+ self.assertEqual(
+ "https://example.org/",
+ view.request.response.getHeader("Location"))
+ self.assertEqual(
+ "Uploads of %s to the store are now authorized." %
+ self.snap.name,
+ view.request.response.notifications[0].message)
+ self.assertEqual(
+ {"root": "root", "discharge": "discharge"},
+ self.snap.store_secrets)
+
class TestSnapDeleteView(BrowserTestCase):
=== modified file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py 2016-12-05 18:57:11 +0000
+++ lib/lp/snappy/interfaces/snap.py 2016-12-08 18:08:19 +0000
@@ -7,6 +7,7 @@
__all__ = [
'BadSnapSearchContext',
+ 'CannotAuthorizeStoreUploads',
'CannotModifySnapProcessor',
'CannotRequestAutoBuilds',
'DuplicateSnapName',
@@ -19,6 +20,7 @@
'SNAP_PRIVATE_FEATURE_FLAG',
'SNAP_TESTING_FLAGS',
'SNAP_WEBHOOKS_FEATURE_FLAG',
+ 'SnapAuthorizationBadMacaroon',
'SnapBuildAlreadyPending',
'SnapBuildArchiveOwnerMismatch',
'SnapBuildDisallowedArchitecture',
@@ -208,6 +210,16 @@
@error_status(httplib.BAD_REQUEST)
+class CannotAuthorizeStoreUploads(Exception):
+ """Cannot authorize uploads of a snap package to the store."""
+
+
+@error_status(httplib.INTERNAL_SERVER_ERROR)
+class SnapAuthorizationBadMacaroon(Exception):
+ """The macaroon generated to authorize store uploads is unusable."""
+
+
+@error_status(httplib.BAD_REQUEST)
class CannotRequestAutoBuilds(Exception):
"""Snap package is not configured for automatic builds."""
@@ -252,9 +264,11 @@
:return: Sequence of `IDistroArchSeries` instances.
"""
- can_upload_to_store = Attribute(
- "Whether everything is set up to allow uploading builds of this snap "
- "package to the store.")
+ can_upload_to_store = exported(Bool(
+ title=_("Can upload to store"), required=True, readonly=True,
+ description=_(
+ "Whether everything is set up to allow uploading builds of this "
+ "snap package to the store.")))
@call_with(requester=REQUEST_USER)
@operation_parameters(
@@ -333,6 +347,31 @@
:return: A sequence of `ISnapBuild` instances.
"""
+ @operation_parameters(
+ success_url=URIField(
+ title=_("URL to redirect to on success"),
+ allowed_schemes=["https"]))
+ @export_write_operation()
+ @operation_for_version("devel")
+ def beginAuthorization(success_url=None):
+ """Begin authorizing uploads of this snap package to the store.
+
+ This is intended for use by third-party sites integrating with
+ Launchpad. Most users should visit <snap URL>/+authorize instead.
+
+ :param success_url: The URL to redirect to when authorization is
+ complete. Defaults to the canonical URL of the snap.
+ :raises CannotAuthorizeStoreUploads: if the snap package is not
+ properly configured for store uploads.
+ :raises BadRequestPackageUploadResponse: if the store returns an
+ error or a response without a macaroon when asked to issue a
+ package_upload macaroon.
+ :raises SnapAuthorizationBadMacaroon: if the package_upload macaroon
+ returned by the store has unsuitable SSO caveats.
+ :return: A URL that the third-party site should redirect the user to
+ in order to continue authorization.
+ """
+
@export_destructor_operation()
@operation_for_version("devel")
def destroySelf():
@@ -430,31 +469,36 @@
title=_("Snap package is stale and is due to be rebuilt."),
required=True, readonly=False)
- store_upload = Bool(
+ store_upload = exported(Bool(
title=_("Automatically upload to store"),
required=True, readonly=False,
description=_(
"Whether builds of this snap package are automatically uploaded "
- "to the store."))
+ "to the store.")))
- store_series = ReferenceChoice(
+ # XXX cjwatson 2016-12-08: We should limit this to series that are
+ # compatible with distro_series, but that entails validating the case
+ # where both are changed in a single PATCH request in such a way that
+ # neither is compatible with the old value of the other. As far as I
+ # can tell lazr.restful only supports per-field validation.
+ store_series = exported(ReferenceChoice(
title=_("Store series"),
schema=ISnappySeries, vocabulary="SnappySeries",
required=False, readonly=False,
description=_(
"The series in which this snap package should be published in the "
- "store."))
+ "store.")))
store_distro_series = ReferenceChoice(
title=_("Store and distro series"),
schema=ISnappyDistroSeries, vocabulary="SnappyDistroSeries",
required=False, readonly=False)
- store_name = TextLine(
+ store_name = exported(TextLine(
title=_("Registered store package name"),
required=False, readonly=False,
description=_(
- "The registered name of this snap package in the store."))
+ "The registered name of this snap package in the store.")))
store_secrets = List(
value_type=TextLine(), title=_("Store upload tokens"),
@@ -518,7 +562,7 @@
"owner", "distro_series", "name", "description", "branch",
"git_repository", "git_repository_url", "git_path", "git_ref",
"auto_build", "auto_build_archive", "auto_build_pocket",
- "private"])
+ "private", "store_upload", "store_series", "store_name"])
@operation_for_version("devel")
def new(registrant, owner, distro_series, name, description=None,
branch=None, git_repository=None, git_repository_url=None,
=== modified file 'lib/lp/snappy/interfaces/snapstoreclient.py'
--- lib/lp/snappy/interfaces/snapstoreclient.py 2016-06-30 16:05:11 +0000
+++ lib/lp/snappy/interfaces/snapstoreclient.py 2016-12-08 18:08:19 +0000
@@ -21,9 +21,13 @@
'UploadNotScannedYetResponse',
]
+import httplib
+
+from lazr.restful.declarations import error_status
from zope.interface import Interface
+@error_status(httplib.INTERNAL_SERVER_ERROR)
class BadRequestPackageUploadResponse(Exception):
pass
=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py 2016-12-06 00:02:46 +0000
+++ lib/lp/snappy/model/snap.py 2016-12-08 18:08:19 +0000
@@ -10,7 +10,10 @@
datetime,
timedelta,
)
+from urllib import urlencode
+from urlparse import urlsplit
+from pymacaroons import Macaroon
import pytz
from storm.expr import (
And,
@@ -97,11 +100,15 @@
LibraryFileAlias,
LibraryFileContent,
)
+from lp.services.openid.adapters.openid import CurrentOpenIDEndPoint
from lp.services.webapp.interfaces import ILaunchBag
+from lp.services.webapp.publisher import canonical_url
+from lp.services.webapp.url import urlappend
from lp.services.webhooks.interfaces import IWebhookSet
from lp.services.webhooks.model import WebhookTargetMixin
from lp.snappy.interfaces.snap import (
BadSnapSearchContext,
+ CannotAuthorizeStoreUploads,
CannotModifySnapProcessor,
CannotRequestAutoBuilds,
DuplicateSnapName,
@@ -110,6 +117,7 @@
NoSourceForSnap,
NoSuchSnap,
SNAP_PRIVATE_FEATURE_FLAG,
+ SnapAuthorizationBadMacaroon,
SnapBuildAlreadyPending,
SnapBuildArchiveOwnerMismatch,
SnapBuildDisallowedArchitecture,
@@ -119,6 +127,7 @@
)
from lp.snappy.interfaces.snapbuild import ISnapBuildSet
from lp.snappy.interfaces.snappyseries import ISnappyDistroSeriesSet
+from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient
from lp.snappy.model.snapbuild import SnapBuild
from lp.soyuz.interfaces.archive import ArchiveDisabled
from lp.soyuz.interfaces.buildrecords import IncompatibleArguments
@@ -340,6 +349,54 @@
self.distro_series = value.distro_series
self.store_series = value.snappy_series
+ @staticmethod
+ def extractSSOCaveat(macaroon):
+ locations = [
+ urlsplit(root).netloc
+ for root in CurrentOpenIDEndPoint.getAllRootURLs()]
+ sso_caveats = [
+ c for c in macaroon.third_party_caveats()
+ if c.location in locations]
+ # We must have exactly one SSO caveat; more than one should never be
+ # required and could be an attempt to substitute weaker caveats. We
+ # might as well OOPS here, even though the cause of this is probably
+ # in some other service, since the user can't do anything about it
+ # and it should show up in our OOPS reports.
+ if not sso_caveats:
+ raise SnapAuthorizationBadMacaroon("Macaroon has no SSO caveats")
+ elif len(sso_caveats) > 1:
+ raise SnapAuthorizationBadMacaroon(
+ "Macaroon has multiple SSO caveats")
+ return sso_caveats[0]
+
+ def beginAuthorization(self, success_url=None):
+ """See `ISnap`."""
+ if self.store_series is None:
+ raise CannotAuthorizeStoreUploads(
+ "Cannot authorize uploads of a snap package with no store "
+ "series.")
+ if self.store_name is None:
+ raise CannotAuthorizeStoreUploads(
+ "Cannot authorize uploads of a snap package with no store "
+ "name.")
+ if success_url is None:
+ success_url = canonical_url(self)
+ snap_store_client = getUtility(ISnapStoreClient)
+ root_macaroon_raw = snap_store_client.requestPackageUploadPermission(
+ self.store_series, self.store_name)
+ sso_caveat = self.extractSSOCaveat(
+ Macaroon.deserialize(root_macaroon_raw))
+ self.store_secrets = {'root': root_macaroon_raw}
+ base_url = canonical_url(self, view_name='+authorize')
+ login_url = urlappend(base_url, '+login')
+ login_url += '?%s' % urlencode([
+ ('macaroon_caveat_id', sso_caveat.caveat_id),
+ ('discharge_macaroon_action', 'field.actions.complete'),
+ ('discharge_macaroon_field', 'field.discharge_macaroon'),
+ ('field.success_url', success_url),
+ ])
+ return login_url
+
@property
def can_upload_to_store(self):
return (
=== modified file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py 2016-12-02 12:53:41 +0000
+++ lib/lp/snappy/tests/test_snap.py 2016-12-08 18:08:19 +0000
@@ -9,8 +9,16 @@
datetime,
timedelta,
)
+import json
+from urllib import quote_plus
+from urlparse import urlsplit
+from httmock import (
+ all_requests,
+ HTTMock,
+ )
from lazr.lifecycle.event import ObjectModifiedEvent
+from pymacaroons import Macaroon
import pytz
from storm.exceptions import LostObjectError
from storm.locals import Store
@@ -37,6 +45,7 @@
from lp.registry.enums import PersonVisibility
from lp.registry.interfaces.distribution import IDistributionSet
from lp.registry.interfaces.pocket import PackagePublishingPocket
+from lp.services.config import config
from lp.services.database.constants import (
ONE_DAY_AGO,
UTC_NOW,
@@ -46,6 +55,7 @@
from lp.services.log.logger import BufferLogger
from lp.services.propertycache import clear_property_cache
from lp.services.webapp.interfaces import OAuthPermission
+from lp.services.webapp.publisher import canonical_url
from lp.snappy.interfaces.snap import (
BadSnapSearchContext,
CannotModifySnapProcessor,
@@ -1106,7 +1116,7 @@
class TestSnapWebservice(TestCaseWithFactory):
- layer = DatabaseFunctionalLayer
+ layer = LaunchpadFunctionalLayer
def setUp(self):
super(TestSnapWebservice, self).setUp()
@@ -1397,6 +1407,90 @@
response = self.setProcessors(self.person, snap, ["386"])
self.assertEqual(403, response.status)
+ def assertBeginsAuthorization(self, snap, **kwargs):
+ snap_url = api_url(snap)
+ root_macaroon = Macaroon()
+ root_macaroon.add_third_party_caveat(
+ urlsplit(config.launchpad.openid_provider_root).netloc, '',
+ 'dummy')
+ root_macaroon_raw = root_macaroon.serialize()
+
+ @all_requests
+ def handler(url, request):
+ self.request = request
+ return {
+ "status_code": 200,
+ "content": {"macaroon": root_macaroon_raw},
+ }
+
+ self.pushConfig("snappy", store_url="http://sca.example/")
+ logout()
+ with HTTMock(handler):
+ response = self.webservice.named_post(
+ snap_url, "beginAuthorization", **kwargs)
+ self.assertThat(self.request, MatchesStructure.byEquality(
+ url="http://sca.example/dev/api/acl/", method="POST"))
+ with person_logged_in(self.person):
+ expected_body = {
+ "packages": [{
+ "name": snap.store_name,
+ "series": snap.store_series.name,
+ }],
+ "permissions": ["package_upload"],
+ }
+ self.assertEqual(expected_body, json.loads(self.request.body))
+ self.assertEqual({"root": root_macaroon_raw}, snap.store_secrets)
+ return response
+
+ def test_beginAuthorization(self):
+ with admin_logged_in():
+ snappy_series = self.factory.makeSnappySeries()
+ snap = self.factory.makeSnap(
+ registrant=self.person, store_upload=True,
+ store_series=snappy_series,
+ store_name=self.factory.getUniqueUnicode())
+ snap_url = canonical_url(snap)
+ response = self.assertBeginsAuthorization(snap)
+ self.assertEqual(
+ snap_url + "/+authorize/+login?macaroon_caveat_id=dummy&"
+ "discharge_macaroon_action=field.actions.complete&"
+ "discharge_macaroon_field=field.discharge_macaroon&"
+ "field.success_url=" + quote_plus(snap_url),
+ response.jsonBody())
+
+ def test_beginAuthorization_with_success_url(self):
+ with admin_logged_in():
+ snappy_series = self.factory.makeSnappySeries()
+ snap = self.factory.makeSnap(
+ registrant=self.person, store_upload=True,
+ store_series=snappy_series,
+ store_name=self.factory.getUniqueUnicode())
+ snap_url = canonical_url(snap)
+ response = self.assertBeginsAuthorization(
+ snap, success_url="https://example.org/")
+ self.assertEqual(
+ snap_url + "/+authorize/+login?macaroon_caveat_id=dummy&"
+ "discharge_macaroon_action=field.actions.complete&"
+ "discharge_macaroon_field=field.discharge_macaroon&"
+ "field.success_url=https%3A%2F%2Fexample.org%2F",
+ response.jsonBody())
+
+ def test_beginAuthorization_unauthorized(self):
+ # A user without edit access cannot authorize snap package uploads.
+ with admin_logged_in():
+ snappy_series = self.factory.makeSnappySeries()
+ snap = self.factory.makeSnap(
+ registrant=self.person, store_upload=True,
+ store_series=snappy_series,
+ store_name=self.factory.getUniqueUnicode())
+ snap_url = api_url(snap)
+ other_person = self.factory.makePerson()
+ other_webservice = webservice_for_person(
+ other_person, permission=OAuthPermission.WRITE_PUBLIC)
+ other_webservice.default_api_version = "devel"
+ response = other_webservice.named_post(snap_url, "beginAuthorization")
+ self.assertEqual(401, response.status)
+
def makeBuildableDistroArchSeries(self, **kwargs):
das = self.factory.makeDistroArchSeries(**kwargs)
fake_chroot = self.factory.makeLibraryFileAlias(
Follow ups