Skip to content

fix(api): document readonly mutation scope exceptions#113119

Draft
dcramer wants to merge 1 commit intomasterfrom
dcramer/fix/api-readonly-mutation-notes
Draft

fix(api): document readonly mutation scope exceptions#113119
dcramer wants to merge 1 commit intomasterfrom
dcramer/fix/api-readonly-mutation-notes

Conversation

@dcramer
Copy link
Copy Markdown
Member

@dcramer dcramer commented Apr 15, 2026

Add a guardrail for published mutation endpoints that still accept readonly scopes.

Previously, a published POST, PUT, PATCH, or DELETE endpoint could accept a readonly scope like org:read, project:read, or event:read without any explicit marker in code explaining why. That made the policy debt hard to audit and easy to grow accidentally.

After this change, published mutation endpoints that still accept a readonly scope must carry an explicit readonly_mutation_scope_exceptions note explaining the current behavior. The new test fails when a mutation endpoint accepts a readonly scope without that note.

This PR does not change runtime permission behavior. It makes the remaining exceptions explicit so the later tightening PRs are easier to review and so new readonly-mutation regressions cannot land silently.

Refs getsentry/getsentry#19897

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>
@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Apr 15, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Backend Test Failures

Failures on c43aebe in this run:

tests/sentry/api/test_permissions.py::PublishedMutationScopeTest::test_published_mutation_endpoints_require_readonly_scope_noteslog
[gw1] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/api/test_permissions.py:314: in test_published_mutation_endpoints_require_readonly_scope_notes
    assert not missing_notes, "\n".join(missing_notes)
E   AssertionError: sentry.api.endpoints.organization_auth_token_details.OrganizationAuthTokenDetailsEndpoint PUT accepts readonly scopes ['org:read'] via sentry.api.bases.organization.OrgAuthTokenPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.api.endpoints.organization_auth_tokens.OrganizationAuthTokensEndpoint POST accepts readonly scopes ['org:read'] via sentry.api.bases.organization.OrgAuthTokenPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.api.endpoints.organization_onboarding_continuation_email.OrganizationOnboardingContinuationEmail POST accepts readonly scopes ['org:read'] via sentry.api.endpoints.organization_onboarding_continuation_email.OnboardingContinuationPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.api.endpoints.organization_onboarding_tasks.OrganizationOnboardingTaskEndpoint POST accepts readonly scopes ['org:read'] via sentry.api.endpoints.organization_onboarding_tasks.OnboardingTaskPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.api.endpoints.organization_pinned_searches.OrganizationPinnedSearchEndpoint DELETE accepts readonly scopes ['org:read'] via sentry.api.bases.organization.OrganizationPinnedSearchPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.api.endpoints.organization_pinned_searches.OrganizationPinnedSearchEndpoint PUT accepts readonly scopes ['org:read'] via sentry.api.bases.organization.OrganizationPinnedSearchPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.api.endpoints.organization_recent_searches.OrganizationRecentSearchesEndpoint POST accepts readonly scopes ['org:read'] via sentry.api.endpoints.organization_recent_searches.OrganizationRecentSearchPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.api.endpoints.organization_search_details.OrganizationSearchDetailsEndpoint DELETE accepts readonly scopes ['org:read'] via sentry.api.endpoints.organization_search_details.OrganizationSearchEditPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.api.endpoints.organization_search_details.OrganizationSearchDetailsEndpoint PUT accepts readonly scopes ['org:read'] via sentry.api.endpoints.organization_search_details.OrganizationSearchEditPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.api.endpoints.project_repo_path_parsing.ProjectRepoPathParsingEndpoint POST accepts readonly scopes ['org:read'] via sentry.api.endpoints.project_repo_path_parsing.ProjectRepoPathParsingEndpointLoosePermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.api.endpoints.prompts_activity.PromptsActivityEndpoint PUT accepts readonly scopes ['org:read'] via sentry.api.endpoints.prompts_activity.PromptsActivityPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.core.endpoints.organization_projects_experiment.OrganizationProjectsExperimentEndpoint POST accepts readonly scopes ['project:read'] via sentry.core.endpoints.organization_projects_experiment.OrgProjectPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.core.endpoints.project_details.ProjectDetailsEndpoint PUT accepts readonly scopes ['project:read'] via sentry.core.endpoints.project_details.RelaxedProjectAndStaffPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.dashboards.endpoints.organization_dashboard_details.OrganizationDashboardDetailsEndpoint DELETE accepts readonly scopes ['org:read'] via sentry.dashboards.endpoints.organization_dashboards.OrganizationDashboardsPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.dashboards.endpoints.organization_dashboard_details.OrganizationDashboardDetailsEndpoint PUT accepts readonly scopes ['org:read'] via sentry.dashboards.endpoints.organization_dashboards.OrganizationDashboardsPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.dashboards.endpoints.organization_dashboard_details.OrganizationDashboardFavoriteEndpoint PUT accepts readonly scopes ['org:read'] via sentry.dashboards.endpoints.organization_dashboards.OrganizationDashboardsPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.dashboards.endpoints.organization_dashboard_details.OrganizationDashboardVisitEndpoint POST accepts readonly scopes ['org:read'] via sentry.dashboards.endpoints.organization_dashboards.OrganizationDashboardsPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.dashboards.endpoints.organization_dashboard_widget_details.OrganizationDashboardWidgetDetailsEndpoint POST accepts readonly scopes ['org:read'] via sentry.dashboards.endpoints.organization_dashboards.OrganizationDashboardsPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.dashboards.endpoints.organization_dashboards.OrganizationDashboardsEndpoint POST accepts readonly scopes ['org:read'] via sentry.dashboards.endpoints.organization_dashboards.OrganizationDashboardsPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.dashboards.endpoints.organization_dashboards_starred.OrganizationDashboardsStarredOrderEndpoint PUT accepts readonly scopes ['member:read'] via sentry.dashboards.endpoints.organization_dashboards_starred.MemberPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.data_export.endpoints.data_export.DataExportEndpoint POST accepts readonly scopes ['event:read'] via sentry.api.bases.organization.OrganizationDataExportPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.discover.endpoints.discover_homepage_query.DiscoverHomepageQueryEndpoint DELETE accepts readonly scopes ['org:read'] via sentry.discover.endpoints.bases.DiscoverSavedQueryPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.discover.endpoints.discover_homepage_query.DiscoverHomepageQueryEndpoint PUT accepts readonly scopes ['org:read'] via sentry.discover.endpoints.bases.DiscoverSavedQueryPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.discover.endpoints.discover_key_transactions.KeyTransactionEndpoint DELETE accepts readonly scopes ['org:read'] via sentry.discover.endpoints.discover_key_transactions.KeyTransactionPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.discover.endpoints.discover_key_transactions.KeyTransactionEndpoint POST accepts readonly scopes ['org:read'] via sentry.discover.endpoints.discover_key_transactions.KeyTransactionPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.discover.endpoints.discover_saved_queries.DiscoverSavedQueriesEndpoint POST accepts readonly scopes ['org:read'] via sentry.discover.endpoints.bases.DiscoverSavedQueryPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.discover.endpoints.discover_saved_query_detail.DiscoverSavedQueryDetailEndpoint DELETE accepts readonly scopes ['org:read'] via sentry.discover.endpoints.bases.DiscoverSavedQueryPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.discover.endpoints.discover_saved_query_detail.DiscoverSavedQueryDetailEndpoint PUT accepts readonly scopes ['org:read'] via sentry.discover.endpoints.bases.DiscoverSavedQueryPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.discover.endpoints.discover_saved_query_detail.DiscoverSavedQueryVisitEndpoint POST accepts readonly scopes ['org:read'] via sentry.discover.endpoints.bases.DiscoverSavedQueryPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.explore.endpoints.explore_saved_queries.ExploreSavedQueriesEndpoint POST accepts readonly scopes ['org:read'] via sentry.explore.endpoints.bases.ExploreSavedQueryPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.explore.endpoints.explore_saved_query_detail.ExploreSavedQueryDetailEndpoint DELETE accepts readonly scopes ['org:read'] via sentry.explore.endpoints.bases.ExploreSavedQueryPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.explore.endpoints.explore_saved_query_detail.ExploreSavedQueryDetailEndpoint PUT accepts readonly scopes ['org:read'] via sentry.explore.endpoints.bases.ExploreSavedQueryPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.explore.endpoints.explore_saved_query_detail.ExploreSavedQueryVisitEndpoint POST accepts readonly scopes ['org:read'] via sentry.explore.endpoints.bases.ExploreSavedQueryPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.explore.endpoints.explore_saved_query_starred.ExploreSavedQueryStarredEndpoint POST accepts readonly scopes ['member:read'] via sentry.explore.endpoints.explore_saved_query_starred.MemberPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.explore.endpoints.explore_saved_query_starred_order.ExploreSavedQueryStarredOrderEndpoint PUT accepts readonly scopes ['member:read'] via sentry.explore.endpoints.explore_saved_query_starred_order.MemberPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.flags.endpoints.secrets.OrganizationFlagsWebHookSigningSecretsEndpoint POST accepts readonly scopes ['org:read'] via sentry.api.bases.organization.OrganizationFlagWebHookSigningSecretPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.incidents.endpoints.organization_alert_rule_index.OrganizationAlertRuleIndexEndpoint POST accepts readonly scopes ['org:read'] via sentry.api.bases.organization.OrganizationAlertRulePermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.incidents.endpoints.organization_incident_details.OrganizationIncidentDetailsEndpoint PUT accepts readonly scopes ['project:read'] via sentry.api.bases.incident.IncidentPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.insights.endpoints.starred_segments.InsightsStarredSegmentsEndpoint DELETE accepts readonly scopes ['member:read'] via sentry.insights.endpoints.starred_segments.MemberPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.insights.endpoints.starred_segments.InsightsStarredSegmentsEndpoint POST accepts readonly scopes ['member:read'] via sentry.insights.endpoints.starred_segments.MemberPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.integrations.api.endpoints.organization_code_mapping_details.OrganizationCodeMappingDetailsEndpoint PUT accepts readonly scopes ['org:read'] via sentry.api.bases.organization.OrganizationIntegrationsLoosePermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.integrations.api.endpoints.organization_code_mappings.OrganizationCodeMappingsEndpoint POST accepts readonly scopes ['org:read'] via sentry.api.bases.organization.OrganizationIntegrationsLoosePermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.integrations.api.endpoints.organization_code_mappings_bulk.OrganizationCodeMappingsBulkEndpoint POST accepts readonly scopes ['org:read'] via sentry.api.bases.organization.OrganizationCodeMappingsBulkPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.integrations.api.endpoints.organization_repositories.OrganizationRepositoriesEndpoint POST accepts readonly scopes ['org:read'] via sentry.api.bases.organization.OrganizationIntegrationsLoosePermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.issues.endpoints.organization_derive_code_mappings.OrganizationDeriveCodeMappingsEndpoint POST accepts readonly scopes ['org:read'] via sentry.api.bases.organization.OrganizationIntegrationsLoosePermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.issues.endpoints.organization_group_search_view_details.OrganizationGroupSearchViewDetailsEndpoint DELETE accepts readonly scopes ['org:read'] via sentry.issues.endpoints.bases.group_search_view.GroupSearchViewPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
E     sentry.issues.endpoints.organization_group_search_view_details.OrganizationGroupSearchViewDetailsEndpoint PUT accepts readonly scopes ['org:read'] via sentry.issues.endpoints.bases.group_search_view.GroupSearchViewPermission. Remove the readonly scopes or add readonly_mutation_scope_exceptions[method] with a justification note.
... (18 more lines)

Copy link
Copy Markdown
Member

@wedamija wedamija left a comment

Choose a reason for hiding this comment

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

This idea makes sense to me. I feel like we might need to add most places we're tightening up here to these exceptions in #113120, because it's hard to understand what we're going to break there.

@dcramer
Copy link
Copy Markdown
Member Author

dcramer commented Apr 15, 2026

@wedamija do you mean mention in a comment that the ones that we expect to tighten up are TODO? i dont think this should actually break anything its mostly just calling out the ones that are explicitly "wrong" per the design guidelines

@wedamija
Copy link
Copy Markdown
Member

@wedamija do you mean mention in a comment that the ones that we expect to tighten up are TODO? i dont think this should actually break anything its mostly just calling out the ones that are explicitly "wrong" per the design guidelines

I'm not saying anything in this pr is wrong, just that more places might need to be added as exceptions. Let me leave a comment on #113120 because that's where my actual concerns are

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants