Skip to content
Open
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
9 changes: 2 additions & 7 deletions src/sentry/sentry_apps/api/endpoints/sentry_app_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
from sentry.sentry_apps.logic import SentryAppUpdater
from sentry.sentry_apps.models.sentry_app import SentryApp
from sentry.sentry_apps.models.sentry_app_installation import SentryAppInstallation
from sentry.sentry_apps.utils.webhooks import has_error_events
from sentry.users.models.user import User
from sentry.users.services.user.model import RpcUser
from sentry.utils.audit import create_audit_entry
Expand Down Expand Up @@ -139,7 +140,7 @@ def put(
)
if (
owner_context
and self._has_hook_events(request)
and has_error_events(request.data.get("events"))
and not features.has(
"organizations:integrations-event-hooks",
owner_context.organization,
Expand Down Expand Up @@ -283,9 +284,3 @@ def delete(
return Response(status=204)

return Response({"detail": ["Published apps cannot be removed."]}, status=403)

def _has_hook_events(self, request: Request):
if not request.data.get("events"):
return False

return "error" in request.data["events"]
9 changes: 2 additions & 7 deletions src/sentry/sentry_apps/api/endpoints/sentry_apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
)
from sentry.sentry_apps.logic import SentryAppCreator
from sentry.sentry_apps.models.sentry_app import SentryApp
from sentry.sentry_apps.utils.webhooks import has_error_events
from sentry.users.models.user import User
from sentry.users.services.user.model import RpcUser
from sentry.users.services.user.service import user_service
Expand Down Expand Up @@ -97,7 +98,7 @@ def post(self, request: Request, organization) -> Response:
),
}

if self._has_hook_events(request) and not features.has(
if has_error_events(request.data.get("events")) and not features.has(
"organizations:integrations-event-hooks", organization, actor=request.user
):
return Response(
Expand Down Expand Up @@ -182,9 +183,3 @@ def _filter_queryset_for_user(self, queryset: BaseQuerySet[SentryApp, SentryApp]
owner_ids.append(o.id)

return queryset.filter(owner_id__in=owner_ids)

def _has_hook_events(self, request: Request):
if not request.data.get("events"):
return False

return "error" in request.data["events"]
8 changes: 8 additions & 0 deletions src/sentry/sentry_apps/utils/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@
return EVENT_TO_RESOURCE.get(event)


def has_error_events(events: Collection[str] | None) -> bool:
"""Whether any entry subscribes to error webhooks, as the whole resource or a single event."""
return any(
event == SentryAppResourceType.ERROR or resource_of(event) is SentryAppResourceType.ERROR
for event in events or ()
)


Check warning on line 146 in src/sentry/sentry_apps/utils/webhooks.py

View check run for this annotation

@sentry/warden / warden: sentry-backend-bugs

has_error_events iterates over raw request events without type guard

has_error_events is called on unvalidated request.data['events'] before the serializer runs. A non-iterable value such as an integer raises TypeError instead of a validation error.
Comment on lines +139 to +146

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has_error_events iterates over raw request events without type guard

has_error_events is called on unvalidated request.data['events'] before the serializer runs. A non-iterable value such as an integer raises TypeError instead of a validation error.

Evidence
  • has_error_events is called from sentry_apps.py:post() and sentry_app_details.py:put() on request.data.get('events') before SentryAppParser serializes the payload.
  • The generator for event in events or () assumes events is iterable; a JSON body like {"events": 42} yields 42, which is not iterable and raises TypeError.
  • The parser's EventListField would also reject an int, but has_error_events runs first and turns a validation error into an unhandled 500.

Identified by Warden · sentry-backend-bugs · 4GZ-TU4

def is_subscribed(stored_events: Collection[str], event: str) -> bool:
"""
Whether a stored subscription covers a fired event.
Expand Down
10 changes: 10 additions & 0 deletions tests/sentry/sentry_apps/api/endpoints/test_sentry_app_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,16 @@ def test_can_add_error_created_hook_with_flag(self) -> None:
status_code=200,
)

@with_feature({"organizations:integrations-event-hooks": False})
@override_options({"staff.ga-rollout": True})
def test_cannot_add_granular_error_created_without_flag(self) -> None:
app = self.create_sentry_app(name="SampleApp", organization=self.organization)
self.get_error_response(
app.slug,
events=["error.created"],
status_code=403,
)

@override_options({"staff.ga-rollout": True})
def test_can_add_granular_events(self) -> None:
app = self.create_sentry_app(name="SampleApp", organization=self.organization)
Expand Down
11 changes: 11 additions & 0 deletions tests/sentry/sentry_apps/api/endpoints/test_sentry_apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,17 @@ def test_cannot_create_with_error_created_hook_without_flag(self) -> None:
]
}

def test_cannot_create_with_granular_error_created_without_flag(self) -> None:
with Feature({"organizations:integrations-event-hooks": False}):
response = self.get_error_response(
**self.get_data(events=("error.created",)), status_code=403
)
assert response.data == {
"non_field_errors": [
"Your organization does not have access to the 'error' resource subscription."
]
}

def test_can_create_with_granular_events(self) -> None:
response = self.get_success_response(
**self.get_data(events=("issue.resolved",)), status_code=201
Expand Down
Loading