From b36a1d47e1651f85f53161ac9c53e0d423a93ba6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 18 Oct 2021 22:39:08 +0200 Subject: [PATCH] move webhook receiver logic --- InvenTree/common/api.py | 93 ++++---------------------------------- InvenTree/common/models.py | 74 ++++++++++++++++++++++++++++++ InvenTree/common/tests.py | 12 ++--- 3 files changed, 90 insertions(+), 89 deletions(-) diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index f2479b4a2a..9624d85a9d 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -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//', WebhookView.as_view(), name='api-webhook'), diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index e99940aa64..023e4e7c18 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -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 diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index 8121b46a6a..8a28d95bed 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -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"}