initial webhook view #2036

This commit is contained in:
Matthias 2021-09-12 16:14:06 +02:00
parent 71ca60d679
commit f600083dee
No known key found for this signature in database
GPG Key ID: F50EF5741D33E076
5 changed files with 205 additions and 2 deletions

View File

@ -54,7 +54,6 @@ admin.site.site_header = "InvenTree Admin"
apipatterns = [
url(r'^barcode/', include(barcode_api_urls)),
url(r'^common/', include(common_api_urls)),
url(r'^part/', include(part_api_urls)),
url(r'^bom/', include(bom_api_urls)),
url(r'^company/', include(company_api_urls)),
@ -70,6 +69,9 @@ apipatterns = [
# Plugin endpoints
url(r'^action/', ActionPluginView.as_view(), name='api-action-plugin'),
# Webhook enpoint
path('', include(common_api_urls)),
# InvenTree information endpoint
url(r'^$', InfoView.as_view(), name='api-inventree-info'),

View File

@ -5,7 +5,7 @@ from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from .models import InvenTreeSetting, InvenTreeUserSetting
from .models import InvenTreeSetting, InvenTreeUserSetting, WebhookEndpoint
class SettingsAdmin(ImportExportModelAdmin):
@ -18,5 +18,11 @@ class UserSettingsAdmin(ImportExportModelAdmin):
list_display = ('key', 'value', 'user', )
class WebhookAdmin(ImportExportModelAdmin):
list_display = ('endpoint_id', 'name', 'active', 'user')
admin.site.register(InvenTreeSetting, SettingsAdmin)
admin.site.register(InvenTreeUserSetting, UserSettingsAdmin)
admin.site.register(WebhookEndpoint, WebhookAdmin)

View File

@ -5,5 +5,122 @@ Provides a JSON API for common components.
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import json
from secrets import compare_digest
from django.utils.decorators import method_decorator
from django.urls import path
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 .models import WebhookEndpoint
class CsrfExemptMixin(object):
"""
Exempts the view from CSRF requirements.
"""
@method_decorator(csrf_exempt)
def dispatch(self, *args, **kwargs):
return super(CsrfExemptMixin, self).dispatch(*args, **kwargs)
class VerificationMethod:
NONE = 0
TOKEN = 1
HMAC = 2
class WebhookView(CsrfExemptMixin, APIView):
"""
Endpoint for receiving webhoks.
"""
authentication_classes = []
permission_classes = []
model_class = WebhookEndpoint
# 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)
# check headers
headers = request.headers
self.validate_token(headers)
# process data
try:
payload = json.loads(request.body)
except json.decoder.JSONDecodeError as error:
raise NotAcceptable(error.msg)
self.save_data(payload, headers, request)
self.process_payload(payload, headers, request)
# return results
return_kwargs = self.get_result(payload, headers, request)
return Response(**return_kwargs)
# To be overridden
def init(self, request, *args, **kwargs):
self.token = ''
self.verify = self.VERIFICATION_METHOD
def get_webhook(self, endpoint):
try:
webhook = self.model_class.objects.get(endpoint_id=endpoint)
self.webhook = webhook
return self.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
return True
def validate_token(self, headers):
token = headers.get(self.TOKEN_NAME, "")
# no token
if self.verify == VerificationMethod.NONE:
return True
# static token
elif self.verify == VerificationMethod.TOKEN:
if not compare_digest(token, self.token):
raise PermissionDenied(self.MESSAGE_TOKEN_ERROR)
return True
# hmac token
elif self.verify == VerificationMethod.HMAC:
# TODO write check
return True
def save_data(self, payload, headers=None, request=None):
# TODO safe data
return
def process_payload(self, payload, headers=None, request=None):
return
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

@ -0,0 +1,28 @@
# Generated by Django 3.2.4 on 2021-09-12 12:42
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('common', '0011_auto_20210722_2114'),
]
operations = [
migrations.CreateModel(
name='WebhookEndpoint',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('endpoint_id', models.CharField(default=uuid.uuid4, editable=False, help_text='Endpoint at which this webhook is received', max_length=255, verbose_name='Endpoint')),
('name', models.CharField(blank=True, help_text='Name for this webhook', max_length=255, null=True, verbose_name='Name')),
('active', models.BooleanField(default=True, help_text='Is this webhook active', verbose_name='Active')),
('token', models.CharField(blank=True, default=uuid.uuid4, help_text='Token for access', max_length=255, null=True, verbose_name='Token')),
('user', models.ForeignKey(blank=True, help_text='User', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
),
]

View File

@ -9,6 +9,7 @@ from __future__ import unicode_literals
import os
import decimal
import math
import uuid
from django.db import models, transaction
from django.contrib.auth.models import User
@ -1165,3 +1166,52 @@ class ColorTheme(models.Model):
return True
return False
class WebhookEndpoint(models.Model):
""" Defines a Webhook entdpoint
Attributes:
endpoint_id: Path to the webhook,
name: Name of the webhook,
active: Is this webhook active?,
user: User associated with webhook,
token: Token for sending a webhook,
"""
endpoint_id = models.CharField(
max_length=255,
verbose_name=_('Endpoint'),
help_text=_('Endpoint at which this webhook is received'),
default=uuid.uuid4,
editable=False,
)
name = models.CharField(
max_length=255,
blank=True, null=True,
verbose_name=_('Name'),
help_text=_('Name for this webhook')
)
active = models.BooleanField(
default=True,
verbose_name=_('Active'),
help_text=_('Is this webhook active')
)
user = models.ForeignKey(
User,
on_delete=models.SET_NULL,
blank=True, null=True,
verbose_name=_('User'),
help_text=_('User'),
)
token = models.CharField(
max_length=255,
blank=True, null=True,
verbose_name=_('Token'),
help_text=_('Token for access'),
default=uuid.uuid4,
)