mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
initial webhook view #2036
This commit is contained in:
parent
71ca60d679
commit
f600083dee
@ -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'),
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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'),
|
||||
]
|
||||
|
28
InvenTree/common/migrations/0012_webhookendpoint.py
Normal file
28
InvenTree/common/migrations/0012_webhookendpoint.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
@ -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,
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user