Skip to content

Commit 3fc2a5a

Browse files
committed
ref(typing): Narrow request.user in agent tokens, releases, notebooks and client config
- resolve_minting_principal() rejects any caller with no user. A service account used to resolve to a user principal carrying its ServiceAccountId and fail later; it now gets the same "Minting requires a user principal" 403 as an anonymous caller. - Release serialization takes a RequestPrincipal; it only forwards the caller to serialize() and serialize_generic_user(), which already take one. - Notebook endpoints read the caller through acting_user_id(). - The client config is built only from Django views, so its request is typed as an HttpRequest. Drops four files from the request.user allowlist; member management and group_integration_details.py remain.
1 parent 367601e commit 3fc2a5a

8 files changed

Lines changed: 23 additions & 26 deletions

File tree

‎src/sentry/api/serializers/models/release.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
GroupEventReleaseSerializerResponse,
1717
ReleaseSerializerResponse,
1818
)
19+
from sentry.auth.principal import RequestPrincipal
1920
from sentry.integrations.models.external_actor import ExternalActor
2021
from sentry.models.commit import Commit
2122
from sentry.models.commitauthor import CommitAuthor
@@ -332,7 +333,7 @@ def get_cached_results(
332333
def get_users_for_authors(
333334
organization_id: int,
334335
authors: list[CommitAuthor],
335-
user: User | AnonymousUser | RpcUser | None = None,
336+
user: RequestPrincipal | None = None,
336337
) -> Mapping[str, Author]:
337338
"""
338339
Returns a dictionary of commit_author_id => user, if a Sentry

‎src/sentry/notebooks/endpoints/base.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from sentry.api.api_owners import ApiOwner
1111
from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission
1212
from sentry.api.exceptions import ResourceDoesNotExist
13+
from sentry.auth.principal import acting_user_id
1314
from sentry.constants import ObjectStatus
1415
from sentry.models.organization import Organization
1516
from sentry.models.project import Project
@@ -47,7 +48,7 @@ def organization_project_ids(organization: Organization) -> set[int]:
4748

4849

4950
def user_id(request: Request) -> int:
50-
resolved = request.user.id
51+
resolved = acting_user_id(request.user)
5152
if resolved is None:
5253
raise PermissionDenied
5354
return resolved

‎src/sentry/releases/use_cases/release.py‎

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from datetime import datetime, timezone
44
from typing import Any, cast
55

6-
from django.contrib.auth.models import AnonymousUser
76
from django.db.models import Sum
87
from sentry_sdk import traces
98

@@ -13,6 +12,7 @@
1312
from sentry.api.serializers.release_details_types import Author, BaseProject, LastDeploy
1413
from sentry.api.serializers.release_details_types import Project as SerializedProject
1514
from sentry.api.serializers.types import ReleaseSerializerResponse
15+
from sentry.auth.principal import RequestPrincipal
1616
from sentry.models.commit import Commit
1717
from sentry.models.commitauthor import CommitAuthor
1818
from sentry.models.deploy import Deploy
@@ -27,16 +27,14 @@
2727
)
2828
from sentry.models.releases.release_project import ReleaseProject
2929
from sentry.releases.serializers import release
30-
from sentry.users.models.user import User
31-
from sentry.users.services.user import RpcUser
3230
from sentry.users.services.user.serial import serialize_generic_user
3331
from sentry.users.services.user.service import user_service
3432

3533

3634
@traces.trace
3735
def serialize(
3836
releases: list[Release],
39-
user: AnonymousUser | User | RpcUser,
37+
user: RequestPrincipal,
4038
organization_id: int,
4139
environment_ids: list[int],
4240
projects: list[Project],
@@ -343,15 +341,15 @@ def fetch_releases_adoption_stages(
343341

344342
@traces.trace
345343
def fetch_authors(
346-
user: AnonymousUser | User | RpcUser, organization_id: int, author_ids: Iterable[str]
344+
user: RequestPrincipal, organization_id: int, author_ids: Iterable[str]
347345
) -> Mapping[str, Any]:
348346
authors = list(CommitAuthor.objects.filter(id__in=author_ids, organization_id=organization_id))
349347
return get_users_for_authors(organization_id=organization_id, authors=authors, user=user)
350348

351349

352350
@traces.trace
353351
def fetch_commits(
354-
user: AnonymousUser | User | RpcUser, organization_id: int, commit_ids: Iterable[int]
352+
user: RequestPrincipal, organization_id: int, commit_ids: Iterable[int]
355353
) -> dict[int, dict[str, Any]]:
356354
commit_list = list(
357355
Commit.objects.filter(id__in=commit_ids, organization_id=organization_id)
@@ -364,17 +362,15 @@ def fetch_commits(
364362

365363
@traces.trace
366364
def fetch_deploys(
367-
user: AnonymousUser | User | RpcUser, organization_id: int, deploy_ids: Iterable[int]
365+
user: RequestPrincipal, organization_id: int, deploy_ids: Iterable[int]
368366
) -> dict[int, LastDeploy]:
369367
deploy_list = list(Deploy.objects.filter(id__in=deploy_ids, organization_id=organization_id))
370368
# XXX: This serializer queries environments.
371369
return {d.id: c for d, c in zip(deploy_list, model_serializer(deploy_list, user))}
372370

373371

374372
@traces.trace
375-
def fetch_owners(
376-
user: AnonymousUser | User | RpcUser, owner_ids: Iterable[int]
377-
) -> dict[int, dict[str, Any]]:
373+
def fetch_owners(user: RequestPrincipal, owner_ids: Iterable[int]) -> dict[int, dict[str, Any]]:
378374
users = user_service.serialize_many(
379375
filter={"user_ids": owner_ids}, as_user=serialize_generic_user(user)
380376
)

‎src/sentry/seer/agent_token.py‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,19 @@
1313
from typing import NotRequired, TypedDict, TypeGuard
1414

1515
from django.conf import settings
16-
from django.contrib.auth.models import AnonymousUser
1716
from django.db import router, transaction
1817
from django.utils import timezone
1918
from jwt import PyJWTError
2019
from pydantic import ValidationError
2120

21+
from sentry.auth.principal import RequestPrincipal, has_user
2222
from sentry.auth.services.auth import AuthenticatedToken
2323
from sentry.seer.models.agent_write_grant import (
2424
AGENT_SESSION_ID_MAX_LENGTH,
2525
DEFAULT_EXPIRATION,
2626
SeerAgentWriteGrant,
2727
)
2828
from sentry.types.superuser import SuperuserAccess
29-
from sentry.users.models.user import User
30-
from sentry.users.services.user import RpcUser
3129
from sentry.utils import jwt
3230

3331
FEATURE_FLAG = "organizations:seer-agent-token-flow"
@@ -66,7 +64,6 @@ class AgentPrincipal:
6664

6765

6866
type MintingPrincipal = AgentPrincipal | MintingPrincipalRejection
69-
type MintingUser = User | RpcUser | AnonymousUser
7067

7168

7269
class AgentTokenClaims(TypedDict):
@@ -97,11 +94,11 @@ def readonly_scopes() -> frozenset[str]:
9794

9895

9996
def resolve_minting_principal(
100-
user: MintingUser, auth: AuthenticatedToken | None
97+
user: RequestPrincipal, auth: AuthenticatedToken | None
10198
) -> MintingPrincipal:
10299
if is_agent_auth(auth):
103100
return MintingPrincipalRejection.AGENT
104-
if isinstance(user, AnonymousUser):
101+
if not has_user(user):
105102
return MintingPrincipalRejection.UNSUPPORTED
106103
if user.is_sentry_app:
107104
return MintingPrincipalRejection.INTEGRATION

‎src/sentry/web/client_config.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from django.core.cache import cache
1313
from django.http import HttpRequest
1414
from packaging.version import parse as parse_version
15-
from rest_framework.request import Request
1615

1716
import sentry
1817
from sentry import features, options
@@ -183,7 +182,7 @@ def _resolve_last_org(
183182
class _ClientConfig:
184183
def __init__(
185184
self,
186-
request: Request | None = None,
185+
request: HttpRequest | None = None,
187186
org_context: RpcUserOrganizationContext | None = None,
188187
) -> None:
189188
self.request = request

‎tests/sentry/auth/test_principal.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,9 +275,9 @@ def _is_request_user_type_check(node: ast.AST) -> bool:
275275

276276

277277
# Lower these as callers move off. Exact, so the baseline can't drift back up.
278-
REQUEST_USER_ID_BASELINE = 131
278+
REQUEST_USER_ID_BASELINE = 130
279279
REQUEST_USER_TYPE_CHECK_BASELINE = 29
280-
REQUEST_USER_ALLOWLIST_BASELINE = 10
280+
REQUEST_USER_ALLOWLIST_BASELINE = 6
281281

282282

283283
def test_request_user_id_uses_only_go_down() -> None:

‎tests/sentry/seer/test_agent_token.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from sentry.organizations.services.organization import organization_service
2525
from sentry.seer import agent_token
2626
from sentry.seer.models.agent_write_grant import SeerAgentWriteGrant
27+
from sentry.service_accounts.principal import ServiceAccountPrincipal
2728
from sentry.testutils.cases import TestCase
2829
from sentry.testutils.requests import drf_request_from_request
2930
from sentry.users.models.user import User
@@ -105,6 +106,12 @@ def test_minting_principal_resolves_only_users(self) -> None:
105106
assert unsupported is agent_token.MintingPrincipalRejection.UNSUPPORTED
106107
assert not agent_token.is_mintable_agent_principal(unsupported)
107108

109+
service_account = ServiceAccountPrincipal(
110+
service_account_id=1, organization_id=self.org.id, name="seer-autofix"
111+
)
112+
rejected = agent_token.resolve_minting_principal(service_account, None)
113+
assert rejected is agent_token.MintingPrincipalRejection.UNSUPPORTED
114+
108115
def test_minting_principal_rejects_integrations_and_agents(self) -> None:
109116
integration_user = self.create_user(is_sentry_app=True)
110117
integration = agent_token.resolve_minting_principal(integration_user, None)
Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
src/sentry/api/bases/organizationmember.py
2-
src/sentry/api/endpoints/organization_releases.py
32
src/sentry/core/endpoints/organization_member_details.py
43
src/sentry/core/endpoints/organization_member_index.py
54
src/sentry/core/endpoints/organization_member_requests_invite_index.py
65
src/sentry/data_export/endpoints/data_export.py
76
src/sentry/issues/endpoints/group_integration_details.py
8-
src/sentry/notebooks/endpoints/base.py
9-
src/sentry/seer/endpoints/organization_agent_token.py
10-
src/sentry/web/client_config.py

0 commit comments

Comments
 (0)