Skip to content

Commit 1e12340

Browse files
dcramercodex
andcommitted
test(api): Add regression coverage for tightened scopes
Add endpoint-level regression tests for the scope changes on this branch so each permission update is covered directly. Cover the write-scope transitions for data export, alert rule creation, project user issues, replay delete, detector updates, and the alert helper endpoints. This keeps the branch from relying on indirect role coverage alone and makes the replay summary readonly exception explicit next to direct token tests. Refs getsentry/getsentry#19897 Co-Authored-By: OpenAI Codex <noreply@openai.com>
1 parent a2d879b commit 1e12340

File tree

8 files changed

+316
-1
lines changed

8 files changed

+316
-1
lines changed

tests/sentry/data_export/endpoints/test_data_export.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from typing import Any
44

5+
from django.urls import reverse
6+
57
from sentry.data_export.base import ExportQueryType, ExportStatus
68
from sentry.data_export.models import ExportedData
79
from sentry.search.utils import parse_datetime_string
@@ -23,6 +25,7 @@ def setUp(self) -> None:
2325
)
2426
self.create_member(user=self.user, organization=self.org, teams=[self.team])
2527
self.login_as(user=self.user)
28+
self.url = reverse(self.endpoint, args=[self.org.slug])
2629

2730
def make_payload(
2831
self, payload_type: str, extras: dict[str, Any] | None = None, overwrite: bool = False
@@ -77,6 +80,34 @@ def test_authorization(self) -> None:
7780
with self.feature("organizations:discover-query"):
7881
self.get_error_response(self.org.slug, status_code=403, **modified_payload)
7982

83+
def test_post_requires_event_write_scope_for_api_keys(self) -> None:
84+
payload = self.make_payload("issue")
85+
api_key = self.create_api_key(organization=self.org, scope_list=["event:read"])
86+
87+
with self.feature("organizations:discover-query"):
88+
response = self.client.post(
89+
self.url,
90+
data=payload,
91+
format="json",
92+
HTTP_AUTHORIZATION=self.create_basic_auth_header(api_key.key),
93+
)
94+
95+
assert response.status_code == 403
96+
97+
def test_post_allows_event_write_scope_for_api_keys(self) -> None:
98+
payload = self.make_payload("issue")
99+
api_key = self.create_api_key(organization=self.org, scope_list=["event:write"])
100+
101+
with self.feature("organizations:discover-query"):
102+
response = self.client.post(
103+
self.url,
104+
data=payload,
105+
format="json",
106+
HTTP_AUTHORIZATION=self.create_basic_auth_header(api_key.key),
107+
)
108+
109+
assert response.status_code == 201
110+
80111
def test_new_export(self) -> None:
81112
"""
82113
Ensures that a request to this endpoint returns a 201 status code

tests/sentry/incidents/endpoints/test_organization_alert_rule_index.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
find_channel_id_for_alert_rule,
4141
)
4242
from sentry.integrations.slack.utils.channel import SlackChannelIdData
43+
from sentry.models.apitoken import ApiToken
4344
from sentry.models.auditlogentry import AuditLogEntry
4445
from sentry.models.organizationmember import OrganizationMember
4546
from sentry.models.projectteam import ProjectTeam
@@ -425,6 +426,10 @@ def setUp(self) -> None:
425426
)
426427
self.login_as(self.user)
427428

429+
def _create_token(self, scope: str) -> ApiToken:
430+
with assume_test_silo_mode(SiloMode.CONTROL):
431+
return ApiToken.objects.create(user=self.user, scope_list=[scope])
432+
428433
def test_simple(self) -> None:
429434
with (
430435
outbox_runner(),
@@ -449,6 +454,35 @@ def test_simple(self) -> None:
449454
== list(audit_log_entry)[0].ip_address
450455
)
451456

457+
def test_create_requires_alerts_write_scope_for_tokens(self) -> None:
458+
token = self._create_token("org:read")
459+
460+
with self.feature(["organizations:incidents", "organizations:performance-view"]):
461+
response = self.client.post(
462+
f"/api/0/organizations/{self.organization.slug}/alert-rules/",
463+
data=self.alert_rule_dict,
464+
format="json",
465+
HTTP_AUTHORIZATION=f"Bearer {token.token}",
466+
)
467+
468+
assert response.status_code == 403
469+
470+
def test_create_allows_alerts_write_scope_for_tokens(self) -> None:
471+
token = self._create_token("alerts:write")
472+
473+
with (
474+
outbox_runner(),
475+
self.feature(["organizations:incidents", "organizations:performance-view"]),
476+
):
477+
response = self.client.post(
478+
f"/api/0/organizations/{self.organization.slug}/alert-rules/",
479+
data=self.alert_rule_dict,
480+
format="json",
481+
HTTP_AUTHORIZATION=f"Bearer {token.token}",
482+
)
483+
484+
assert response.status_code == 201
485+
452486
@patch("sentry.incidents.serializers.alert_rule.are_any_projects_error_upsampled")
453487
def test_count_automatically_converted_to_upsampled_count_for_upsampled_projects(
454488
self, mock_are_any_projects_error_upsampled

tests/sentry/issues/endpoints/test_project_user_issue.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66

77
from sentry.issues.grouptype import WebVitalsGroup
88
from sentry.issues.producer import PayloadType
9+
from sentry.models.apitoken import ApiToken
10+
from sentry.silo.base import SiloMode
911
from sentry.testutils.cases import APITestCase
1012
from sentry.testutils.helpers.features import with_feature
11-
from sentry.testutils.silo import cell_silo_test
13+
from sentry.testutils.silo import assume_test_silo_mode, cell_silo_test
1214

1315

1416
@cell_silo_test
@@ -31,6 +33,10 @@ def setUp(self) -> None:
3133
},
3234
)
3335

36+
def _create_token(self, scope: str) -> ApiToken:
37+
with assume_test_silo_mode(SiloMode.CONTROL):
38+
return ApiToken.objects.create(user=self.user, scope_list=[scope])
39+
3440
@with_feature("organizations:performance-web-vitals-seer-suggestions")
3541
def test_create_web_vitals_issue_success(self) -> None:
3642
data = {
@@ -110,6 +116,48 @@ def test_no_access(self) -> None:
110116

111117
assert response.status_code == 404
112118

119+
@with_feature("organizations:performance-web-vitals-seer-suggestions")
120+
def test_create_web_vitals_issue_requires_event_write_scope(self) -> None:
121+
token = self._create_token("event:read")
122+
123+
response = self.client.post(
124+
self.url,
125+
data={
126+
"transaction": "/test-transaction",
127+
"issueType": WebVitalsGroup.slug,
128+
"score": 75,
129+
"value": 1000,
130+
"vital": "lcp",
131+
},
132+
format="json",
133+
HTTP_AUTHORIZATION=f"Bearer {token.token}",
134+
)
135+
136+
assert response.status_code == 403
137+
138+
@with_feature("organizations:performance-web-vitals-seer-suggestions")
139+
def test_create_web_vitals_issue_allows_event_write_scope(self) -> None:
140+
token = self._create_token("event:write")
141+
142+
with patch(
143+
"sentry.issues.endpoints.project_user_issue.produce_occurrence_to_kafka"
144+
) as mock_produce:
145+
response = self.client.post(
146+
self.url,
147+
data={
148+
"transaction": "/test-transaction",
149+
"issueType": WebVitalsGroup.slug,
150+
"score": 75,
151+
"value": 1000,
152+
"vital": "lcp",
153+
},
154+
format="json",
155+
HTTP_AUTHORIZATION=f"Bearer {token.token}",
156+
)
157+
158+
assert response.status_code == 200
159+
assert response.data == {"event_id": mock_produce.call_args[1]["occurrence"].event_id}
160+
113161
@with_feature("organizations:performance-web-vitals-seer-suggestions")
114162
def test_missing_required_fields(self) -> None:
115163
data = {

tests/sentry/replays/endpoints/test_project_replay_details.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,17 @@ def test_delete_requires_event_write_scope_for_api_tokens(self) -> None:
206206

207207
assert response.status_code == 403
208208

209+
def test_delete_denies_project_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=["project:write"])
212+
213+
with self.feature(REPLAYS_FEATURES):
214+
response = self.client.delete(
215+
self.url, HTTP_AUTHORIZATION=f"Bearer {token.token}", format="json"
216+
)
217+
218+
assert response.status_code == 403
219+
209220
def test_delete_allows_event_write_scope_for_api_tokens(self) -> None:
210221
with assume_test_silo_mode(SiloMode.CONTROL):
211222
token = ApiToken.objects.create(user=self.user, scope_list=["event:write"])

tests/sentry/seer/endpoints/test_organization_events_anomalies.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from unittest.mock import MagicMock, patch
33

44
import orjson
5+
from django.urls import reverse
56
from urllib3 import HTTPResponse
67
from urllib3.exceptions import TimeoutError
78

@@ -10,6 +11,7 @@
1011
AlertRuleSensitivity,
1112
AlertRuleThresholdType,
1213
)
14+
from sentry.models.apitoken import ApiToken
1315
from sentry.seer.anomaly_detection.types import (
1416
Anomaly,
1517
AnomalyDetectionConfig,
@@ -18,10 +20,12 @@
1820
TimeSeriesPoint,
1921
)
2022
from sentry.seer.anomaly_detection.utils import translate_direction
23+
from sentry.silo.base import SiloMode
2124
from sentry.testutils.cases import APITestCase
2225
from sentry.testutils.helpers.datetime import before_now, freeze_time
2326
from sentry.testutils.helpers.features import with_feature
2427
from sentry.testutils.outbox import outbox_runner
28+
from sentry.testutils.silo import assume_test_silo_mode
2529

2630

2731
@freeze_time()
@@ -44,6 +48,10 @@ class OrganizationEventsAnomaliesEndpointTest(APITestCase):
4448
current_timestamp_1 = one_week_ago.timestamp()
4549
current_timestamp_2 = (one_week_ago + timedelta(minutes=10)).timestamp()
4650

51+
def _create_token(self, scope: str) -> ApiToken:
52+
with assume_test_silo_mode(SiloMode.CONTROL):
53+
return ApiToken.objects.create(user=self.user, scope_list=[scope])
54+
4755
def get_test_data(self, project_id: int) -> dict:
4856
return {
4957
"project_id": str(project_id), # UI provides project_id as str
@@ -157,6 +165,47 @@ def test_member_permission(self, mock_seer_request: MagicMock) -> None:
157165
assert mock_seer_request.call_count == 1
158166
assert resp.data == seer_return_value["timeseries"]
159167

168+
@with_feature("organizations:anomaly-detection-alerts")
169+
@with_feature("organizations:incidents")
170+
@patch(
171+
"sentry.seer.anomaly_detection.get_historical_anomalies.seer_anomaly_detection_connection_pool.urlopen"
172+
)
173+
def test_alerts_write_scope_allows_post(self, mock_seer_request: MagicMock) -> None:
174+
mock_seer_request.return_value = HTTPResponse(
175+
orjson.dumps(DetectAnomaliesResponse(success=True, message="", timeseries=[])),
176+
status=200,
177+
)
178+
token = self._create_token("alerts:write")
179+
data = self.get_test_data(self.project.id)
180+
url = reverse(self.endpoint, args=[self.organization.slug])
181+
182+
with outbox_runner():
183+
response = self.client.post(
184+
url,
185+
data=orjson.dumps(data),
186+
content_type="application/json",
187+
HTTP_AUTHORIZATION=f"Bearer {token.token}",
188+
)
189+
190+
assert response.status_code == 200
191+
192+
@with_feature("organizations:anomaly-detection-alerts")
193+
@with_feature("organizations:incidents")
194+
def test_org_read_scope_cannot_post(self) -> None:
195+
token = self._create_token("org:read")
196+
data = self.get_test_data(self.project.id)
197+
url = reverse(self.endpoint, args=[self.organization.slug])
198+
199+
with outbox_runner():
200+
response = self.client.post(
201+
url,
202+
data=orjson.dumps(data),
203+
content_type="application/json",
204+
HTTP_AUTHORIZATION=f"Bearer {token.token}",
205+
)
206+
207+
assert response.status_code == 403
208+
160209
@with_feature("organizations:anomaly-detection-alerts")
161210
@with_feature("organizations:incidents")
162211
@patch(

tests/sentry/uptime/endpoints/test_organization_uptime_alert_preview.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,27 @@ def test_alerts_write_permission(self) -> None:
134134
)
135135

136136
assert response.status_code == 200, response.content
137+
138+
def test_org_read_scope_cannot_run_preview_check(self) -> None:
139+
api_key = self.create_api_key(organization=self.organization, scope_list=["org:read"])
140+
141+
url = reverse(
142+
"sentry-api-0-organization-uptime-alert-preview-check",
143+
kwargs={"organization_id_or_slug": self.organization.slug},
144+
)
145+
response = self.client.post(
146+
url,
147+
data={
148+
"name": "test",
149+
"environment": "uptime-prod",
150+
"owner": f"user:{self.user.id}",
151+
"url": "http://sentry.io",
152+
"timeoutMs": 1500,
153+
"body": None,
154+
"region": "default",
155+
},
156+
format="json",
157+
HTTP_AUTHORIZATION=self.create_basic_auth_header(api_key.key),
158+
)
159+
160+
assert response.status_code == 403
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from unittest import mock
2+
3+
from django.test import override_settings
4+
from django.urls import reverse
5+
6+
from sentry.conf.types.uptime import UptimeRegionConfig
7+
from tests.sentry.uptime.endpoints import UptimeAlertBaseEndpointTest
8+
9+
10+
@override_settings(
11+
UPTIME_REGIONS=[
12+
UptimeRegionConfig(
13+
slug="default",
14+
name="Default Region",
15+
config_redis_key_prefix="default",
16+
api_endpoint="pop-st-1.uptime-checker.s4s.sentry.internal:80",
17+
)
18+
]
19+
)
20+
class OrganizationUptimeAssertionSuggestionsTest(UptimeAlertBaseEndpointTest):
21+
endpoint = "sentry-api-0-organization-uptime-assertion-suggestions"
22+
method = "post"
23+
24+
def setUp(self) -> None:
25+
super().setUp()
26+
self.url = reverse(self.endpoint, args=[self.organization.slug])
27+
self.payload = {
28+
"name": "test",
29+
"environment": "uptime-prod",
30+
"owner": f"user:{self.user.id}",
31+
"url": "http://sentry.io",
32+
"timeoutMs": 1500,
33+
"body": None,
34+
"region": "default",
35+
}
36+
37+
@mock.patch(
38+
"sentry.uptime.endpoints.organization_uptime_assertion_suggestions.generate_assertion_suggestions"
39+
)
40+
@mock.patch(
41+
"sentry.uptime.endpoints.organization_uptime_assertion_suggestions.checker_api.invoke_checker_preview"
42+
)
43+
@mock.patch(
44+
"sentry.uptime.endpoints.organization_uptime_assertion_suggestions.UptimeCheckPreviewValidator"
45+
)
46+
@mock.patch("sentry.uptime.endpoints.organization_uptime_assertion_suggestions.has_seer_access")
47+
def test_alerts_write_scope_can_generate_suggestions(
48+
self,
49+
mock_has_seer_access: mock.MagicMock,
50+
mock_validator_cls: mock.MagicMock,
51+
mock_preview: mock.MagicMock,
52+
mock_generate: mock.MagicMock,
53+
) -> None:
54+
api_key = self.create_api_key(organization=self.organization, scope_list=["alerts:write"])
55+
mock_has_seer_access.return_value = True
56+
mock_validator = mock_validator_cls.return_value
57+
mock_validator.is_valid.return_value = True
58+
mock_validator.save.return_value = {"active_regions": ["default"]}
59+
mock_preview.return_value = mock.Mock(
60+
status_code=200,
61+
json=mock.Mock(return_value={"status": 200}),
62+
raise_for_status=mock.Mock(),
63+
)
64+
mock_generate.return_value = (None, None)
65+
66+
response = self.client.post(
67+
self.url,
68+
data=self.payload,
69+
format="json",
70+
HTTP_AUTHORIZATION=self.create_basic_auth_header(api_key.key),
71+
)
72+
73+
assert response.status_code == 200
74+
75+
def test_org_read_scope_cannot_generate_suggestions(self) -> None:
76+
api_key = self.create_api_key(organization=self.organization, scope_list=["org:read"])
77+
78+
response = self.client.post(
79+
self.url,
80+
data=self.payload,
81+
format="json",
82+
HTTP_AUTHORIZATION=self.create_basic_auth_header(api_key.key),
83+
)
84+
85+
assert response.status_code == 403

0 commit comments

Comments
 (0)