← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/snap-mail-store-auth-errors into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-mail-store-auth-errors into lp:launchpad.

Commit message:
Mail the snap build requester if store uploading returns an unauthorized response.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snap-mail-store-auth-errors/+merge/296443

Mail the snap build requester if store uploading returns an unauthorized response.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-mail-store-auth-errors into lp:launchpad.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg	2016-06-02 19:08:32 +0000
+++ database/schema/security.cfg	2016-06-03 14:54:17 +0000
@@ -2541,13 +2541,16 @@
 [snap-build-job]
 type=user
 groups=script
+public.archive                          = SELECT
 public.distribution                     = SELECT
 public.distroarchseries                 = SELECT
 public.distroseries                     = SELECT
+public.emailaddress                     = SELECT
 public.job                              = SELECT, UPDATE
 public.libraryfilealias                 = SELECT
 public.libraryfilecontent               = SELECT
 public.person                           = SELECT
+public.personsettings                   = SELECT
 public.snap                             = SELECT
 public.snapbuild                        = SELECT, UPDATE
 public.snapbuildjob                     = SELECT, UPDATE

=== modified file 'lib/lp/snappy/interfaces/snapstoreclient.py'
--- lib/lp/snappy/interfaces/snapstoreclient.py	2016-05-28 01:00:33 +0000
+++ lib/lp/snappy/interfaces/snapstoreclient.py	2016-06-03 14:54:17 +0000
@@ -12,6 +12,7 @@
     'BadUploadResponse',
     'ISnapStoreClient',
     'NeedsRefreshResponse',
+    'UnauthorizedUploadResponse',
     ]
 
 from zope.interface import Interface
@@ -33,6 +34,10 @@
     pass
 
 
+class UnauthorizedUploadResponse(Exception):
+    pass
+
+
 class ISnapStoreClient(Interface):
     """Interface for the API provided by the snap store."""
 

=== modified file 'lib/lp/snappy/mail/snapbuild.py'
--- lib/lp/snappy/mail/snapbuild.py	2015-08-23 22:53:55 +0000
+++ lib/lp/snappy/mail/snapbuild.py	2016-06-03 14:54:17 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015 Canonical Ltd.  This software is licensed under the
+# Copyright 2015-2016 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -30,13 +30,27 @@
         return cls(
             "[Snap build #%(build_id)d] %(build_title)s",
             "snapbuild-notification.txt", recipients,
-            config.canonical.noreply_from_address, build)
+            config.canonical.noreply_from_address, "snap-build-status", build)
+
+    @classmethod
+    def forUnauthorizedUpload(cls, build):
+        """Create a mailer for notifying about unauthorized store uploads.
+
+        :param build: The relevant build.
+        """
+        requester = build.requester
+        recipients = {requester: RecipientReason.forBuildRequester(requester)}
+        return cls(
+            "Store authorization failed for %(snap_name)s",
+            "snapbuild-unauthorized.txt", recipients,
+            config.canonical.noreply_from_address,
+            "snap-build-upload-unauthorized", build)
 
     def __init__(self, subject, template_name, recipients, from_address,
-                 build):
+                 notification_type, build):
         super(SnapBuildMailer, self).__init__(
             subject, template_name, recipients, from_address,
-            notification_type="snap-build-status")
+            notification_type=notification_type)
         self.build = build
 
     def _getHeaders(self, email, recipient):
@@ -63,7 +77,9 @@
             "log_url": "",
             "upload_log_url": "",
             "builder_url": "",
-            "build_url": canonical_url(self.build),
+            "build_url": canonical_url(build),
+            "snap_authorize_url": canonical_url(
+                build.snap, view_name="+authorize"),
             })
         if build.duration is not None:
             duration_formatter = DurationFormatterAPI(build.duration)

=== modified file 'lib/lp/snappy/model/snapbuildjob.py'
--- lib/lp/snappy/model/snapbuildjob.py	2016-05-06 16:34:21 +0000
+++ lib/lp/snappy/model/snapbuildjob.py	2016-06-03 14:54:17 +0000
@@ -47,7 +47,11 @@
     ISnapStoreUploadJob,
     ISnapStoreUploadJobSource,
     )
-from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient
+from lp.snappy.interfaces.snapstoreclient import (
+    ISnapStoreClient,
+    UnauthorizedUploadResponse,
+    )
+from lp.snappy.mail.snapbuild import SnapBuildMailer
 
 
 class SnapBuildJobType(DBEnumeratedType):
@@ -187,5 +191,8 @@
             # message.
             transaction.abort()
             self.error_message = str(e)
+            if isinstance(e, UnauthorizedUploadResponse):
+                mailer = SnapBuildMailer.forUnauthorizedUpload(self.snapbuild)
+                mailer.sendAll()
             transaction.commit()
             raise

=== modified file 'lib/lp/snappy/model/snapstoreclient.py'
--- lib/lp/snappy/model/snapstoreclient.py	2016-05-31 10:54:32 +0000
+++ lib/lp/snappy/model/snapstoreclient.py	2016-06-03 14:54:17 +0000
@@ -28,6 +28,7 @@
     BadUploadResponse,
     ISnapStoreClient,
     NeedsRefreshResponse,
+    UnauthorizedUploadResponse,
     )
 
 
@@ -163,10 +164,12 @@
                     snap.store_secrets["root"],
                     snap.store_secrets["discharge"]))
         except requests.HTTPError as e:
-            if (e.response.status_code == 401 and
-                e.response.headers.get("WWW-Authenticate") ==
-                    "Macaroon needs_refresh=1"):
-                raise NeedsRefreshResponse()
+            if e.response.status_code == 401:
+                if (e.response.headers.get("WWW-Authenticate") ==
+                        "Macaroon needs_refresh=1"):
+                    raise NeedsRefreshResponse()
+                else:
+                    raise UnauthorizedUploadResponse("Authorization failed.")
             raise BadUploadResponse(e.args[0])
 
     def upload(self, snapbuild):

=== modified file 'lib/lp/snappy/tests/test_snapbuildjob.py'
--- lib/lp/snappy/tests/test_snapbuildjob.py	2016-05-06 16:34:21 +0000
+++ lib/lp/snappy/tests/test_snapbuildjob.py	2016-06-03 14:54:17 +0000
@@ -17,7 +17,10 @@
     ISnapBuildJob,
     ISnapStoreUploadJob,
     )
-from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient
+from lp.snappy.interfaces.snapstoreclient import (
+    ISnapStoreClient,
+    UnauthorizedUploadResponse,
+    )
 from lp.snappy.model.snapbuildjob import (
     SnapBuildJob,
     SnapBuildJobType,
@@ -31,6 +34,7 @@
     DatabaseFunctionalLayer,
     LaunchpadZopelessLayer,
     )
+from lp.testing.mail_helpers import pop_notifications
 
 
 @implementer(ISnapStoreClient)
@@ -89,6 +93,7 @@
         self.assertEqual([((snapbuild,), {})], client.upload.calls)
         self.assertContentEqual([job], snapbuild.store_upload_jobs)
         self.assertIsNone(job.error_message)
+        self.assertEqual([], pop_notifications())
 
     def test_run_failed(self):
         # A failed run sets the store upload status to FAILED.
@@ -103,3 +108,42 @@
         self.assertEqual([((snapbuild,), {})], client.upload.calls)
         self.assertContentEqual([job], snapbuild.store_upload_jobs)
         self.assertEqual("An upload failure", job.error_message)
+        self.assertEqual([], pop_notifications())
+
+    def test_run_unauthorized_notifies(self):
+        # A run that gets 401 from the store sends mail.
+        requester = self.factory.makePerson(name="requester")
+        snapbuild = self.factory.makeSnapBuild(
+            requester=requester, name="test-snap", owner=requester)
+        self.assertContentEqual([], snapbuild.store_upload_jobs)
+        job = SnapStoreUploadJob.create(snapbuild)
+        client = FakeSnapStoreClient()
+        client.upload.failure = UnauthorizedUploadResponse(
+            "Authorization failed.")
+        self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
+        with dbuser(config.ISnapStoreUploadJobSource.dbuser):
+            JobRunner([job]).runAll()
+        self.assertEqual([((snapbuild,), {})], client.upload.calls)
+        self.assertContentEqual([job], snapbuild.store_upload_jobs)
+        self.assertEqual("Authorization failed.", job.error_message)
+        [notification] = pop_notifications()
+        self.assertEqual(
+            config.canonical.noreply_from_address, notification["From"])
+        self.assertEqual(
+            "Requester <%s>" % requester.preferredemail.email,
+            notification["To"])
+        subject = notification["Subject"].replace("\n ", " ")
+        self.assertEqual("Store authorization failed for test-snap", subject)
+        self.assertEqual(
+            "Requester", notification["X-Launchpad-Message-Rationale"])
+        self.assertEqual(
+            requester.name, notification["X-Launchpad-Message-For"])
+        self.assertEqual(
+            "snap-build-upload-unauthorized",
+            notification["X-Launchpad-Notification-Type"])
+        body, footer = notification.get_payload(decode=True).split("\n-- \n")
+        self.assertIn(
+            "http://launchpad.dev/~requester/+snap/test-snap/+authorize";, body)
+        self.assertEqual(
+            "http://launchpad.dev/~requester/+snap/test-snap/+build/%d\n";
+            "You are the requester of the build.\n" % snapbuild.id, footer)

=== modified file 'lib/lp/snappy/tests/test_snapstoreclient.py'
--- lib/lp/snappy/tests/test_snapstoreclient.py	2016-05-31 10:54:32 +0000
+++ lib/lp/snappy/tests/test_snapstoreclient.py	2016-06-03 14:54:17 +0000
@@ -44,6 +44,7 @@
 from lp.snappy.interfaces.snapstoreclient import (
     BadRequestPackageUploadResponse,
     ISnapStoreClient,
+    UnauthorizedUploadResponse,
     )
 from lp.snappy.model.snapstoreclient import MacaroonAuth
 from lp.testing import (
@@ -279,6 +280,29 @@
                 "name": "test-snap", "updown_id": 1, "series": "rolling",
                 }))
 
+    def test_upload_unauthorized(self):
+        @urlmatch(path=r".*/snap-push/$")
+        def snap_push_handler(url, request):
+            self.snap_push_request = request
+            return {
+                "status_code": 401,
+                "headers": {"WWW-Authenticate": 'Macaroon realm="Devportal"'},
+                }
+
+        store_secrets = self._make_store_secrets()
+        snap = self.factory.makeSnap(
+            store_upload=True,
+            store_series=self.factory.makeSnappySeries(name="rolling"),
+            store_name="test-snap", store_secrets=store_secrets)
+        snapbuild = self.factory.makeSnapBuild(snap=snap)
+        lfa = self.factory.makeLibraryFileAlias(content="dummy snap content")
+        self.factory.makeSnapFile(snapbuild=snapbuild, libraryfile=lfa)
+        transaction.commit()
+        with HTTMock(self._unscanned_upload_handler, snap_push_handler,
+                     self._macaroon_refresh_handler):
+            self.assertRaises(
+                UnauthorizedUploadResponse, self.client.upload, snapbuild)
+
     def test_upload_needs_discharge_macaroon_refresh(self):
         @urlmatch(path=r".*/snap-push/$")
         def snap_push_handler(url, request):


Follow ups