Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 7 additions & 13 deletions src/sentry/incidents/serializers/alert_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from sentry.api.serializers.rest_framework.base import CamelSnakeModelSerializer
from sentry.api.serializers.rest_framework.environment import EnvironmentField
from sentry.api.serializers.rest_framework.project import ProjectField
from sentry.api.utils import to_valid_int_id_list
from sentry.incidents.logic import (
CRITICAL_TRIGGER_LABEL,
WARNING_TRIGGER_LABEL,
Expand All @@ -39,6 +38,7 @@
AlertRuleThresholdType,
AlertRuleTrigger,
)
from sentry.incidents.serializers.utils import validate_object_ids_belong
from sentry.incidents.utils.subscription_limits import get_max_metric_alert_subscriptions
from sentry.search.eap.trace_metrics.validator import validate_trace_metrics_aggregate
from sentry.snuba.dataset import Dataset
Expand Down Expand Up @@ -422,18 +422,12 @@ def _handle_triggers(self, alert_rule: AlertRule, triggers: list[dict[str, Any]]
if triggers is not None:
# Delete triggers we don't have present in the incoming data
raw_trigger_ids = [x["id"] for x in triggers if "id" in x]
trigger_ids = to_valid_int_id_list("triggers", raw_trigger_ids)
if trigger_ids:
existing_trigger_ids = set(
AlertRuleTrigger.objects.filter(
alert_rule=alert_rule, id__in=trigger_ids
).values_list("id", flat=True)
)
missing_ids = [tid for tid in trigger_ids if tid not in existing_trigger_ids]
if missing_ids:
raise serializers.ValidationError(
f"Trigger IDs do not belong to this alert rule: {missing_ids}"
)
trigger_ids = validate_object_ids_belong(
"triggers",
raw_trigger_ids,
AlertRuleTrigger.objects.filter(alert_rule=alert_rule),
"Trigger IDs do not belong to this alert rule, this alert rule may be incompatible with the legacy API.",
)
triggers_to_delete = AlertRuleTrigger.objects.filter(alert_rule=alert_rule).exclude(
id__in=trigger_ids
)
Expand Down
10 changes: 9 additions & 1 deletion src/sentry/incidents/serializers/alert_rule_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
update_alert_rule_trigger,
)
from sentry.incidents.models.alert_rule import AlertRuleTrigger, AlertRuleTriggerAction
from sentry.incidents.serializers.utils import validate_object_ids_belong
from sentry.workflow_engine.migration_helpers.alert_rule import (
dual_delete_migrated_alert_rule_trigger_action,
)
Expand Down Expand Up @@ -85,11 +86,18 @@ def _handle_actions(
) -> None:
channel_lookup_timeout_error = None
if actions is not None:
action_ids = validate_object_ids_belong(
"actions",
[x["id"] for x in actions if "id" in x],
AlertRuleTriggerAction.objects.filter(alert_rule_trigger=alert_rule_trigger),
"Action IDs do not belong to this trigger",
)

# Delete actions we don't have present in the updated data.
action_ids = [x["id"] for x in actions if "id" in x]
actions_to_delete = AlertRuleTriggerAction.objects.filter(
alert_rule_trigger=alert_rule_trigger
).exclude(id__in=action_ids)

for action in actions_to_delete:
with transaction.atomic(router.db_for_write(AlertRuleTriggerAction)):
dual_delete_migrated_alert_rule_trigger_action(action)
Expand Down
27 changes: 27 additions & 0 deletions src/sentry/incidents/serializers/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from collections.abc import Sequence
from typing import Any

from django.db.models import QuerySet
from rest_framework import serializers

from sentry.api.utils import to_valid_int_id_list


def validate_object_ids_belong(
name: str,
raw_ids: Sequence[str | int],
queryset: QuerySet[Any],
error_message: str,
) -> list[int]:
"""
Validate that all supplied ids are valid integers and belong to the given
(parent-scoped) queryset. Returns the validated ints. Raises a
serializers.ValidationError (-> HTTP 400) listing any ids that don't belong.
"""
ids = to_valid_int_id_list(name, raw_ids)
if ids:
existing = set(queryset.filter(id__in=ids).values_list("id", flat=True))
missing = [i for i in ids if i not in existing]
if missing:
raise serializers.ValidationError(f"{error_message}: {missing}")
return ids
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,27 @@ def test_update_trigger_id_not_belonging_to_alert_rule(self) -> None:
)
assert "do not belong to this alert rule" in str(resp.data)

def test_update_action_id_not_belonging_to_trigger(self) -> None:
self.create_member(
user=self.user, organization=self.organization, role="owner", teams=[self.team]
)
self.login_as(self.user)

alert_rule = self.alert_rule

serialized_alert_rule = self.get_serialized_alert_rule()
# Point the critical trigger's action at an action that belongs to the
# warning trigger. The trigger ids are still valid for this alert rule, so
# the (foreign-to-this-trigger) action id is what should be rejected.
foreign_action_id = serialized_alert_rule["triggers"][1]["actions"][0]["id"]
serialized_alert_rule["triggers"][0]["actions"][0]["id"] = foreign_action_id

with self.feature("organizations:incidents"):
resp = self.get_error_response(
self.organization.slug, alert_rule.id, status_code=400, **serialized_alert_rule
)
assert "do not belong to this trigger" in str(resp.data)

def test_update_trigger_alert_threshold(self) -> None:
self.create_member(
user=self.user, organization=self.organization, role="owner", teams=[self.team]
Expand Down
Loading