Skip to content

Commit 6144c10

Browse files
dcramercodex
andcommitted
fix(replays): require event write for replay deletes
Replay delete is a public mutation, so it should not depend on a project read-style contract. Move the DELETE permission to event write/admin so member sessions keep working while token auth requires a real write scope. Add direct endpoint tests covering the token contract: event:read is denied and event:write is allowed for replay delete. Co-Authored-By: OpenAI Codex <noreply@openai.com>
1 parent bccb7ae commit 6144c10

File tree

2 files changed

+27
-1
lines changed

2 files changed

+27
-1
lines changed

src/sentry/replays/endpoints/project_replay_details.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class ReplayDetailsPermission(ProjectPermission):
2323
"GET": ["project:read", "project:write", "project:admin"],
2424
"POST": ["project:write", "project:admin"],
2525
"PUT": ["project:write", "project:admin"],
26-
"DELETE": ["project:write", "project:admin"],
26+
"DELETE": ["event:write", "event:admin"],
2727
}
2828

2929

tests/sentry/replays/endpoints/test_project_replay_details.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55

66
from django.urls import reverse
77

8+
from sentry.models.apitoken import ApiToken
89
from sentry.models.files.file import File
910
from sentry.replays.lib import kafka
1011
from sentry.replays.lib.storage import RecordingSegmentStorageMeta, storage
1112
from sentry.replays.models import ReplayRecordingSegment
1213
from sentry.replays.testutils import assert_expected_response, mock_expected_response, mock_replay
14+
from sentry.silo.base import SiloMode
1315
from sentry.testutils.cases import APITestCase, ReplaysSnubaTestCase
1416
from sentry.testutils.helpers import TaskRunner
17+
from sentry.testutils.silo import assume_test_silo_mode
1518
from sentry.utils import kafka_config
1619

1720
REPLAYS_FEATURES = {"organizations:session-replay": True}
@@ -192,6 +195,29 @@ def test_delete_replay_from_filestore(self) -> None:
192195
except File.DoesNotExist:
193196
pass
194197

198+
def test_delete_requires_event_write_scope_for_api_tokens(self) -> None:
199+
with assume_test_silo_mode(SiloMode.CONTROL):
200+
token = ApiToken.objects.create(user=self.user, scope_list=["event:read"])
201+
202+
with self.feature(REPLAYS_FEATURES):
203+
response = self.client.delete(
204+
self.url, HTTP_AUTHORIZATION=f"Bearer {token.token}", format="json"
205+
)
206+
207+
assert response.status_code == 403
208+
209+
def test_delete_allows_event_write_scope_for_api_tokens(self) -> None:
210+
with assume_test_silo_mode(SiloMode.CONTROL):
211+
token = ApiToken.objects.create(user=self.user, scope_list=["event:write"])
212+
213+
with self.feature(REPLAYS_FEATURES):
214+
with patch("sentry.replays.endpoints.project_replay_details.delete_replay.delay"):
215+
response = self.client.delete(
216+
self.url, HTTP_AUTHORIZATION=f"Bearer {token.token}", format="json"
217+
)
218+
219+
assert response.status_code == 204
220+
195221
def test_delete_replay_from_clickhouse_data(self) -> None:
196222
"""Test deleting files uploaded through the direct storage interface."""
197223
kept_replay_id = uuid4().hex

0 commit comments

Comments
 (0)