mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 12:06:44 +00:00
move webhook receiver logic
This commit is contained in:
parent
20bb2d438e
commit
b36a1d47e1
@ -6,10 +6,6 @@ Provides a JSON API for common components.
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import hmac
|
|
||||||
import hashlib
|
|
||||||
import base64
|
|
||||||
from secrets import compare_digest
|
|
||||||
|
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.urls import path
|
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.views import APIView
|
||||||
from rest_framework.response import Response
|
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 django_q.tasks import async_task
|
||||||
|
|
||||||
from .models import WebhookEndpoint, WebhookMessage
|
from .models import WebhookEndpoint, WebhookMessage
|
||||||
@ -33,12 +29,6 @@ class CsrfExemptMixin(object):
|
|||||||
return super(CsrfExemptMixin, self).dispatch(*args, **kwargs)
|
return super(CsrfExemptMixin, self).dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class VerificationMethod:
|
|
||||||
NONE = 0
|
|
||||||
TOKEN = 1
|
|
||||||
HMAC = 2
|
|
||||||
|
|
||||||
|
|
||||||
class WebhookView(CsrfExemptMixin, APIView):
|
class WebhookView(CsrfExemptMixin, APIView):
|
||||||
"""
|
"""
|
||||||
Endpoint for receiving webhooks.
|
Endpoint for receiving webhooks.
|
||||||
@ -48,17 +38,9 @@ class WebhookView(CsrfExemptMixin, APIView):
|
|||||||
model_class = WebhookEndpoint
|
model_class = WebhookEndpoint
|
||||||
run_async = False
|
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):
|
def post(self, request, endpoint, *args, **kwargs):
|
||||||
self.init(request, *args, **kwargs)
|
|
||||||
# get webhook definition
|
# get webhook definition
|
||||||
self.get_webhook(endpoint, *args, **kwargs)
|
self._get_webhook(endpoint, request, *args, **kwargs)
|
||||||
|
|
||||||
# check headers
|
# check headers
|
||||||
headers = request.headers
|
headers = request.headers
|
||||||
@ -68,89 +50,34 @@ class WebhookView(CsrfExemptMixin, APIView):
|
|||||||
raise NotAcceptable(error.msg)
|
raise NotAcceptable(error.msg)
|
||||||
|
|
||||||
# validate
|
# validate
|
||||||
self.validate_token(payload, headers, request)
|
self.webhook.validate_token(payload, headers, request)
|
||||||
# process data
|
# process data
|
||||||
message = self.save_data(payload, headers, request)
|
message = self.webhook.save_data(payload, headers, request)
|
||||||
if self.run_async:
|
if self.run_async:
|
||||||
async_task(self._process_payload, message.id)
|
async_task(self._process_payload, message.id)
|
||||||
else:
|
else:
|
||||||
message.worked_on = self.process_payload(message, payload, headers)
|
message.worked_on = self.webhook.process_payload(message, payload, headers)
|
||||||
message.save()
|
message.save()
|
||||||
|
|
||||||
# return results
|
# return results
|
||||||
return_kwargs = self.get_result(payload, headers, request)
|
return_kwargs = self.webhook.get_result(payload, headers, request)
|
||||||
return Response(**return_kwargs)
|
return Response(**return_kwargs)
|
||||||
|
|
||||||
def _process_payload(self, message_id):
|
def _process_payload(self, message_id):
|
||||||
message = WebhookMessage.objects.get(message_id=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.worked_on = process_result
|
||||||
message.save()
|
message.save()
|
||||||
|
|
||||||
# To be overridden
|
def _get_webhook(self, endpoint, request, *args, **kwargs):
|
||||||
def init(self, request, *args, **kwargs):
|
|
||||||
self.token = ''
|
|
||||||
self.secret = ''
|
|
||||||
self.verify = self.VERIFICATION_METHOD
|
|
||||||
|
|
||||||
def get_webhook(self, endpoint):
|
|
||||||
try:
|
try:
|
||||||
webhook = self.model_class.objects.get(endpoint_id=endpoint)
|
webhook = self.model_class.objects.get(endpoint_id=endpoint)
|
||||||
self.webhook = webhook
|
self.webhook = webhook
|
||||||
return self.process_webhook()
|
self.webhook.init(request, *args, **kwargs)
|
||||||
|
return self.webhook.process_webhook()
|
||||||
except self.model_class.DoesNotExist:
|
except self.model_class.DoesNotExist:
|
||||||
raise NotFound()
|
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 = [
|
common_api_urls = [
|
||||||
path('webhook/<slug:endpoint>/', WebhookView.as_view(), name='api-webhook'),
|
path('webhook/<slug:endpoint>/', WebhookView.as_view(), name='api-webhook'),
|
||||||
|
@ -10,6 +10,10 @@ import os
|
|||||||
import decimal
|
import decimal
|
||||||
import math
|
import math
|
||||||
import uuid
|
import uuid
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
import base64
|
||||||
|
from secrets import compare_digest
|
||||||
|
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.contrib.auth.models import User, Group
|
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.models import convert_money
|
||||||
from djmoney.contrib.exchange.exceptions import MissingRate
|
from djmoney.contrib.exchange.exceptions import MissingRate
|
||||||
|
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.core.validators import MinValueValidator, URLValidator
|
from django.core.validators import MinValueValidator, URLValidator
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@ -1248,6 +1254,12 @@ class ColorTheme(models.Model):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class VerificationMethod:
|
||||||
|
NONE = 0
|
||||||
|
TOKEN = 1
|
||||||
|
HMAC = 2
|
||||||
|
|
||||||
|
|
||||||
class WebhookEndpoint(models.Model):
|
class WebhookEndpoint(models.Model):
|
||||||
""" Defines a Webhook entdpoint
|
""" Defines a Webhook entdpoint
|
||||||
|
|
||||||
@ -1260,6 +1272,13 @@ class WebhookEndpoint(models.Model):
|
|||||||
secret: Shared secret for HMAC verification,
|
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(
|
endpoint_id = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name=_('Endpoint'),
|
verbose_name=_('Endpoint'),
|
||||||
@ -1304,6 +1323,61 @@ class WebhookEndpoint(models.Model):
|
|||||||
help_text=_('Shared secret for HMAC'),
|
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):
|
class WebhookMessage(models.Model):
|
||||||
""" Defines a webhook message
|
""" Defines a webhook message
|
||||||
|
@ -109,7 +109,7 @@ class WebhookMessageTests(TestCase):
|
|||||||
|
|
||||||
assert response.status_code == HTTPStatus.FORBIDDEN
|
assert response.status_code == HTTPStatus.FORBIDDEN
|
||||||
assert (
|
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):
|
def test_bad_token(self):
|
||||||
@ -120,7 +120,7 @@ class WebhookMessageTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == HTTPStatus.FORBIDDEN
|
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):
|
def test_bad_url(self):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@ -155,7 +155,7 @@ class WebhookMessageTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == HTTPStatus.OK
|
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):
|
def test_bad_hmac(self):
|
||||||
# delete token
|
# delete token
|
||||||
@ -170,7 +170,7 @@ class WebhookMessageTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == HTTPStatus.FORBIDDEN
|
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):
|
def test_success_hmac(self):
|
||||||
# delete token
|
# delete token
|
||||||
@ -186,7 +186,7 @@ class WebhookMessageTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == HTTPStatus.OK
|
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):
|
def test_success(self):
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
@ -197,6 +197,6 @@ class WebhookMessageTests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == HTTPStatus.OK
|
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()
|
message = WebhookMessage.objects.get()
|
||||||
assert message.body == {"this": "is a message"}
|
assert message.body == {"this": "is a message"}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user