mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-26 18:58:54 +02:00
Compare commits
1 Commits
main
...
21989-even
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d501f56a5f |
@@ -181,9 +181,26 @@ def process_event_rules(event_rules, object_type, event):
|
||||
if not event_rule.eval_conditions(event['data']):
|
||||
continue
|
||||
|
||||
# Guard against action_data that is valid JSON but not a dict
|
||||
# (e.g. a bare string or number). Existing rows with bad data are
|
||||
# tolerated at runtime; validation on EventRule.clean() prevents
|
||||
# new ones.
|
||||
if event_rule.action_data is None:
|
||||
action_data = {}
|
||||
elif isinstance(event_rule.action_data, dict):
|
||||
action_data = event_rule.action_data
|
||||
else:
|
||||
logger.warning(
|
||||
_('Ignoring invalid action_data on event rule "{rule}" (got {data_type})').format(
|
||||
rule=event_rule,
|
||||
data_type=type(event_rule.action_data).__name__,
|
||||
)
|
||||
)
|
||||
action_data = {}
|
||||
|
||||
# Merge rule-specific action_data with the event payload.
|
||||
# Copy to avoid mutating the rule's stored action_data dict.
|
||||
event_data = {**(event_rule.action_data or {}), **event['data']}
|
||||
event_data = {**action_data, **event['data']}
|
||||
|
||||
# Webhooks
|
||||
if event_rule.action_type == EventRuleActionChoices.WEBHOOK:
|
||||
|
||||
@@ -143,6 +143,10 @@ class EventRule(CustomFieldsMixin, ExportTemplatesMixin, OwnerMixin, TagsMixin,
|
||||
except ValueError as e:
|
||||
raise ValidationError({'conditions': e})
|
||||
|
||||
# action_data must be a JSON object (or null)
|
||||
if self.action_data is not None and not isinstance(self.action_data, dict):
|
||||
raise ValidationError({'action_data': _('Action data must be a JSON object or null.')})
|
||||
|
||||
def eval_conditions(self, data):
|
||||
"""
|
||||
Test whether the given data meets the conditions of the event rule (if any). Return True
|
||||
|
||||
@@ -563,3 +563,30 @@ class EventRuleTest(APITestCase):
|
||||
job = self.queue.get_jobs()[0]
|
||||
self.assertEqual(job.kwargs['event_type'], OBJECT_DELETED)
|
||||
self.queue.empty()
|
||||
|
||||
def test_non_dict_action_data_does_not_crash_flush(self):
|
||||
"""
|
||||
Pre-existing non-dict action_data must not cause flush_events() to
|
||||
raise.
|
||||
"""
|
||||
site_type = ObjectType.objects.get_for_model(Site)
|
||||
webhook = Webhook.objects.get(name='Webhook 1')
|
||||
webhook_type = ObjectType.objects.get_for_model(Webhook)
|
||||
|
||||
bad_rule = EventRule.objects.create(
|
||||
name='Bad action_data rule',
|
||||
event_types=[OBJECT_CREATED],
|
||||
action_type=EventRuleActionChoices.WEBHOOK,
|
||||
action_object_type=webhook_type,
|
||||
action_object_id=webhook.pk,
|
||||
action_data={},
|
||||
)
|
||||
bad_rule.object_types.set([site_type])
|
||||
|
||||
# Simulate a legacy row that predates model validation.
|
||||
EventRule.objects.filter(pk=bad_rule.pk).update(action_data='not a dict')
|
||||
|
||||
url = reverse('dcim-api:site-list')
|
||||
self.add_permissions('dcim.add_site')
|
||||
response = self.client.post(url, {'name': 'Site X', 'slug': 'site-x'}, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
||||
|
||||
@@ -11,9 +11,18 @@ from django.forms import ValidationError
|
||||
from django.test import TestCase, tag
|
||||
from PIL import Image
|
||||
|
||||
from core.events import OBJECT_CREATED
|
||||
from core.models import AutoSyncRecord, DataSource, ObjectType
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup
|
||||
from extras.models import ConfigContext, ConfigContextProfile, ConfigTemplate, ImageAttachment, Tag, TaggedItem
|
||||
from extras.models import (
|
||||
ConfigContext,
|
||||
ConfigContextProfile,
|
||||
ConfigTemplate,
|
||||
EventRule,
|
||||
ImageAttachment,
|
||||
Tag,
|
||||
TaggedItem,
|
||||
)
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.exceptions import AbortRequest
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||
@@ -889,3 +898,24 @@ class ConfigTemplateTest(TestCase):
|
||||
object_id=config_template.pk
|
||||
)
|
||||
self.assertEqual(autosync_records.count(), 0, "AutoSyncRecord should be deleted after detaching")
|
||||
|
||||
|
||||
class EventRuleTest(TestCase):
|
||||
|
||||
def test_action_data_clean_accepts_dict(self):
|
||||
"""
|
||||
clean() should accept a JSON object (or null) as action_data.
|
||||
"""
|
||||
for value in ({'key': 'value'}, None):
|
||||
rule = EventRule(name='test', event_types=[OBJECT_CREATED], action_data=value)
|
||||
rule.clean()
|
||||
|
||||
def test_action_data_clean_rejects_non_dict(self):
|
||||
"""
|
||||
clean() should reject action_data that is valid JSON but not an object (#21989).
|
||||
"""
|
||||
for value in ('test', 42, [1, 2, 3], True):
|
||||
rule = EventRule(name='test', event_types=[OBJECT_CREATED], action_data=value)
|
||||
with self.assertRaises(ValidationError) as cm:
|
||||
rule.clean()
|
||||
self.assertIn('action_data', cm.exception.message_dict)
|
||||
|
||||
Reference in New Issue
Block a user