mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-05 06:48:48 +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 = [
|
apipatterns = [
|
||||||
url(r'^barcode/', include(barcode_api_urls)),
|
url(r'^barcode/', include(barcode_api_urls)),
|
||||||
url(r'^common/', include(common_api_urls)),
|
|
||||||
url(r'^part/', include(part_api_urls)),
|
url(r'^part/', include(part_api_urls)),
|
||||||
url(r'^bom/', include(bom_api_urls)),
|
url(r'^bom/', include(bom_api_urls)),
|
||||||
url(r'^company/', include(company_api_urls)),
|
url(r'^company/', include(company_api_urls)),
|
||||||
@ -70,6 +69,9 @@ apipatterns = [
|
|||||||
# Plugin endpoints
|
# Plugin endpoints
|
||||||
url(r'^action/', ActionPluginView.as_view(), name='api-action-plugin'),
|
url(r'^action/', ActionPluginView.as_view(), name='api-action-plugin'),
|
||||||
|
|
||||||
|
# Webhook enpoint
|
||||||
|
path('', include(common_api_urls)),
|
||||||
|
|
||||||
# InvenTree information endpoint
|
# InvenTree information endpoint
|
||||||
url(r'^$', InfoView.as_view(), name='api-inventree-info'),
|
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 import_export.admin import ImportExportModelAdmin
|
||||||
|
|
||||||
from .models import InvenTreeSetting, InvenTreeUserSetting
|
from .models import InvenTreeSetting, InvenTreeUserSetting, WebhookEndpoint
|
||||||
|
|
||||||
|
|
||||||
class SettingsAdmin(ImportExportModelAdmin):
|
class SettingsAdmin(ImportExportModelAdmin):
|
||||||
@ -18,5 +18,11 @@ class UserSettingsAdmin(ImportExportModelAdmin):
|
|||||||
list_display = ('key', 'value', 'user', )
|
list_display = ('key', 'value', 'user', )
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
|
list_display = ('endpoint_id', 'name', 'active', 'user')
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(InvenTreeSetting, SettingsAdmin)
|
admin.site.register(InvenTreeSetting, SettingsAdmin)
|
||||||
admin.site.register(InvenTreeUserSetting, UserSettingsAdmin)
|
admin.site.register(InvenTreeUserSetting, UserSettingsAdmin)
|
||||||
|
admin.site.register(WebhookEndpoint, WebhookAdmin)
|
||||||
|
@ -5,5 +5,122 @@ Provides a JSON API for common components.
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
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 = [
|
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 os
|
||||||
import decimal
|
import decimal
|
||||||
import math
|
import math
|
||||||
|
import uuid
|
||||||
|
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
@ -1165,3 +1166,52 @@ class ColorTheme(models.Model):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
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…
x
Reference in New Issue
Block a user