Fixes #6776: Fix erroneous webhook dispatch on failure to save objects

This commit is contained in:
jeremystretch 2021-08-23 12:06:43 -04:00
parent 75c62ff729
commit 0b0ab9277c
4 changed files with 33 additions and 4 deletions

View File

@ -14,6 +14,7 @@
* [#5968](https://github.com/netbox-community/netbox/issues/5968) - Model forms should save empty custom field values as null * [#5968](https://github.com/netbox-community/netbox/issues/5968) - Model forms should save empty custom field values as null
* [#6686](https://github.com/netbox-community/netbox/issues/6686) - Force assignment of null custom field values to objects * [#6686](https://github.com/netbox-community/netbox/issues/6686) - Force assignment of null custom field values to objects
* [#6776](https://github.com/netbox-community/netbox/issues/6776) - Fix erroneous webhook dispatch on failure to save objects
* [#6974](https://github.com/netbox-community/netbox/issues/6974) - Show contextual label for IP address role * [#6974](https://github.com/netbox-community/netbox/issues/6974) - Show contextual label for IP address role
* [#7012](https://github.com/netbox-community/netbox/issues/7012) - Fix hidden "add components" dropdown on devices list * [#7012](https://github.com/netbox-community/netbox/issues/7012) - Fix hidden "add components" dropdown on devices list

View File

@ -2,7 +2,7 @@ from contextlib import contextmanager
from django.db.models.signals import m2m_changed, pre_delete, post_save from django.db.models.signals import m2m_changed, pre_delete, post_save
from extras.signals import _handle_changed_object, _handle_deleted_object from extras.signals import clear_webhooks, _clear_webhook_queue, _handle_changed_object, _handle_deleted_object
from utilities.utils import curry from utilities.utils import curry
from .webhooks import flush_webhooks from .webhooks import flush_webhooks
@ -20,11 +20,13 @@ def change_logging(request):
# Curry signals receivers to pass the current request # Curry signals receivers to pass the current request
handle_changed_object = curry(_handle_changed_object, request, webhook_queue) handle_changed_object = curry(_handle_changed_object, request, webhook_queue)
handle_deleted_object = curry(_handle_deleted_object, request, webhook_queue) handle_deleted_object = curry(_handle_deleted_object, request, webhook_queue)
clear_webhook_queue = curry(_clear_webhook_queue, webhook_queue)
# Connect our receivers to the post_save and post_delete signals. # Connect our receivers to the post_save and post_delete signals.
post_save.connect(handle_changed_object, dispatch_uid='handle_changed_object') post_save.connect(handle_changed_object, dispatch_uid='handle_changed_object')
m2m_changed.connect(handle_changed_object, dispatch_uid='handle_changed_object') m2m_changed.connect(handle_changed_object, dispatch_uid='handle_changed_object')
pre_delete.connect(handle_deleted_object, dispatch_uid='handle_deleted_object') pre_delete.connect(handle_deleted_object, dispatch_uid='handle_deleted_object')
clear_webhooks.connect(clear_webhook_queue, dispatch_uid='clear_webhook_queue')
yield yield
@ -33,6 +35,7 @@ def change_logging(request):
post_save.disconnect(handle_changed_object, dispatch_uid='handle_changed_object') post_save.disconnect(handle_changed_object, dispatch_uid='handle_changed_object')
m2m_changed.disconnect(handle_changed_object, dispatch_uid='handle_changed_object') m2m_changed.disconnect(handle_changed_object, dispatch_uid='handle_changed_object')
pre_delete.disconnect(handle_deleted_object, dispatch_uid='handle_deleted_object') pre_delete.disconnect(handle_deleted_object, dispatch_uid='handle_deleted_object')
clear_webhooks.disconnect(clear_webhook_queue, dispatch_uid='clear_webhook_queue')
# Flush queued webhooks to RQ # Flush queued webhooks to RQ
flush_webhooks(webhook_queue) flush_webhooks(webhook_queue)

View File

@ -1,3 +1,4 @@
import logging
import random import random
from datetime import timedelta from datetime import timedelta
@ -6,6 +7,7 @@ from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import DEFAULT_DB_ALIAS from django.db import DEFAULT_DB_ALIAS
from django.db.models.signals import m2m_changed, post_save, pre_delete from django.db.models.signals import m2m_changed, post_save, pre_delete
from django.dispatch import Signal
from django.utils import timezone from django.utils import timezone
from django_prometheus.models import model_deletes, model_inserts, model_updates from django_prometheus.models import model_deletes, model_inserts, model_updates
from prometheus_client import Counter from prometheus_client import Counter
@ -19,6 +21,10 @@ from .webhooks import enqueue_object, get_snapshots, serialize_for_webhook
# Change logging/webhooks # Change logging/webhooks
# #
# Define a custom signal that can be sent to clear any queued webhooks
clear_webhooks = Signal()
def _handle_changed_object(request, webhook_queue, sender, instance, **kwargs): def _handle_changed_object(request, webhook_queue, sender, instance, **kwargs):
""" """
Fires when an object is created or updated. Fires when an object is created or updated.
@ -104,6 +110,16 @@ def _handle_deleted_object(request, webhook_queue, sender, instance, **kwargs):
model_deletes.labels(instance._meta.model_name).inc() model_deletes.labels(instance._meta.model_name).inc()
def _clear_webhook_queue(webhook_queue, sender, **kwargs):
"""
Delete any queued webhooks (e.g. because of an aborted bulk transaction)
"""
logger = logging.getLogger('webhooks')
logger.info(f"Clearing {len(webhook_queue)} queued webhooks ({sender})")
webhook_queue.clear()
# #
# Custom fields # Custom fields
# #

View File

@ -17,6 +17,7 @@ from django.views.generic import View
from django_tables2.export import TableExport from django_tables2.export import TableExport
from extras.models import CustomField, ExportTemplate from extras.models import CustomField, ExportTemplate
from extras.signals import clear_webhooks
from utilities.error_handlers import handle_protectederror from utilities.error_handlers import handle_protectederror
from utilities.exceptions import AbortTransaction, PermissionsViolation from utilities.exceptions import AbortTransaction, PermissionsViolation
from utilities.forms import ( from utilities.forms import (
@ -325,6 +326,7 @@ class ObjectEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
msg = "Object save failed due to object-level permissions violation" msg = "Object save failed due to object-level permissions violation"
logger.debug(msg) logger.debug(msg)
form.add_error(None, msg) form.add_error(None, msg)
clear_webhooks.send(sender=self)
else: else:
logger.debug("Form validation failed") logger.debug("Form validation failed")
@ -603,12 +605,13 @@ class ObjectImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
raise ObjectDoesNotExist raise ObjectDoesNotExist
except AbortTransaction: except AbortTransaction:
pass clear_webhooks.send(sender=self)
except PermissionsViolation: except PermissionsViolation:
msg = "Object creation failed due to object-level permissions violation" msg = "Object creation failed due to object-level permissions violation"
logger.debug(msg) logger.debug(msg)
form.add_error(None, msg) form.add_error(None, msg)
clear_webhooks.send(sender=self)
if not model_form.errors: if not model_form.errors:
logger.info(f"Import object {obj} (PK: {obj.pk})") logger.info(f"Import object {obj} (PK: {obj.pk})")
@ -751,12 +754,13 @@ class BulkImportView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
}) })
except ValidationError: except ValidationError:
pass clear_webhooks.send(sender=self)
except PermissionsViolation: except PermissionsViolation:
msg = "Object import failed due to object-level permissions violation" msg = "Object import failed due to object-level permissions violation"
logger.debug(msg) logger.debug(msg)
form.add_error(None, msg) form.add_error(None, msg)
clear_webhooks.send(sender=self)
else: else:
logger.debug("Form validation failed") logger.debug("Form validation failed")
@ -879,11 +883,13 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
except ValidationError as e: except ValidationError as e:
messages.error(self.request, "{} failed validation: {}".format(obj, e)) messages.error(self.request, "{} failed validation: {}".format(obj, e))
clear_webhooks.send(sender=self)
except PermissionsViolation: except PermissionsViolation:
msg = "Object update failed due to object-level permissions violation" msg = "Object update failed due to object-level permissions violation"
logger.debug(msg) logger.debug(msg)
form.add_error(None, msg) form.add_error(None, msg)
clear_webhooks.send(sender=self)
else: else:
logger.debug("Form validation failed") logger.debug("Form validation failed")
@ -987,6 +993,7 @@ class BulkRenameView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
msg = "Object update failed due to object-level permissions violation" msg = "Object update failed due to object-level permissions violation"
logger.debug(msg) logger.debug(msg)
form.add_error(None, msg) form.add_error(None, msg)
clear_webhooks.send(sender=self)
else: else:
form = self.form(initial={'pk': request.POST.getlist('pk')}) form = self.form(initial={'pk': request.POST.getlist('pk')})
@ -1183,6 +1190,7 @@ class ComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View
msg = "Component creation failed due to object-level permissions violation" msg = "Component creation failed due to object-level permissions violation"
logger.debug(msg) logger.debug(msg)
form.add_error(None, msg) form.add_error(None, msg)
clear_webhooks.send(sender=self)
return render(request, self.template_name, { return render(request, self.template_name, {
'component_type': self.queryset.model._meta.verbose_name, 'component_type': self.queryset.model._meta.verbose_name,
@ -1264,12 +1272,13 @@ class BulkComponentCreateView(GetReturnURLMixin, ObjectPermissionRequiredMixin,
raise PermissionsViolation raise PermissionsViolation
except IntegrityError: except IntegrityError:
pass clear_webhooks.send(sender=self)
except PermissionsViolation: except PermissionsViolation:
msg = "Component creation failed due to object-level permissions violation" msg = "Component creation failed due to object-level permissions violation"
logger.debug(msg) logger.debug(msg)
form.add_error(None, msg) form.add_error(None, msg)
clear_webhooks.send(sender=self)
if not form.errors: if not form.errors:
msg = "Added {} {} to {} {}.".format( msg = "Added {} {} to {} {}.".format(