|
7 | 7 | from django.db.models.query import QuerySet
|
8 | 8 | from drf_spectacular.utils import PolymorphicProxySerializer, extend_schema
|
9 | 9 | from rest_framework import status
|
10 |
| -from rest_framework.exceptions import ValidationError |
| 10 | +from rest_framework.exceptions import PermissionDenied, ValidationError |
11 | 11 | from rest_framework.request import Request
|
12 | 12 | from rest_framework.response import Response
|
13 | 13 |
|
14 |
| -from sentry import features |
| 14 | +from sentry import audit_log, features |
15 | 15 | from sentry.api.api_owners import ApiOwner
|
16 | 16 | from sentry.api.api_publish_status import ApiPublishStatus
|
17 | 17 | from sentry.api.base import region_silo_endpoint
|
|
25 | 25 | from sentry.apidocs.constants import (
|
26 | 26 | RESPONSE_BAD_REQUEST,
|
27 | 27 | RESPONSE_FORBIDDEN,
|
| 28 | + RESPONSE_NO_CONTENT, |
28 | 29 | RESPONSE_NOT_FOUND,
|
29 | 30 | RESPONSE_UNAUTHORIZED,
|
30 | 31 | )
|
31 | 32 | from sentry.apidocs.parameters import DetectorParams, GlobalParams, OrganizationParams
|
| 33 | +from sentry.constants import ObjectStatus |
| 34 | +from sentry.deletions.models.scheduleddeletion import RegionScheduledDeletion |
32 | 35 | from sentry.incidents.grouptype import MetricIssue
|
33 | 36 | from sentry.issues import grouptype
|
34 | 37 | from sentry.models.organization import Organization
|
|
37 | 40 | from sentry.uptime.grouptype import UptimeDomainCheckFailure
|
38 | 41 | from sentry.users.models.user import User
|
39 | 42 | from sentry.users.services.user import RpcUser
|
| 43 | +from sentry.utils.audit import create_audit_entry |
40 | 44 | from sentry.workflow_engine.endpoints.serializers import DetectorSerializer
|
41 | 45 | from sentry.workflow_engine.endpoints.utils.filters import apply_filter
|
42 | 46 | from sentry.workflow_engine.endpoints.utils.sortby import SortByParam
|
43 | 47 | from sentry.workflow_engine.endpoints.validators.base import BaseDetectorTypeValidator
|
44 | 48 | from sentry.workflow_engine.endpoints.validators.detector_workflow import (
|
45 | 49 | BulkDetectorWorkflowsValidator,
|
| 50 | + can_edit_detector, |
46 | 51 | )
|
47 | 52 | from sentry.workflow_engine.endpoints.validators.utils import get_unknown_detector_type_error
|
48 | 53 | from sentry.workflow_engine.models import Detector
|
@@ -121,51 +126,36 @@ class OrganizationDetectorIndexEndpoint(OrganizationEndpoint):
|
121 | 126 | publish_status = {
|
122 | 127 | "GET": ApiPublishStatus.EXPERIMENTAL,
|
123 | 128 | "POST": ApiPublishStatus.EXPERIMENTAL,
|
| 129 | + "DELETE": ApiPublishStatus.EXPERIMENTAL, |
124 | 130 | }
|
125 | 131 | owner = ApiOwner.ISSUES
|
126 | 132 |
|
127 | 133 | # TODO: We probably need a specific permission for detectors. Possibly specific detectors have different perms
|
128 | 134 | # too?
|
129 | 135 | permission_classes = (OrganizationAlertRulePermission,)
|
130 | 136 |
|
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]: |
149 | 138 | """
|
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. |
153 | 140 | """
|
154 |
| - if not request.user.is_authenticated: |
155 |
| - return self.respond(status=status.HTTP_401_UNAUTHORIZED) |
156 |
| - |
157 | 141 | projects = self.get_projects(request, organization)
|
158 | 142 | queryset: QuerySet[Detector] = Detector.objects.filter(
|
159 | 143 | project_id__in=projects,
|
160 | 144 | )
|
161 | 145 |
|
| 146 | + if not request.user.is_authenticated: |
| 147 | + return queryset |
| 148 | + |
162 | 149 | if raw_idlist := request.GET.getlist("id"):
|
163 | 150 | try:
|
164 | 151 | ids = [int(id) for id in raw_idlist]
|
165 | 152 | except ValueError:
|
166 | 153 | raise ValidationError({"id": ["Invalid ID format"]})
|
167 | 154 | queryset = queryset.filter(id__in=ids)
|
168 | 155 |
|
| 156 | + # If specific IDs are provided, skip other filtering |
| 157 | + return queryset |
| 158 | + |
169 | 159 | if raw_query := request.GET.get("query"):
|
170 | 160 | for filter in parse_detector_query(raw_query):
|
171 | 161 | assert isinstance(filter, SearchFilter)
|
@@ -206,6 +196,36 @@ def get(self, request: Request, organization: Organization) -> Response:
|
206 | 196 | | Q(type__icontains=filter.value.value)
|
207 | 197 | ).distinct()
|
208 | 198 |
|
| 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 | + |
209 | 229 | sort_by = SortByParam.parse(request.GET.get("sortBy", "id"), SORT_ATTRS)
|
210 | 230 | if sort_by.db_field_name == "connected_workflows":
|
211 | 231 | queryset = queryset.annotate(connected_workflows=Count("detectorworkflow"))
|
@@ -307,3 +327,63 @@ def post(self, request: Request, organization: Organization) -> Response:
|
307 | 327 | bulk_validator.save()
|
308 | 328 |
|
309 | 329 | 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