Skip to content

Commit e7ced48

Browse files
shellmayrclaude
andcommitted
ref(inbound-filters): Gate endpoints with shared has_feature base
Replace the per-endpoint feature_access_denied helper with a has_feature method on a shared ProjectCustomInboundFilterEndpoint base, matching the convention used elsewhere (e.g. project service hooks). Callers now return the 400 response inline when the project feature is disabled. This also fixes a latent bug: every call site invoked feature_access_denied as a bare function, which only existed as a method on the list endpoint and was undefined on the details endpoint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a3e5794 commit e7ced48

1 file changed

Lines changed: 31 additions & 36 deletions

File tree

src/sentry/api/endpoints/project_custom_inbound_filters.py

Lines changed: 31 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -85,17 +85,11 @@ class CustomInboundFilterSerializer(serializers.Serializer):
8585
dateUpdated = serializers.DateTimeField(source="date_updated", read_only=True)
8686

8787
def validate_conditions(self, conditions: list[dict[str, Any]]) -> list[dict[str, Any]]:
88-
condition_types = [condition["type"] for condition in conditions]
89-
90-
primary_condition_types = PRIMARY_CONDITION_TYPES.intersection(condition_types)
91-
if len(primary_condition_types) > 1:
92-
raise serializers.ValidationError(
93-
"Only one of error_message, log_message, or metric_name can be used in a filter."
94-
)
95-
9688
organization = self.context["project"].organization
9789
request = self.context["request"]
90+
condition_types = [condition["type"] for condition in conditions]
9891

92+
primary_condition_types = PRIMARY_CONDITION_TYPES.intersection(condition_types)
9993
if CustomInboundFilterConditionType.LOG_MESSAGE in condition_types and not features.has(
10094
"organizations:ourlogs-ingestion", organization, actor=request.user
10195
):
@@ -109,22 +103,14 @@ def validate_conditions(self, conditions: list[dict[str, Any]]) -> list[dict[str
109103
raise serializers.ValidationError(
110104
"Metric name filters are not enabled for this organization."
111105
)
106+
if len(primary_condition_types) > 1:
107+
raise serializers.ValidationError(
108+
"Only one of error_message, log_message, or metric_name can be used in a filter."
109+
)
112110

113111
return conditions
114112

115113

116-
def feature_access_denied(request: Request, project: Project) -> Response | None:
117-
if not features.has(
118-
"organizations:inbound-filters-v2", project.organization, actor=request.user
119-
):
120-
raise ResourceDoesNotExist
121-
122-
if not features.has("projects:custom-inbound-filters", project, actor=request.user):
123-
return Response({"detail": "You do not have that feature enabled"}, status=400)
124-
125-
return None
126-
127-
128114
def get_audit_log_data(
129115
project: Project,
130116
custom_filter: CustomInboundFilter,
@@ -146,11 +132,22 @@ def get_audit_log_data(
146132
return data
147133

148134

149-
@cell_silo_endpoint
150-
@extend_schema(tags=["Projects"])
151-
class CustomInboundFiltersEndpoint(ProjectEndpoint):
135+
class ProjectCustomInboundFilterEndpoint(ProjectEndpoint):
152136
owner = ApiOwner.TELEMETRY_EXPERIENCE
153137
permission_classes = (ProjectSettingPermission,)
138+
139+
def has_feature(self, request: Request, project: Project) -> bool:
140+
if not features.has(
141+
"organizations:inbound-filters-v2", project.organization, actor=request.user
142+
):
143+
raise ResourceDoesNotExist
144+
145+
return features.has("projects:custom-inbound-filters", project, actor=request.user)
146+
147+
148+
@cell_silo_endpoint
149+
@extend_schema(tags=["Projects"])
150+
class CustomInboundFiltersEndpoint(ProjectCustomInboundFilterEndpoint):
154151
publish_status = {
155152
"GET": ApiPublishStatus.EXPERIMENTAL,
156153
"POST": ApiPublishStatus.EXPERIMENTAL,
@@ -173,8 +170,8 @@ def get(self, request: Request, project: Project) -> Response:
173170
"""
174171
List the custom inbound filters configured for a project.
175172
"""
176-
if denied := feature_access_denied(request, project):
177-
return denied
173+
if not self.has_feature(request, project):
174+
return Response({"detail": "You do not have that feature enabled"}, status=400)
178175

179176
filters = CustomInboundFilter.objects.filter(project_id=project.id)
180177
return self.paginate(
@@ -203,8 +200,8 @@ def post(self, request: Request, project: Project) -> Response:
203200
"""
204201
Create a custom inbound filter for a project.
205202
"""
206-
if denied := feature_access_denied(request, project):
207-
return denied
203+
if not self.has_feature(request, project):
204+
return Response({"detail": "You do not have that feature enabled"}, status=400)
208205

209206
if CustomInboundFilter.objects.filter(project_id=project.id).count() >= (
210207
MAX_FILTERS_PER_PROJECT
@@ -246,9 +243,7 @@ def post(self, request: Request, project: Project) -> Response:
246243

247244
@cell_silo_endpoint
248245
@extend_schema(tags=["Projects"])
249-
class CustomInboundFilterDetailsEndpoint(ProjectEndpoint):
250-
owner = ApiOwner.TELEMETRY_EXPERIENCE
251-
permission_classes = (ProjectSettingPermission,)
246+
class CustomInboundFilterDetailsEndpoint(ProjectCustomInboundFilterEndpoint):
252247
publish_status = {
253248
"GET": ApiPublishStatus.EXPERIMENTAL,
254249
"PUT": ApiPublishStatus.EXPERIMENTAL,
@@ -278,8 +273,8 @@ def get(self, request: Request, project: Project, filter_id: str) -> Response:
278273
"""
279274
Retrieve a single custom inbound filter.
280275
"""
281-
if denied := feature_access_denied(request, project):
282-
return denied
276+
if not self.has_feature(request, project):
277+
return Response({"detail": "You do not have that feature enabled"}, status=400)
283278

284279
custom_filter = self.get_custom_inbound_filter(project, filter_id)
285280
return Response(CustomInboundFilterSerializer(custom_filter).data)
@@ -302,8 +297,8 @@ def put(self, request: Request, project: Project, filter_id: str) -> Response:
302297
"""
303298
Update a custom inbound filter's name, active state, or conditions.
304299
"""
305-
if denied := feature_access_denied(request, project):
306-
return denied
300+
if not self.has_feature(request, project):
301+
return Response({"detail": "You do not have that feature enabled"}, status=400)
307302

308303
custom_filter = self.get_custom_inbound_filter(project, filter_id)
309304
serializer = CustomInboundFilterSerializer(
@@ -355,8 +350,8 @@ def delete(self, request: Request, project: Project, filter_id: str) -> Response
355350
"""
356351
Delete a custom inbound filter.
357352
"""
358-
if denied := feature_access_denied(request, project):
359-
return denied
353+
if not self.has_feature(request, project):
354+
return Response({"detail": "You do not have that feature enabled"}, status=400)
360355

361356
custom_filter = self.get_custom_inbound_filter(project, filter_id)
362357
audit_log_data = get_audit_log_data(project, custom_filter, "remove")

0 commit comments

Comments
 (0)