Skip to content

Commit f14d8a2

Browse files
authored
feat(aci): add bulk delete detectors endpoint (#96810)
similar to #96791 (adding bulk delete workflows endpoint)
1 parent 92b5e1b commit f14d8a2

File tree

2 files changed

+364
-26
lines changed

2 files changed

+364
-26
lines changed

src/sentry/workflow_engine/endpoints/organization_detector_index.py

Lines changed: 106 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
from django.db.models.query import QuerySet
88
from drf_spectacular.utils import PolymorphicProxySerializer, extend_schema
99
from rest_framework import status
10-
from rest_framework.exceptions import ValidationError
10+
from rest_framework.exceptions import PermissionDenied, ValidationError
1111
from rest_framework.request import Request
1212
from rest_framework.response import Response
1313

14-
from sentry import features
14+
from sentry import audit_log, features
1515
from sentry.api.api_owners import ApiOwner
1616
from sentry.api.api_publish_status import ApiPublishStatus
1717
from sentry.api.base import region_silo_endpoint
@@ -25,10 +25,13 @@
2525
from sentry.apidocs.constants import (
2626
RESPONSE_BAD_REQUEST,
2727
RESPONSE_FORBIDDEN,
28+
RESPONSE_NO_CONTENT,
2829
RESPONSE_NOT_FOUND,
2930
RESPONSE_UNAUTHORIZED,
3031
)
3132
from sentry.apidocs.parameters import DetectorParams, GlobalParams, OrganizationParams
33+
from sentry.constants import ObjectStatus
34+
from sentry.deletions.models.scheduleddeletion import RegionScheduledDeletion
3235
from sentry.incidents.grouptype import MetricIssue
3336
from sentry.issues import grouptype
3437
from sentry.models.organization import Organization
@@ -37,12 +40,14 @@
3740
from sentry.uptime.grouptype import UptimeDomainCheckFailure
3841
from sentry.users.models.user import User
3942
from sentry.users.services.user import RpcUser
43+
from sentry.utils.audit import create_audit_entry
4044
from sentry.workflow_engine.endpoints.serializers import DetectorSerializer
4145
from sentry.workflow_engine.endpoints.utils.filters import apply_filter
4246
from sentry.workflow_engine.endpoints.utils.sortby import SortByParam
4347
from sentry.workflow_engine.endpoints.validators.base import BaseDetectorTypeValidator
4448
from sentry.workflow_engine.endpoints.validators.detector_workflow import (
4549
BulkDetectorWorkflowsValidator,
50+
can_edit_detector,
4651
)
4752
from sentry.workflow_engine.endpoints.validators.utils import get_unknown_detector_type_error
4853
from sentry.workflow_engine.models import Detector
@@ -121,51 +126,36 @@ class OrganizationDetectorIndexEndpoint(OrganizationEndpoint):
121126
publish_status = {
122127
"GET": ApiPublishStatus.EXPERIMENTAL,
123128
"POST": ApiPublishStatus.EXPERIMENTAL,
129+
"DELETE": ApiPublishStatus.EXPERIMENTAL,
124130
}
125131
owner = ApiOwner.ISSUES
126132

127133
# TODO: We probably need a specific permission for detectors. Possibly specific detectors have different perms
128134
# too?
129135
permission_classes = (OrganizationAlertRulePermission,)
130136

131-
@extend_schema(
132-
operation_id="Fetch a Project's Detectors",
133-
parameters=[
134-
GlobalParams.ORG_ID_OR_SLUG,
135-
OrganizationParams.PROJECT,
136-
DetectorParams.QUERY,
137-
DetectorParams.SORT,
138-
DetectorParams.ID,
139-
],
140-
responses={
141-
201: DetectorSerializer,
142-
400: RESPONSE_BAD_REQUEST,
143-
401: RESPONSE_UNAUTHORIZED,
144-
403: RESPONSE_FORBIDDEN,
145-
404: RESPONSE_NOT_FOUND,
146-
},
147-
)
148-
def get(self, request: Request, organization: Organization) -> Response:
137+
def filter_detectors(self, request: Request, organization) -> QuerySet[Detector]:
149138
"""
150-
List an Organization's Detectors
151-
`````````````````````````````
152-
Return a list of detectors for a given organization.
139+
Filter detectors based on the request parameters.
153140
"""
154-
if not request.user.is_authenticated:
155-
return self.respond(status=status.HTTP_401_UNAUTHORIZED)
156-
157141
projects = self.get_projects(request, organization)
158142
queryset: QuerySet[Detector] = Detector.objects.filter(
159143
project_id__in=projects,
160144
)
161145

146+
if not request.user.is_authenticated:
147+
return queryset
148+
162149
if raw_idlist := request.GET.getlist("id"):
163150
try:
164151
ids = [int(id) for id in raw_idlist]
165152
except ValueError:
166153
raise ValidationError({"id": ["Invalid ID format"]})
167154
queryset = queryset.filter(id__in=ids)
168155

156+
# If specific IDs are provided, skip other filtering
157+
return queryset
158+
169159
if raw_query := request.GET.get("query"):
170160
for filter in parse_detector_query(raw_query):
171161
assert isinstance(filter, SearchFilter)
@@ -206,6 +196,36 @@ def get(self, request: Request, organization: Organization) -> Response:
206196
| Q(type__icontains=filter.value.value)
207197
).distinct()
208198

199+
return queryset
200+
201+
@extend_schema(
202+
operation_id="Fetch a Project's Detectors",
203+
parameters=[
204+
GlobalParams.ORG_ID_OR_SLUG,
205+
OrganizationParams.PROJECT,
206+
DetectorParams.QUERY,
207+
DetectorParams.SORT,
208+
DetectorParams.ID,
209+
],
210+
responses={
211+
201: DetectorSerializer,
212+
400: RESPONSE_BAD_REQUEST,
213+
401: RESPONSE_UNAUTHORIZED,
214+
403: RESPONSE_FORBIDDEN,
215+
404: RESPONSE_NOT_FOUND,
216+
},
217+
)
218+
def get(self, request: Request, organization: Organization) -> Response:
219+
"""
220+
List an Organization's Detectors
221+
`````````````````````````````
222+
Return a list of detectors for a given organization.
223+
"""
224+
if not request.user.is_authenticated:
225+
return self.respond(status=status.HTTP_401_UNAUTHORIZED)
226+
227+
queryset = self.filter_detectors(request, organization)
228+
209229
sort_by = SortByParam.parse(request.GET.get("sortBy", "id"), SORT_ATTRS)
210230
if sort_by.db_field_name == "connected_workflows":
211231
queryset = queryset.annotate(connected_workflows=Count("detectorworkflow"))
@@ -307,3 +327,63 @@ def post(self, request: Request, organization: Organization) -> Response:
307327
bulk_validator.save()
308328

309329
return Response(serialize(detector, request.user), status=status.HTTP_201_CREATED)
330+
331+
@extend_schema(
332+
operation_id="Delete an Organization's Detectors",
333+
parameters=[
334+
GlobalParams.ORG_ID_OR_SLUG,
335+
OrganizationParams.PROJECT,
336+
DetectorParams.QUERY,
337+
DetectorParams.SORT,
338+
DetectorParams.ID,
339+
],
340+
responses={
341+
201: RESPONSE_NO_CONTENT,
342+
400: RESPONSE_BAD_REQUEST,
343+
401: RESPONSE_UNAUTHORIZED,
344+
403: RESPONSE_FORBIDDEN,
345+
404: RESPONSE_NOT_FOUND,
346+
},
347+
)
348+
def delete(self, request: Request, organization: Organization) -> Response:
349+
"""
350+
Delete an Organization's Detectors
351+
"""
352+
if not request.user.is_authenticated:
353+
return self.respond(status=status.HTTP_401_UNAUTHORIZED)
354+
355+
if not (
356+
request.GET.getlist("id")
357+
or request.GET.get("query")
358+
or request.GET.getlist("project")
359+
or request.GET.getlist("projectSlug")
360+
):
361+
return Response(
362+
{
363+
"detail": "At least one of 'id', 'query', 'project', or 'projectSlug' must be provided."
364+
},
365+
status=status.HTTP_400_BAD_REQUEST,
366+
)
367+
368+
queryset = self.filter_detectors(request, organization)
369+
370+
detectors_to_delete = list(queryset)
371+
372+
# Check permissions for all detectors first
373+
for detector in detectors_to_delete:
374+
if not can_edit_detector(detector, request):
375+
raise PermissionDenied
376+
377+
with transaction.atomic(router.db_for_write(Detector)):
378+
for detector in detectors_to_delete:
379+
RegionScheduledDeletion.schedule(detector, days=0, actor=request.user)
380+
create_audit_entry(
381+
request=request,
382+
organization=organization,
383+
target_object=detector.id,
384+
event=audit_log.get_event_id("DETECTOR_REMOVE"),
385+
data=detector.get_audit_log_data(),
386+
)
387+
detector.update(status=ObjectStatus.PENDING_DELETION)
388+
389+
return Response(status=status.HTTP_204_NO_CONTENT)

0 commit comments

Comments
 (0)