Skip to content

Commit d45bcf0

Browse files
dcramercodex
andcommitted
test(api): Require notes for readonly mutation scopes
Add a guardrail for published mutation endpoints that still accept readonly scopes. Previously, write methods could keep readonly scopes in scope_map without any explicit marker in code, which made the policy debt hard to audit and easy to expand accidentally. Require those endpoints to carry a readonly_mutation_scope_exceptions note, and fail the invariant test when a published mutation endpoint accepts readonly scopes without that note. Co-Authored-By: OpenAI Codex <noreply@openai.com>
1 parent 95827eb commit d45bcf0

File tree

20 files changed

+151
-0
lines changed

20 files changed

+151
-0
lines changed

src/sentry/api/bases/organization_request_change.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88

99
class OrganizationRequestChangeEndpointPermission(OrganizationPermission):
10+
readonly_mutation_scope_exceptions = {
11+
"POST": "This endpoint only files a request for an organization change; members use it without organization write access.",
12+
}
1013
# just requesting so read permission is enough
1114
scope_map = {
1215
"POST": ["org:read"],

src/sentry/codecov/endpoints/repository_token_regenerate/repository_token_regenerate.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818

1919

2020
class RepositoryTokenRegeneratePermission(OrganizationPermission):
21+
readonly_mutation_scope_exceptions = {
22+
"POST": "Codecov token regeneration preserves read-only token access for now.",
23+
}
2124
scope_map = {
2225
"GET": ["org:read", "org:write", "org:admin"],
2326
"POST": ["org:read", "org:write", "org:admin"],

src/sentry/codecov/endpoints/sync_repos/sync_repos.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818

1919

2020
class SyncReposPermission(OrganizationPermission):
21+
readonly_mutation_scope_exceptions = {
22+
"POST": "Codecov sync preserves read-only token access pending integration-scope cleanup.",
23+
}
2124
scope_map = {
2225
"GET": ["org:read", "org:write", "org:admin"],
2326
"POST": ["org:read", "org:write", "org:admin"],

src/sentry/conduit/endpoints/organization_conduit_demo.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ class OrganizationConduitDemoPermission(OrganizationPermission):
3131
This is a demo-only feature and doesn't modify organization state.
3232
"""
3333

34+
readonly_mutation_scope_exceptions = {
35+
"POST": "Demo credential generation preserves read-only token access for now.",
36+
}
3437
scope_map = {
3538
"POST": ["org:read", "org:write", "org:admin"],
3639
}

src/sentry/core/endpoints/organization_member_invite/index.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838

3939

4040
class MemberInvitePermission(OrganizationPermission):
41+
readonly_mutation_scope_exceptions = {
42+
"POST": "Members can create invite requests on this endpoint even when they cannot send invites directly.",
43+
}
4144
scope_map = {
4245
"GET": ["member:read", "member:write", "member:admin"],
4346
# We will do an additional check to see if a user can invite members. If

src/sentry/core/endpoints/organization_member_invite/utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77

88
class MemberInviteDetailsPermission(OrganizationPermission):
9+
readonly_mutation_scope_exceptions = {
10+
"DELETE": "Invite deletion keeps current member-read token semantics for now.",
11+
}
912
scope_map = {
1013
"GET": ["member:read", "member:write", "member:admin"],
1114
"PUT": ["member:write", "member:admin"],

src/sentry/core/endpoints/organization_member_requests_invite_index.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020

2121

2222
class InviteRequestPermissions(OrganizationPermission):
23+
readonly_mutation_scope_exceptions = {
24+
"POST": "Invite request creation keeps member-read token access for now.",
25+
}
2326
scope_map = {
2427
"GET": ["member:read", "member:write", "member:admin"],
2528
"POST": ["member:read", "member:write", "member:admin"],

src/sentry/core/endpoints/organization_member_team_details.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ def serialize(
7272

7373

7474
class OrganizationTeamMemberPermission(OrganizationPermission):
75+
readonly_mutation_scope_exceptions = {
76+
"POST": "Team membership writes keep current mixed org/member/team token semantics for now.",
77+
"PUT": "Team membership writes keep current mixed org/member/team token semantics for now.",
78+
"DELETE": "Team membership writes keep current mixed org/member/team token semantics for now.",
79+
}
7580
scope_map = {
7681
"GET": [
7782
"org:read",

src/sentry/core/endpoints/organization_member_utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ class MemberConflictValidationError(serializers.ValidationError):
6161

6262

6363
class RelaxedMemberPermission(OrganizationPermission):
64+
readonly_mutation_scope_exceptions = {
65+
"DELETE": "Member deletion keeps self-service and role-comparison semantics for now.",
66+
}
6467
scope_map = {
6568
"GET": ["member:read", "member:write", "member:admin"],
6669
"POST": ["member:write", "member:admin"],

src/sentry/dashboards/endpoints/organization_dashboard_generate.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ class DashboardGenerateSerializer(serializers.Serializer[dict[str, Any]]):
4949

5050

5151
class OrganizationDashboardGeneratePermission(OrganizationPermission):
52+
readonly_mutation_scope_exceptions = {
53+
"POST": "Dashboard generation is a POST helper/action and needs separate contract cleanup.",
54+
}
5255
scope_map = {
5356
"POST": ["org:read"],
5457
}

0 commit comments

Comments
 (0)