-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
fix(api): add project create and codeowners scopes #113122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dcramer/fix/api-write-scope-mutations
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1830,6 +1830,8 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: | |
| "project:read", | ||
| "project:write", | ||
| "project:admin", | ||
| "project:create", | ||
| "project:codeowners", | ||
| "project:releases", | ||
| "project:distribution", | ||
| "event:read", | ||
|
|
@@ -1867,8 +1869,21 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: | |
| "team:write": {"team:read", "team:write"}, | ||
| "team:admin": {"team:read", "team:write", "team:admin"}, | ||
| "project:read": {"project:read"}, | ||
| "project:write": {"project:read", "project:write"}, | ||
| "project:admin": {"project:read", "project:write", "project:admin"}, | ||
| "project:write": { | ||
| "project:read", | ||
| "project:write", | ||
| "project:create", | ||
| "project:codeowners", | ||
| }, | ||
| "project:admin": { | ||
| "project:read", | ||
| "project:write", | ||
| "project:admin", | ||
| "project:create", | ||
| "project:codeowners", | ||
| }, | ||
| "project:create": {"project:create"}, | ||
| "project:codeowners": {"project:codeowners"}, | ||
| "project:releases": {"project:releases"}, | ||
| "project:distribution": {"project:distribution"}, | ||
| "event:read": {"event:read"}, | ||
|
|
@@ -1916,7 +1931,9 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: | |
| ( | ||
| ("project:admin", "Read, write, and admin access to projects."), | ||
| ("project:write", "Read and write access to projects."), | ||
| ("project:codeowners", "Manage code owner and ownership rules."), | ||
| ("project:read", "Read access to projects."), | ||
| ("project:create", "Create projects."), | ||
| ), | ||
| (("project:releases", "Read, write, and admin access to project releases."),), | ||
| (("project:distribution", "Access to app distribution and preprod artifacts."),), | ||
|
|
@@ -1957,6 +1974,8 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: | |
| "event:write", | ||
| "event:admin", | ||
| "project:releases", | ||
| "project:create", | ||
| "project:codeowners", | ||
| "project:read", | ||
| "org:read", | ||
| "member:invite", | ||
|
|
@@ -1989,6 +2008,7 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: | |
| "project:read", | ||
| "project:write", | ||
| "project:admin", | ||
| "project:codeowners", | ||
| "project:releases", | ||
| "team:read", | ||
| "team:write", | ||
|
|
@@ -2014,6 +2034,7 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: | |
| "project:read", | ||
| "project:write", | ||
| "project:admin", | ||
| "project:codeowners", | ||
| "project:releases", | ||
| "team:read", | ||
| "team:write", | ||
|
|
@@ -2051,6 +2072,7 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: | |
| "project:read", | ||
| "project:write", | ||
| "project:admin", | ||
| "project:codeowners", | ||
| "project:releases", | ||
| "event:read", | ||
| "event:write", | ||
|
|
@@ -2097,6 +2119,7 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]: | |
| "org:read", | ||
| "member:read", | ||
| "project:read", | ||
| "project:codeowners", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Team admin role also missing
|
||
| "project:write", | ||
| "project:admin", | ||
| "project:releases", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -118,11 +118,17 @@ def validate_name(self, value: str) -> str: | |
| class TeamProjectPermission(TeamPermission): | ||
| scope_map = { | ||
| "GET": ["project:read", "project:write", "project:admin"], | ||
| "POST": ["project:write", "project:admin"], | ||
| "POST": ["project:create"], | ||
| "PUT": ["project:write", "project:admin"], | ||
| "DELETE": ["project:admin"], | ||
| } | ||
|
|
||
| def has_object_permission(self, request: Request, view, team) -> bool: | ||
| if request.method == "POST" and request.access.has_scope("project:create"): | ||
| return request.access.has_team_access(team) | ||
|
|
||
| return super().has_object_permission(request, view, team) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Early return check uses uninitialized access objectMedium Severity The Reviewed by Cursor Bugbot for commit 70087ed. Configure here. |
||
|
|
||
|
|
||
| class AuditData(TypedDict): | ||
| request: Request | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| # Generated by Django 5.2.12 on 2026-04-15 00:00 | ||
|
|
||
| from django.db import migrations | ||
|
|
||
| import bitfield.models | ||
| from sentry.new_migrations.migrations import CheckedMigration | ||
|
|
||
|
|
||
| class Migration(CheckedMigration): | ||
| is_post_deployment = False | ||
|
|
||
| dependencies = [ | ||
| ("sentry", "1062_backfill_eventattachment_date_expires"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AlterField( | ||
| model_name="apiauthorization", | ||
| name="scopes", | ||
| field=bitfield.models.BitField( | ||
| [ | ||
| "project:read", | ||
| "project:write", | ||
| "project:admin", | ||
| "project:releases", | ||
| "team:read", | ||
| "team:write", | ||
| "team:admin", | ||
| "event:read", | ||
| "event:write", | ||
| "event:admin", | ||
| "org:read", | ||
| "org:write", | ||
| "org:admin", | ||
| "member:read", | ||
| "member:write", | ||
| "member:admin", | ||
| "org:integrations", | ||
| "alerts:read", | ||
| "alerts:write", | ||
| "member:invite", | ||
| "project:distribution", | ||
| "project:create", | ||
| "project:codeowners", | ||
| ], | ||
| default=None, | ||
| ), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name="apikey", | ||
| name="scopes", | ||
| field=bitfield.models.BitField( | ||
| [ | ||
| "project:read", | ||
| "project:write", | ||
| "project:admin", | ||
| "project:releases", | ||
| "team:read", | ||
| "team:write", | ||
| "team:admin", | ||
| "event:read", | ||
| "event:write", | ||
| "event:admin", | ||
| "org:read", | ||
| "org:write", | ||
| "org:admin", | ||
| "member:read", | ||
| "member:write", | ||
| "member:admin", | ||
| "org:integrations", | ||
| "alerts:read", | ||
| "alerts:write", | ||
| "member:invite", | ||
| "project:distribution", | ||
| "project:create", | ||
| "project:codeowners", | ||
| ], | ||
| default=None, | ||
| ), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name="apitoken", | ||
| name="scopes", | ||
| field=bitfield.models.BitField( | ||
| [ | ||
| "project:read", | ||
| "project:write", | ||
| "project:admin", | ||
| "project:releases", | ||
| "team:read", | ||
| "team:write", | ||
| "team:admin", | ||
| "event:read", | ||
| "event:write", | ||
| "event:admin", | ||
| "org:read", | ||
| "org:write", | ||
| "org:admin", | ||
| "member:read", | ||
| "member:write", | ||
| "member:admin", | ||
| "org:integrations", | ||
| "alerts:read", | ||
| "alerts:write", | ||
| "member:invite", | ||
| "project:distribution", | ||
| "project:create", | ||
| "project:codeowners", | ||
| ], | ||
| default=None, | ||
| ), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name="sentryapp", | ||
| name="scopes", | ||
| field=bitfield.models.BitField( | ||
| [ | ||
| "project:read", | ||
| "project:write", | ||
| "project:admin", | ||
| "project:releases", | ||
| "team:read", | ||
| "team:write", | ||
| "team:admin", | ||
| "event:read", | ||
| "event:write", | ||
| "event:admin", | ||
| "org:read", | ||
| "org:write", | ||
| "org:admin", | ||
| "member:read", | ||
| "member:write", | ||
| "member:admin", | ||
| "org:integrations", | ||
| "alerts:read", | ||
| "alerts:write", | ||
| "member:invite", | ||
| "project:distribution", | ||
| "project:create", | ||
| "project:codeowners", | ||
| ], | ||
| default=None, | ||
| ), | ||
| ), | ||
| ] |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing
project:createscope breaks non-member role project creationHigh Severity
The admin (retired), manager, and owner org roles all have
project:write/project:adminbut lack explicitproject:createin their scopes. The member role is the only one withproject:create. Sinceadd_scope_hierarchyis only applied to token scopes andscopes_upper_bound, but NOT to member role scopes (viaRpcBackedAccess.scopes), these roles lose the ability to create projects. For session auth,scopes_upper_boundisNoneso member scopes are used directly. For token auth, the intersection of unexpanded member scopes with the expanded upper bound still excludesproject:create. BothTeamProjectPermissionandOrgProjectPermissionnow requireproject:createfor POST, so admins/managers/owners get 403s.Additional Locations (2)
src/sentry/conf/server.py#L2026-L2047src/sentry/conf/server.py#L2064-L2084Reviewed by Cursor Bugbot for commit 70087ed. Configure here.