Skip to content

Commit a2d879b

Browse files
dcramercodex
andcommitted
ref(api): document helper permission intent
Keep replay summary POST on read-level replay scopes and mark it as an explicit readonly-mutation exception, since it derives summary data from existing replay/event data. Document the preview endpoints that intentionally keep write-aligned permissions because they are part of alert and monitor authoring flows, even though they use POST helper endpoints. Add replay summary token tests covering the read-scope contract. Co-Authored-By: OpenAI Codex <noreply@openai.com>
1 parent 9b80874 commit a2d879b

5 files changed

Lines changed: 52 additions & 1 deletion

File tree

src/sentry/replays/endpoints/project_replay_summary.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
class ReplaySummaryPermission(ProjectPermission):
4040
scope_map = {
4141
"GET": ["event:read", "event:write", "event:admin"],
42-
"POST": ["event:write", "event:admin"],
42+
"POST": ["event:read", "event:write", "event:admin"],
4343
"PUT": [],
4444
"DELETE": [],
4545
}
@@ -52,6 +52,13 @@ class ProjectReplaySummaryEndpoint(ProjectReplayEndpoint):
5252
"GET": ApiPublishStatus.EXPERIMENTAL,
5353
"POST": ApiPublishStatus.EXPERIMENTAL,
5454
}
55+
readonly_mutation_scope_exceptions = {
56+
"POST": (
57+
"POST starts replay-summary generation but only derives summary data from existing "
58+
"replay/event data. It intentionally follows replay read access instead of requiring "
59+
"a separate write capability."
60+
)
61+
}
5562
permission_classes = (ReplaySummaryPermission,)
5663

5764
def __init__(self, **kw) -> None:

src/sentry/seer/endpoints/organization_events_anomalies.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ class OrganizationEventsAnomaliesEndpoint(OrganizationEventsEndpointBase):
3333
publish_status = {
3434
"POST": ApiPublishStatus.EXPERIMENTAL,
3535
}
36+
# This POST previews anomaly-detection config used while authoring metric
37+
# alerts/detectors, so it intentionally follows alert-write permissions.
3638
permission_classes = (OrganizationAlertRulePermission,)
3739

3840
@extend_schema(

src/sentry/uptime/endpoints/organization_uptime_alert_preview_check.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
@cell_silo_endpoint
3131
class OrganizationUptimeAlertPreviewCheckEndpoint(OrganizationEndpoint):
3232
owner = ApiOwner.CRONS
33+
# This POST previews monitor creation and validation, so it intentionally
34+
# uses the same permission surface as creating the alert itself.
3335
permission_classes = (OrganizationAlertRulePermission,)
3436

3537
publish_status = {

src/sentry/uptime/endpoints/organization_uptime_assertion_suggestions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ class OrganizationUptimeAssertionSuggestionsEndpoint(OrganizationEndpoint):
5050
"""
5151

5252
owner = ApiOwner.CRONS
53+
# This POST is part of the uptime monitor authoring flow, so it should
54+
# track the same alert-write permission as the monitor it helps create.
5355
permission_classes = (OrganizationAlertRulePermission,)
5456

5557
publish_status = {

tests/sentry/replays/endpoints/test_project_replay_summary.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
from django.conf import settings
88
from django.urls import reverse
99

10+
from sentry.models.apitoken import ApiToken
1011
from sentry.replays.testutils import mock_replay
12+
from sentry.silo.base import SiloMode
1113
from sentry.testutils.cases import SnubaTestCase, TransactionTestCase
14+
from sentry.testutils.silo import assume_test_silo_mode
1215
from sentry.utils import json
1316

1417

@@ -140,6 +143,41 @@ def test_post_simple(self, mock_seer_request: Mock) -> None:
140143
assert body["project_id"] == self.project.id
141144
assert body.get("temperature") is None
142145

146+
@patch("sentry.replays.endpoints.project_replay_summary.make_replay_summary_start_request")
147+
def test_post_allows_event_read_scope_for_api_tokens(self, mock_seer_request: Mock) -> None:
148+
mock_seer_request.return_value = MockSeerResponse(200, json_data={"hello": "world"})
149+
self.store_replay()
150+
151+
with assume_test_silo_mode(SiloMode.CONTROL):
152+
token = ApiToken.objects.create(user=self.user, scope_list=["event:read"])
153+
154+
with self.feature(self.features):
155+
response = self.client.post(
156+
self.url,
157+
data={"num_segments": 1},
158+
content_type="application/json",
159+
HTTP_AUTHORIZATION=f"Bearer {token.token}",
160+
)
161+
162+
assert response.status_code == 200
163+
assert response.json() == {"hello": "world"}
164+
165+
def test_post_requires_replay_read_scope_for_api_tokens(self) -> None:
166+
self.store_replay()
167+
168+
with assume_test_silo_mode(SiloMode.CONTROL):
169+
token = ApiToken.objects.create(user=self.user, scope_list=["org:read"])
170+
171+
with self.feature(self.features):
172+
response = self.client.post(
173+
self.url,
174+
data={"num_segments": 1},
175+
content_type="application/json",
176+
HTTP_AUTHORIZATION=f"Bearer {token.token}",
177+
)
178+
179+
assert response.status_code == 403
180+
143181
def test_post_replay_not_found(self) -> None:
144182
with self.feature(self.features):
145183
response = self.client.post(

0 commit comments

Comments
 (0)