Skip to content

Commit 32e75c1

Browse files
committed
Skip webhook tests if pynacl is unavailable, support Django versions pre-4.2, do not attempt to validate webhooks if webhook key is not set
1 parent 0362a1c commit 32e75c1

File tree

3 files changed

+46
-32
lines changed

3 files changed

+46
-32
lines changed

anymail/webhooks/mailpace.py

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.utils.dateparse import parse_datetime
77

88
from anymail.exceptions import (
9+
AnymailConfigurationError,
910
AnymailImproperlyInstalled,
1011
AnymailWebhookValidationFailure,
1112
_LazyError,
@@ -50,9 +51,12 @@ class MailPaceTrackingWebhookView(MailPaceBaseWebhookView):
5051
webhook_key = None
5152

5253
def __init__(self, **kwargs):
53-
self.webhook_key = get_anymail_setting(
54-
"webhook_key", esp_name=self.esp_name, kwargs=kwargs, allow_bare=True
55-
)
54+
try:
55+
get_anymail_setting(
56+
"webhook_key", esp_name=self.esp_name, kwargs=kwargs, allow_bare=True
57+
)
58+
except AnymailConfigurationError:
59+
self.webhook_key = None
5660

5761
super().__init__(**kwargs)
5862

@@ -71,26 +75,29 @@ def __init__(self, **kwargs):
7175
# MailPace doesn't send a signature for inbound webhooks, yet
7276
# When/if MailPace does this, move this to the parent class
7377
def validate_request(self, request):
74-
try:
75-
signature_base64 = request.headers["X-MailPace-Signature"]
76-
signature = base64.b64decode(signature_base64)
77-
except (KeyError, binascii.Error):
78-
raise AnymailWebhookValidationFailure(
79-
"MailPace webhook called with invalid or missing signature"
80-
)
81-
82-
verify_key_base64 = self.webhook_key
83-
84-
verify_key = VerifyKey(base64.b64decode(verify_key_base64))
85-
86-
message = request.body
87-
88-
try:
89-
verify_key.verify(message, signature)
90-
except (CryptoError, ValueError):
91-
raise AnymailWebhookValidationFailure(
92-
"MailPace webhook called with incorrect signature"
93-
)
78+
if self.webhook_key is None:
79+
return True
80+
else:
81+
try:
82+
signature_base64 = request.headers["X-MailPace-Signature"]
83+
signature = base64.b64decode(signature_base64)
84+
except (KeyError, binascii.Error):
85+
raise AnymailWebhookValidationFailure(
86+
"MailPace webhook called with invalid or missing signature"
87+
)
88+
89+
verify_key_base64 = self.webhook_key
90+
91+
verify_key = VerifyKey(base64.b64decode(verify_key_base64))
92+
93+
message = request.body
94+
95+
try:
96+
verify_key.verify(message, signature)
97+
except (CryptoError, ValueError):
98+
raise AnymailWebhookValidationFailure(
99+
"MailPace webhook called with incorrect signature"
100+
)
94101

95102
def esp_to_anymail_event(self, esp_event):
96103
event_type = self.event_record_types.get(esp_event["event"], EventType.UNKNOWN)

tests/test_mailpace_webhooks.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import json
2+
import unittest
23
from base64 import b64encode
34
from unittest.mock import ANY
45

56
from django.test import tag
67

78
from anymail.signals import AnymailTrackingEvent
89
from anymail.webhooks.mailpace import MailPaceTrackingWebhookView
9-
from tests.utils import ClientWithCsrfChecks
1010

1111
from .utils_mailpace import ClientWithMailPaceSignature, make_key
1212
from .webhook_cases import WebhookTestCase
1313

1414
# These tests are triggered both with and without 'pynacl' installed,
15-
# if pynacl is unavailable, we use the ClientWithCsrfChecks class
15+
# without the ability to generate a signing key, there is no way to test
16+
# the webhook signature validation.
1617
try:
1718
from nacl.signing import SigningKey
1819

@@ -22,13 +23,13 @@
2223

2324

2425
@tag("mailpace")
26+
@unittest.skipUnless(PYNACL_INSTALLED, "pynacl is not installed")
2527
class MailPaceWebhookSecurityTestCase(WebhookTestCase):
2628
client_class = ClientWithMailPaceSignature
2729

2830
def setUp(self):
2931
super().setUp()
3032
self.clear_basic_auth()
31-
3233
self.client.set_private_key(make_key())
3334

3435
def test_failed_signature_check(self):
@@ -58,16 +59,13 @@ def test_failed_signature_check(self):
5859

5960

6061
@tag("mailpace")
62+
@unittest.skipUnless(PYNACL_INSTALLED, "pynacl is not installed")
6163
class MailPaceDeliveryTestCase(WebhookTestCase):
62-
if PYNACL_INSTALLED:
63-
client_class = ClientWithMailPaceSignature
64-
else:
65-
client_class = ClientWithCsrfChecks
64+
client_class = ClientWithMailPaceSignature
6665

6766
def setUp(self):
6867
super().setUp()
6968
self.clear_basic_auth()
70-
7169
self.client.set_private_key(make_key())
7270

7371
def test_queued_event(self):

tests/utils_mailpace.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,16 @@ def post(self, *args, **kwargs):
5050

5151
webhook_key = derive_public_webhook_key(self.private_key)
5252
with override_settings(ANYMAIL={"MAILPACE_WEBHOOK_KEY": webhook_key}):
53-
return super().post(*args, **kwargs)
53+
# Django 4.2+ test Client allows headers=headers;
54+
# before that, must convert to HTTP_ args:
55+
return super().post(
56+
*args,
57+
**kwargs,
58+
**{
59+
f"HTTP_{header.upper().replace('-', '_')}": value
60+
for header, value in headers.items()
61+
},
62+
)
5463

5564

5665
ClientWithMailPaceSignature = _ClientWithMailPaceSignature

0 commit comments

Comments
 (0)