mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 15:15:42 +00:00 
			
		
		
		
	initial webhook view #2036
This commit is contained in:
		@@ -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,
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user