Simplify Mandrill webhook validation handshake.

Anymail was requiring Mandrill's webhook authentication key for the initial webhook url validation request from Mandrill, but Mandrill doesn't issue the key until that validation request succeeds.

* Defer complaining about missing Mandrill webhook key until actual event post.
* Document the double-deploy process required to set up Mandrill webhooks.

Fixes #46.
This commit is contained in:
medmunds
2016-12-16 14:24:46 -08:00
parent 52596394cc
commit 146afbaf3b
3 changed files with 39 additions and 12 deletions

View File

@@ -23,8 +23,11 @@ class MandrillSignatureMixin(object):
def __init__(self, **kwargs):
# noinspection PyUnresolvedReferences
esp_name = self.esp_name
webhook_key = get_anymail_setting('webhook_key', esp_name=esp_name,
# webhook_key is required for POST, but not for HEAD when Mandrill validates webhook url.
# Defer "missing setting" error until we actually try to use it in the POST...
webhook_key = get_anymail_setting('webhook_key', esp_name=esp_name, default=None,
kwargs=kwargs, allow_bare=True)
if webhook_key is not None:
self.webhook_key = webhook_key.encode('ascii') # hmac.new requires bytes key in python 3
self.webhook_url = get_anymail_setting('webhook_url', esp_name=esp_name, default=None,
kwargs=kwargs, allow_bare=True)
@@ -32,6 +35,11 @@ class MandrillSignatureMixin(object):
super(MandrillSignatureMixin, self).__init__(**kwargs)
def validate_request(self, request):
if self.webhook_key is None:
# issue deferred "missing setting" error (re-call get-setting without a default)
# noinspection PyUnresolvedReferences
get_anymail_setting('webhook_key', esp_name=self.esp_name, allow_bare=True)
try:
signature = request.META["HTTP_X_MANDRILL_SIGNATURE"]
except KeyError:

View File

@@ -191,20 +191,32 @@ Status tracking webhooks
------------------------
If you are using Anymail's normalized :ref:`status tracking <event-tracking>`,
follow `Mandrill's instructions`_ to add Anymail's webhook URL:
setting up Anymail's webhook URL requires deploying your Django project twice:
1. First, follow the instructions to
:ref:`configure Anymail's webhooks <webhooks-configuration>`. You *must*
deploy before adding the webhook URL to Mandrill, because it will attempt
to verify the URL against your production server.
Follow `Mandrill's instructions`_ to add Anymail's webhook URL in their settings:
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/mandrill/tracking/`
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
* *yoursite.example.com* is your Django site
Be sure to check the boxes in the Mandrill settings for the event types you want to receive.
The same Anymail tracking URL can handle all Mandrill "message" and "sync" events.
Be sure to check the boxes in the Mandrill settings for the event types you want to receive.
The same Anymail tracking URL can handle all Mandrill "message" and "sync" events.
2. Mandrill will provide you a "webhook authentication key" once it verifies the URL
is working. Add this to your Django project's Anymail settings under
:setting:`MANDRILL_WEBHOOK_KEY <ANYMAIL_MANDRILL_WEBHOOK_KEY>`.
(You may also need to set :setting:`MANDRILL_WEBHOOK_URL <ANYMAIL_MANDRILL_WEBHOOK_URL>`
depending on your server config.) Then deploy your project again.
Mandrill implements webhook signing on the entire event payload, and Anymail will
verify the signature. You must set :setting:`ANYMAIL_MANDRILL_WEBHOOK_KEY` to the
webhook key authentication key issued by Mandrill. You may also need to set
:setting:`ANYMAIL_MANDRILL_WEBHOOK_URL` depending on your server config.
verify the signature. Until the correct webhook key is set, Anymail will raise
an exception for any webhook calls from Mandrill (other than the initial validation request).
Mandrill will report these Anymail :attr:`~anymail.signals.AnymailTrackingEvent.event_type`\s:
sent, rejected, deferred, bounced, opened, clicked, complained, unsubscribed. Mandrill does

View File

@@ -41,10 +41,17 @@ def mandrill_args(events=None, url='/anymail/mandrill/tracking/', key=TEST_WEBHO
class MandrillWebhookSettingsTestCase(WebhookTestCase):
def test_requires_webhook_key(self):
with self.assertRaises(ImproperlyConfigured):
with self.assertRaisesRegex(ImproperlyConfigured, r'MANDRILL_WEBHOOK_KEY'):
self.client.post('/anymail/mandrill/tracking/',
data={'mandrill_events': '[]'})
def test_head_does_not_require_webhook_key(self):
# Mandrill issues an unsigned HEAD request to verify the wehbook url.
# Only *after* that succeeds will Mandrill will tell you the webhook key.
# So make sure that HEAD request will go through without any key set:
response = self.client.head('/anymail/mandrill/tracking/')
self.assertEqual(response.status_code, 200)
@override_settings(ANYMAIL_MANDRILL_WEBHOOK_KEY=TEST_WEBHOOK_KEY)
class MandrillWebhookSecurityTestCase(WebhookTestCase, WebhookBasicAuthTestsMixin):