move webhook receiver logic

This commit is contained in:
Matthias 2021-10-18 22:39:08 +02:00
parent 20bb2d438e
commit b36a1d47e1
No known key found for this signature in database
GPG Key ID: F50EF5741D33E076
3 changed files with 90 additions and 89 deletions

View File

@ -6,10 +6,6 @@ Provides a JSON API for common components.
from __future__ import unicode_literals
import json
import hmac
import hashlib
import base64
from secrets import compare_digest
from django.utils.decorators import method_decorator
from django.urls import path
@ -17,7 +13,7 @@ from django.views.decorators.csrf import csrf_exempt
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.exceptions import PermissionDenied, NotFound, NotAcceptable
from rest_framework.exceptions import NotAcceptable, NotFound
from django_q.tasks import async_task
from .models import WebhookEndpoint, WebhookMessage
@ -33,12 +29,6 @@ class CsrfExemptMixin(object):
return super(CsrfExemptMixin, self).dispatch(*args, **kwargs)
class VerificationMethod:
NONE = 0
TOKEN = 1
HMAC = 2
class WebhookView(CsrfExemptMixin, APIView):
"""
Endpoint for receiving webhooks.
@ -48,17 +38,9 @@ class WebhookView(CsrfExemptMixin, APIView):
model_class = WebhookEndpoint
run_async = False
# Token
TOKEN_NAME = "Token"
VERIFICATION_METHOD = VerificationMethod.NONE
MESSAGE_OK = "Message was received."
MESSAGE_TOKEN_ERROR = "Incorrect token in header."
def post(self, request, endpoint, *args, **kwargs):
self.init(request, *args, **kwargs)
# get webhook definition
self.get_webhook(endpoint, *args, **kwargs)
self._get_webhook(endpoint, request, *args, **kwargs)
# check headers
headers = request.headers
@ -68,89 +50,34 @@ class WebhookView(CsrfExemptMixin, APIView):
raise NotAcceptable(error.msg)
# validate
self.validate_token(payload, headers, request)
self.webhook.validate_token(payload, headers, request)
# process data
message = self.save_data(payload, headers, request)
message = self.webhook.save_data(payload, headers, request)
if self.run_async:
async_task(self._process_payload, message.id)
else:
message.worked_on = self.process_payload(message, payload, headers)
message.worked_on = self.webhook.process_payload(message, payload, headers)
message.save()
# return results
return_kwargs = self.get_result(payload, headers, request)
return_kwargs = self.webhook.get_result(payload, headers, request)
return Response(**return_kwargs)
def _process_payload(self, message_id):
message = WebhookMessage.objects.get(message_id=message_id)
process_result = self.process_payload(message, message.body, message.header)
process_result = self.webhook.process_payload(message, message.body, message.header)
message.worked_on = process_result
message.save()
# To be overridden
def init(self, request, *args, **kwargs):
self.token = ''
self.secret = ''
self.verify = self.VERIFICATION_METHOD
def get_webhook(self, endpoint):
def _get_webhook(self, endpoint, request, *args, **kwargs):
try:
webhook = self.model_class.objects.get(endpoint_id=endpoint)
self.webhook = webhook
return self.process_webhook()
self.webhook.init(request, *args, **kwargs)
return self.webhook.process_webhook()
except self.model_class.DoesNotExist:
raise NotFound()
def process_webhook(self):
if self.webhook.token:
self.token = self.webhook.token
self.verify = VerificationMethod.TOKEN
# TODO make a object-setting
if self.webhook.secret:
self.secret = self.webhook.secret
self.verify = VerificationMethod.HMAC
# TODO make a object-setting
return True
def validate_token(self, payload, headers, request):
token = headers.get(self.TOKEN_NAME, "")
# no token
if self.verify == VerificationMethod.NONE:
pass
# static token
elif self.verify == VerificationMethod.TOKEN:
if not compare_digest(token, self.token):
raise PermissionDenied(self.MESSAGE_TOKEN_ERROR)
# hmac token
elif self.verify == VerificationMethod.HMAC:
digest = hmac.new(self.secret.encode('utf-8'), request.body, hashlib.sha256).digest()
computed_hmac = base64.b64encode(digest)
if not hmac.compare_digest(computed_hmac, token.encode('utf-8')):
raise PermissionDenied(self.MESSAGE_TOKEN_ERROR)
return True
def save_data(self, payload, headers=None, request=None):
return WebhookMessage.objects.create(
# host=request.host,
# TODO fix
header=headers,
body=payload,
endpoint=self.webhook,
)
def process_payload(self, message, payload=None, headers=None):
return True
def get_result(self, payload, headers=None, request=None):
context = {}
context['data'] = {'message': self.MESSAGE_OK}
context['status'] = 200
return context
common_api_urls = [
path('webhook/<slug:endpoint>/', WebhookView.as_view(), name='api-webhook'),

View File

@ -10,6 +10,10 @@ import os
import decimal
import math
import uuid
import hmac
import hashlib
import base64
from secrets import compare_digest
from django.db import models, transaction
from django.contrib.auth.models import User, Group
@ -20,6 +24,8 @@ from djmoney.settings import CURRENCY_CHOICES
from djmoney.contrib.exchange.models import convert_money
from djmoney.contrib.exchange.exceptions import MissingRate
from rest_framework.exceptions import PermissionDenied
from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator, URLValidator
from django.core.exceptions import ValidationError
@ -1248,6 +1254,12 @@ class ColorTheme(models.Model):
return False
class VerificationMethod:
NONE = 0
TOKEN = 1
HMAC = 2
class WebhookEndpoint(models.Model):
""" Defines a Webhook entdpoint
@ -1260,6 +1272,13 @@ class WebhookEndpoint(models.Model):
secret: Shared secret for HMAC verification,
"""
# Token
TOKEN_NAME = "Token"
VERIFICATION_METHOD = VerificationMethod.NONE
MESSAGE_OK = "Message was received."
MESSAGE_TOKEN_ERROR = "Incorrect token in header."
endpoint_id = models.CharField(
max_length=255,
verbose_name=_('Endpoint'),
@ -1304,6 +1323,61 @@ class WebhookEndpoint(models.Model):
help_text=_('Shared secret for HMAC'),
)
# To be overridden
def init(self, request, *args, **kwargs):
self.verify = self.VERIFICATION_METHOD
def process_webhook(self):
if self.token:
self.token = self.token
self.verify = VerificationMethod.TOKEN
# TODO make a object-setting
if self.secret:
self.secret = self.secret
self.verify = VerificationMethod.HMAC
# TODO make a object-setting
return True
def validate_token(self, payload, headers, request):
token = headers.get(self.TOKEN_NAME, "")
# no token
if self.verify == VerificationMethod.NONE:
pass
# static token
elif self.verify == VerificationMethod.TOKEN:
if not compare_digest(token, self.token):
raise PermissionDenied(self.MESSAGE_TOKEN_ERROR)
# hmac token
elif self.verify == VerificationMethod.HMAC:
digest = hmac.new(self.secret.encode('utf-8'), request.body, hashlib.sha256).digest()
computed_hmac = base64.b64encode(digest)
if not hmac.compare_digest(computed_hmac, token.encode('utf-8')):
raise PermissionDenied(self.MESSAGE_TOKEN_ERROR)
return True
def save_data(self, payload, headers=None, request=None):
return WebhookMessage.objects.create(
# host=request.host,
# TODO fix
header=headers,
body=payload,
endpoint=self,
)
def process_payload(self, message, payload=None, headers=None):
return True
def get_result(self, payload, headers=None, request=None):
context = {}
context['data'] = {'message': self.MESSAGE_OK}
context['status'] = 200
return context
class WebhookMessage(models.Model):
""" Defines a webhook message

View File

@ -109,7 +109,7 @@ class WebhookMessageTests(TestCase):
assert response.status_code == HTTPStatus.FORBIDDEN
assert (
json.loads(response.content)['detail'] == WebhookView.MESSAGE_TOKEN_ERROR
json.loads(response.content)['detail'] == WebhookView.model_class.MESSAGE_TOKEN_ERROR
)
def test_bad_token(self):
@ -120,7 +120,7 @@ class WebhookMessageTests(TestCase):
)
assert response.status_code == HTTPStatus.FORBIDDEN
assert (json.loads(response.content)['detail'] == WebhookView.MESSAGE_TOKEN_ERROR)
assert (json.loads(response.content)['detail'] == WebhookView.model_class.MESSAGE_TOKEN_ERROR)
def test_bad_url(self):
response = self.client.post(
@ -155,7 +155,7 @@ class WebhookMessageTests(TestCase):
)
assert response.status_code == HTTPStatus.OK
assert json.loads(response.content)['message'] == WebhookView.MESSAGE_OK
assert json.loads(response.content)['message'] == WebhookView.model_class.MESSAGE_OK
def test_bad_hmac(self):
# delete token
@ -170,7 +170,7 @@ class WebhookMessageTests(TestCase):
)
assert response.status_code == HTTPStatus.FORBIDDEN
assert (json.loads(response.content)['detail'] == WebhookView.MESSAGE_TOKEN_ERROR)
assert (json.loads(response.content)['detail'] == WebhookView.model_class.MESSAGE_TOKEN_ERROR)
def test_success_hmac(self):
# delete token
@ -186,7 +186,7 @@ class WebhookMessageTests(TestCase):
)
assert response.status_code == HTTPStatus.OK
assert json.loads(response.content)['message'] == WebhookView.MESSAGE_OK
assert json.loads(response.content)['message'] == WebhookView.model_class.MESSAGE_OK
def test_success(self):
response = self.client.post(
@ -197,6 +197,6 @@ class WebhookMessageTests(TestCase):
)
assert response.status_code == HTTPStatus.OK
assert json.loads(response.content)['message'] == WebhookView.MESSAGE_OK
assert json.loads(response.content)['message'] == WebhookView.model_class.MESSAGE_OK
message = WebhookMessage.objects.get()
assert message.body == {"this": "is a message"}