Skip to content
4 changes: 2 additions & 2 deletions migrations_lockfile.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ feedback: 0006_safe_del_feedback_model

flags: 0001_squashed_0004_add_flag_audit_log_provider_column

hybridcloud: 0023_correct_webhook_payload_constraint
hybridcloud: 0024_add_project_distribution_scope

insights: 0002_backfill_team_starred

Expand All @@ -31,7 +31,7 @@ releases: 0001_release_models

replays: 0006_add_bulk_delete_job

sentry: 0999_add_extrapolation_mode_to_snuba_query
sentry: 1000_add_project_distribution_scope

social_auth: 0003_social_auth_json_field

Expand Down
2 changes: 2 additions & 0 deletions src/sentry/conf/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1729,6 +1729,7 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]:
"project:write",
"project:admin",
"project:releases",
"project:distribution",
"event:read",
"event:write",
"event:admin",
Expand Down Expand Up @@ -1767,6 +1768,7 @@ def custom_parameter_sort(parameter: dict) -> tuple[str, int]:
"project:write": {"project:read", "project:write"},
"project:admin": {"project:read", "project:write", "project:admin"},
"project:releases": {"project:releases"},
"project:distribution": {"project:distribution"},
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: Scope Misalignment in Token Validation

The project:distribution scope was added to SENTRY_SCOPES and SENTRY_SCOPE_HIERARCHY_MAPPING. This goes against the design intent to restrict this scope to API tokens only, making it incorrectly available for OrgAuthToken validation.

Fix in Cursor Fix in Web

"event:read": {"event:read"},
"event:write": {"event:read", "event:write"},
"event:admin": {"event:read", "event:write", "event:admin"},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Generated by Django 5.2.1 on 2025-10-27 10:46

from django.db import migrations

import bitfield.models
from sentry.new_migrations.migrations import CheckedMigration


class Migration(CheckedMigration):
# This flag is used to mark that a migration shouldn't be automatically run in production.
# This should only be used for operations where it's safe to run the migration after your
# code has deployed. So this should not be used for most operations that alter the schema
# of a table.
# Here are some things that make sense to mark as post deployment:
# - Large data migrations. Typically we want these to be run manually so that they can be
# monitored and not block the deploy for a long period of time while they run.
# - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to
# run this outside deployments so that we don't block them. Note that while adding an index
# is a schema change, it's completely safe to run the operation after the code has deployed.
# Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment

is_post_deployment = False

dependencies = [
("hybridcloud", "0023_correct_webhook_payload_constraint"),
]

operations = [
migrations.AlterField(
model_name="apikeyreplica",
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",
],
default=None,
),
),
migrations.AlterField(
model_name="apitokenreplica",
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",
],
default=None,
),
),
]
149 changes: 149 additions & 0 deletions src/sentry/migrations/1000_add_project_distribution_scope.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Generated by Django 5.2.1 on 2025-10-27 10:46

from django.db import migrations

import bitfield.models
from sentry.new_migrations.migrations import CheckedMigration


class Migration(CheckedMigration):
# This flag is used to mark that a migration shouldn't be automatically run in production.
# This should only be used for operations where it's safe to run the migration after your
# code has deployed. So this should not be used for most operations that alter the schema
# of a table.
# Here are some things that make sense to mark as post deployment:
# - Large data migrations. Typically we want these to be run manually so that they can be
# monitored and not block the deploy for a long period of time while they run.
# - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to
# run this outside deployments so that we don't block them. Note that while adding an index
# is a schema change, it's completely safe to run the operation after the code has deployed.
# Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment

is_post_deployment = False

dependencies = [
("sentry", "0999_add_extrapolation_mode_to_snuba_query"),
]

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",
],
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",
],
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",
],
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",
],
default=None,
),
),
]
9 changes: 8 additions & 1 deletion src/sentry/models/apiscopes.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ def add_scope_hierarchy(curr_scopes: Sequence[str]) -> list[str]:


class ApiScopes(Sequence):
project = (("project:read"), ("project:write"), ("project:admin"), ("project:releases"))
project = (
("project:read"),
("project:write"),
("project:admin"),
("project:releases"),
("project:distribution"),
)

team = (("team:read"), ("team:write"), ("team:admin"))

Expand Down Expand Up @@ -85,6 +91,7 @@ class Meta:
"alerts:read": bool,
"alerts:write": bool,
"member:invite": bool,
"project:distribution": bool,
},
)
assert set(ScopesDict.__annotations__) == set(ApiScopes())
Expand Down
2 changes: 1 addition & 1 deletion tests/sentry/api/serializers/test_organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

pytestmark = [requires_snuba]

non_default_owner_scopes = ["org:ci", "openid", "email", "profile"]
non_default_owner_scopes = ["org:ci", "openid", "email", "profile", "project:distribution"]
default_owner_scopes = frozenset(
filter(lambda scope: scope not in non_default_owner_scopes, settings.SENTRY_SCOPES)
)
Expand Down
2 changes: 1 addition & 1 deletion tools/migrations/squash.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def _migration_root(app: str) -> str:

def _migrations(root: str) -> Generator[str]:
for fname in os.listdir(root):
if fname.startswith("0") and fname.endswith(".py"):
if fname[0].isdigit() and fname.endswith(".py"):
yield fname


Expand Down
Loading