1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Add conditions for webhooks

This commit is contained in:
jeremystretch
2021-10-22 17:15:08 -04:00
parent 7e26d92190
commit 78ecc8673c
7 changed files with 51 additions and 18 deletions

View File

@ -61,7 +61,7 @@ class WebhookSerializer(ValidatedModelSerializer):
fields = [ fields = [
'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url', 'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', 'payload_url',
'enabled', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', 'enabled', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
'ssl_verification', 'ca_file_path', 'conditions', 'ssl_verification', 'ca_file_path',
] ]

View File

@ -137,7 +137,7 @@ class WebhookBulkEditForm(BootstrapMixin, BulkEditForm):
) )
class Meta: class Meta:
nullable_fields = ['secret', 'ca_file_path'] nullable_fields = ['secret', 'conditions', 'ca_file_path']
class TagBulkEditForm(BootstrapMixin, BulkEditForm): class TagBulkEditForm(BootstrapMixin, BulkEditForm):

View File

@ -102,6 +102,7 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
('HTTP Request', ( ('HTTP Request', (
'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', 'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
)), )),
('Conditions', ('conditions',)),
('SSL', ('ssl_verification', 'ca_file_path')), ('SSL', ('ssl_verification', 'ca_file_path')),
) )
widgets = { widgets = {

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.8 on 2021-10-22 20:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('extras', '0062_clear_secrets_changelog'),
]
operations = [
migrations.AddField(
model_name='webhook',
name='conditions',
field=models.JSONField(blank=True, null=True),
),
]

View File

@ -9,11 +9,12 @@ from django.db import models
from django.http import HttpResponse from django.http import HttpResponse
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.formats import date_format, time_format from django.utils.formats import date_format
from rest_framework.utils.encoders import JSONEncoder from rest_framework.utils.encoders import JSONEncoder
from extras.choices import * from extras.choices import *
from extras.constants import * from extras.constants import *
from extras.conditions import ConditionSet
from extras.utils import extras_features, FeatureQuery, image_upload from extras.utils import extras_features, FeatureQuery, image_upload
from netbox.models import BigIDModel, ChangeLoggedModel from netbox.models import BigIDModel, ChangeLoggedModel
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
@ -107,6 +108,11 @@ class Webhook(ChangeLoggedModel):
"the secret as the key. The secret is not transmitted in " "the secret as the key. The secret is not transmitted in "
"the request." "the request."
) )
conditions = models.JSONField(
blank=True,
null=True,
help_text="A set of conditions which determine whether the webhook will be generated."
)
ssl_verification = models.BooleanField( ssl_verification = models.BooleanField(
default=True, default=True,
verbose_name='SSL verification', verbose_name='SSL verification',
@ -138,9 +144,13 @@ class Webhook(ChangeLoggedModel):
# At least one action type must be selected # At least one action type must be selected
if not self.type_create and not self.type_delete and not self.type_update: if not self.type_create and not self.type_delete and not self.type_update:
raise ValidationError( raise ValidationError("At least one type must be selected: create, update, and/or delete.")
"You must select at least one type: create, update, and/or delete."
) if self.conditions:
try:
ConditionSet(self.conditions)
except ValueError as e:
raise ValidationError({'conditions': e})
# CA file path requires SSL verification enabled # CA file path requires SSL verification enabled
if not self.ssl_verification and self.ca_file_path: if not self.ssl_verification and self.ca_file_path:

View File

@ -145,6 +145,7 @@ class WebhookTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'payload_url': 'http://example.com/?x', 'payload_url': 'http://example.com/?x',
'http_method': 'GET', 'http_method': 'GET',
'http_content_type': 'application/foo', 'http_content_type': 'application/foo',
'conditions': None,
} }
cls.csv_data = ( cls.csv_data = (

View File

@ -6,6 +6,7 @@ from django_rq import job
from jinja2.exceptions import TemplateError from jinja2.exceptions import TemplateError
from .choices import ObjectChangeActionChoices from .choices import ObjectChangeActionChoices
from .conditions import ConditionSet
from .webhooks import generate_signature from .webhooks import generate_signature
logger = logging.getLogger('netbox.webhooks_worker') logger = logging.getLogger('netbox.webhooks_worker')
@ -16,6 +17,12 @@ def process_webhook(webhook, model_name, event, data, snapshots, timestamp, user
""" """
Make a POST request to the defined Webhook Make a POST request to the defined Webhook
""" """
# Evaluate webhook conditions (if any)
if webhook.conditions:
if not ConditionSet(webhook.conditions).eval(data):
return
# Prepare context data for headers & body templates
context = { context = {
'event': dict(ObjectChangeActionChoices)[event].lower(), 'event': dict(ObjectChangeActionChoices)[event].lower(),
'timestamp': timestamp, 'timestamp': timestamp,
@ -33,14 +40,14 @@ def process_webhook(webhook, model_name, event, data, snapshots, timestamp, user
try: try:
headers.update(webhook.render_headers(context)) headers.update(webhook.render_headers(context))
except (TemplateError, ValueError) as e: except (TemplateError, ValueError) as e:
logger.error("Error parsing HTTP headers for webhook {}: {}".format(webhook, e)) logger.error(f"Error parsing HTTP headers for webhook {webhook}: {e}")
raise e raise e
# Render the request body # Render the request body
try: try:
body = webhook.render_body(context) body = webhook.render_body(context)
except TemplateError as e: except TemplateError as e:
logger.error("Error rendering request body for webhook {}: {}".format(webhook, e)) logger.error(f"Error rendering request body for webhook {webhook}: {e}")
raise e raise e
# Prepare the HTTP request # Prepare the HTTP request
@ -51,15 +58,13 @@ def process_webhook(webhook, model_name, event, data, snapshots, timestamp, user
'data': body.encode('utf8'), 'data': body.encode('utf8'),
} }
logger.info( logger.info(
"Sending {} request to {} ({} {})".format( f"Sending {params['method']} request to {params['url']} ({context['model']} {context['event']})"
params['method'], params['url'], context['model'], context['event']
)
) )
logger.debug(params) logger.debug(params)
try: try:
prepared_request = requests.Request(**params).prepare() prepared_request = requests.Request(**params).prepare()
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
logger.error("Error forming HTTP request: {}".format(e)) logger.error(f"Error forming HTTP request: {e}")
raise e raise e
# If a secret key is defined, sign the request with a hash of the key and its content # If a secret key is defined, sign the request with a hash of the key and its content
@ -74,12 +79,10 @@ def process_webhook(webhook, model_name, event, data, snapshots, timestamp, user
response = session.send(prepared_request, proxies=settings.HTTP_PROXIES) response = session.send(prepared_request, proxies=settings.HTTP_PROXIES)
if 200 <= response.status_code <= 299: if 200 <= response.status_code <= 299:
logger.info("Request succeeded; response status {}".format(response.status_code)) logger.info(f"Request succeeded; response status {response.status_code}")
return 'Status {} returned, webhook successfully processed.'.format(response.status_code) return f"Status {response.status_code} returned, webhook successfully processed."
else: else:
logger.warning("Request failed; response status {}: {}".format(response.status_code, response.content)) logger.warning(f"Request failed; response status {response.status_code}: {response.content}")
raise requests.exceptions.RequestException( raise requests.exceptions.RequestException(
"Status {} returned with content '{}', webhook FAILED to process.".format( f"Status {response.status_code} returned with content '{response.content}', webhook FAILED to process."
response.status_code, response.content
)
) )