Repository navigation
Conversation
…oint The viewer-context endpoint now signs a SERVICE_ACCOUNT actor for the account instead of Seer's org-only context. Requests carrying that context authenticate as a userless credential bound to the account's org, with scopes read from the cell replica on every request, and get a new ServiceAccountAccess: every active project with open membership, otherwise only the account's linked active projects. A service-account context can't mint another account's context, and service accounts skip the release permission cache so their IDs don't share keys with API keys. Refs ENG-8829 Co-Authored-By: Shaun Kaasten <shaun.kaasten@sentry.io>
…t-actor Co-Authored-By: Shaun Kaasten <shaun.kaasten@sentry.io>
OrganizationReleasesBaseEndpoint.get_projects only admitted API keys, org tokens, agents and users, so a service account got no projects and has_release_permission never passed. Admit service accounts bound to the organization. Endpoints that default to "my projects" filter by membership, which a service account never has. Treat its linked projects as its membership. Refs ENG-8829
SeerRpcViewerContextAuthentication accepted a service-account ViewerContext like Seer's own org-only one. seer-rpc methods trust their org_id argument and skip access checks, so the account's scopes, project links and active status were not enforced there. Reject service-account actors; HMAC callers are unaffected. This also covers the viewer-context endpoint, so its separate service-account check is no longer needed. Refs ENG-8829 Co-Authored-By: Shaun Kaasten <shaun.kaasten@sentry.io>
Show that the release endpoints' project lookup stays limited to a service account's linked projects, including for $all, and clarify why service accounts skip the release permission cache. Refs ENG-8829
gricha
force-pushed
the
ref/request-principal-interface
branch
from
October 28, 2026 18:56
7090aa2 to
df72683
Compare
gricha
force-pushed
the
feat/service-account-request-principal
branch
from
October 28, 2026 19:04
2c9daae to
e9c5638
Compare
gricha
force-pushed
the
ref/request-principal-interface
branch
from
October 29, 2026 00:37
df72683 to
2bea975
Compare
gricha
force-pushed
the
feat/service-account-request-principal
branch
from
October 29, 2026 00:38
e9c5638 to
73ba3bd
Compare
gricha
changed the base branch from
ref/request-principal-interface
to
ref/request-actor-adoption
October 29, 2026 00:38
Behind auth.service-account-principal (default off), a service account's request.user is a ServiceAccountPrincipal instead of AnonymousUser. Off, it is anonymous, as #128779 has it. set_request_principal applies the option, so there is one place to turn it off. The principal's user_id is None and its actor is SERVICE_ACCOUNT. Its id is a ServiceAccountId that never equals an int and raises UserRequired (403, user-required) when converted to one, so no existing request.user.id call site can reach the user with the same number. The Django-level request.user stays anonymous, so session auth and web views never see the account. SentryIsAuthenticated rejects callers with no user_id; service accounts count as sudo, like org auth tokens, and get their own rate-limit bucket. The stack's shared defaults cover serializers. What's left here is per-site: team and org lookups, update_groups, ScheduledDeletion, Activity and creator attribution, saved queries, dashboards, replay has_viewed and onboarding. Creating a project calls require_user. 14 of the 36 known service-account gaps in the auth matrix now match the oracle, and 5 human-only routes assert user-required instead.
gricha
force-pushed
the
ref/request-actor-adoption
branch
from
October 29, 2026 06:38
621787d to
8cb1029
Compare
gricha
force-pushed
the
feat/service-account-request-principal
branch
from
October 29, 2026 06:39
73ba3bd to
7edc006
Compare
This was referenced Oct 29, 2026
gricha
changed the base branch from
ref/request-actor-adoption
to
ref/request-principal-lints
October 29, 2026 06:40
This was referenced Oct 29, 2026
Contributor
Backend Test FailuresFailures on
|
This was referenced Oct 29, 2026
This was referenced Oct 29, 2026
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
The dashboard creator on the model and the serializer read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
Scheduled deletion actor, onboarding, Tempest credentials, Seer agentic triage and integration direct-enable read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
update_groups' acting user, group integration and external-issue activity, and per-user issue defaults read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
Discover and Explore saved queries, saved formulas, and the events base read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
Org details, org index, teams, project index, Team lookups, org deletion actor and auth_organization_config read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
The detector and workflow creator in the validators read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
The snapshot uploader on preprod snapshot endpoints read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
has_viewed and viewed-by on replay details read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
Org details, org index, teams, project index, Team lookups, org deletion actor and auth_organization_config read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
update_groups' acting user, group integration and external-issue activity, and per-user issue defaults read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
The detector and workflow creator in the validators read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
Discover and Explore saved queries, saved formulas, and the events base read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
The dashboard creator on the model and the serializer read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
The snapshot uploader on preprod snapshot endpoints read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
Org details, org index, teams, project index, Team lookups, org deletion actor and auth_organization_config read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
has_viewed and viewed-by on replay details read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
Scheduled deletion actor, onboarding, Tempest credentials, Seer agentic triage and integration direct-enable read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
The dashboard creator on the model and the serializer read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
The detector and workflow creator in the validators read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
Org details, org index, teams, project index, Team lookups, org deletion actor and auth_organization_config read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
The snapshot uploader on preprod snapshot endpoints read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
Scheduled deletion actor, onboarding, Tempest credentials, Seer agentic triage and integration direct-enable read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
has_viewed and viewed-by on replay details read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
update_groups' acting user, group integration and external-issue activity, and per-user issue defaults read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 29, 2026
Discover and Explore saved queries, saved formulas, and the events base read the caller's human from request.user.user_id instead of request.user.id if request.user.is_authenticated else None, or is_authenticated used to mean "is a person". No behavior change: today only AnonymousUser has no user_id, and every authenticated caller's user_id is its id. Once service accounts can sit on request.user (behind auth.service-account-principal), their user_id is None, so these places record no user instead of reading an id that raises UserRequired. Split out of #128983 so owning teams can review their slice; the core change is #129146. Needs #128982 (user_id on request.user).
gricha
added a commit
that referenced
this pull request
Oct 30, 2026
) On a DRF request, a service account's `request.user` is a `ServiceAccountPrincipal` instead of `AnonymousUser`. Service accounts are gated by `organizations:service-accounts`, which is now checked at authentication too (`authenticate_service_account`), so that flag is the one switch. Before, only service-account creation and the token endpoint checked it, and anything holding the shared secret could sign a service-account viewer context. The principal has no user, so `has_user()` is False and `acting_user_id()` is None, and its actor is `SERVICE_ACCOUNT`. Its `id` is a `ServiceAccountId` that never equals an int and raises `UserRequired` (403 `user-required`) when converted to one, so no `request.user.id` call site can reach the user with the same number. **Temporary:** `ServiceAccountId` is a runtime guard, because about 490 `request.user.id` reads still assume a user. It's deleted, along with the RPC pass-through for `UserRequired`, once mypy types `request.user` to include `ServiceAccountPrincipal` (plugin, shrinking per-module allowlist) and those reads are migrated. This PR sits right above the chokepoint on purpose: every PR after it is written and tested against the real principal. Endpoints aren't blocked up front. What breaks for a service account shows up in the auth matrix, and the PRs above fix it or mark the endpoint `requires_user`. What else this changes: - **Web views:** `set_request_principal` sets a service account only on a DRF `Request` (its setter also copies it to Django's `request.user`). Until DRF authenticates it, Django's `request.user` is anonymous, and session auth never sees it, since `ViewerContextAuthentication` runs before `SessionAuthentication`. The check reads the credential (`is_service_account_auth(auth)`), not `isinstance(user, ...)`: the middleware passes Django's lazy user, and loading it reads the session, which adds `Vary: Cookie` to every response. `test_leaves_lazy_user_unloaded` guards it. - **Access:** `SentryPermission.determine_access` asks `has_user()` and `acting_user_id()`, so a service account keeps the credential-based access (`from_rpc_auth`) it had as `AnonymousUser`, instead of its `ServiceAccountId` reaching the org lookup. - **Sudo:** neither the decorator nor the sudo middleware treats a service account as sudo; both ask `has_user()` before `has_usable_password()`. Sudo re-authenticates a person, so `require_sudo()` answers a caller with no user with a 403 `user-required` instead of a sudo prompt, from `@sudo_required` and from project delete. Org, team and project delete and project transfer stay user-only. - **Rate limits:** service accounts get their own bucket. - **RPC:** `UserRequired` passes through as an API error. The auth matrix enables the flag for its service-account rows and records what still fails at this point in the stack. Rows that don't match the oracle are strict xfails, so the PR that fixes a row has to remove it. At this point 89 service-account rows are known gaps: 28 audit actors, 21 per-user serializer lookups, 20 `request.user.id` reads, 2 `serialize_generic_user`, 1 project creation that needs `require_user`, 12 control-silo endpoints and 5 that check the caller's own membership or org role. The PRs above remove them. At the top of the stack, 17 remain: the control-silo and membership ones. This replaces #128983, split per review. It builds on #128779, now landed. **Stack** (each PR is based on the one above it): 1. #128982 Shared `request.user` interface 2. #129133 Set `request.user` / `request.auth` through one helper 3. #129146 Service accounts on `request.user` 4. #129136 `has_user()` in access, org lookups and `users/me` 5. #129143 `requires_user` / `require_user()` 6. #129144 Lints for patterns that break for a service account 7. #129154 Orgs, teams, projects 8. #129155 Issues 9. #129149 Discover, Explore 10. #129156 Dashboards 11. #129150 Workflow engine 12. #129151 Replays 13. #129152 Preprod 14. #129153 Remaining endpoints 15. #129253 Audit entries 16. #129254 Per-user serializer lookups 17. #129255 `serialize_generic_user` 18. #129256 Flagpole user context
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Behind
auth.service-account-principal(default off), a service account'srequest.useris aServiceAccountPrincipalinstead ofAnonymousUser. With the option off it stays anonymous, as #128779 has it.set_request_principalapplies the option, so there is one place to turn it off.The principal's
user_idis None and its actor isSERVICE_ACCOUNT. Itsidis aServiceAccountIdthat never equals an int and raisesUserRequired(403user-required) when converted to one, so no existingrequest.user.idcall site can reach the user with the same number. Those 403s show up inapi.user_required, tagged by endpoint.The Django-level
request.userstays anonymous, so session auth and web views never see the account.SentryIsAuthenticatedrejects callers with nouser_id. Service accounts count as sudo, like org auth tokens, and get their own rate-limit bucket.What's covered where
update_groups,ScheduledDeletion, Activity and creator attribution, saved queries, dashboards, replayhas_viewedand onboarding. Creating a project callsrequire_user.user-required. The other 17 remain as strict xfails: 12 because service-account auth is cell-only, and 5 because they check the caller's own membership.Merges #128779 (still open) to build on its
ViewerContextAuthenticationandServiceAccountAccess.Stack (each PR is based on the one above it):
request.userinterface andget_current_actorrequest.user/request.auththrough one helperrequest.user.user_idin access, permissions and org lookupsrequires_user/require_user()request.user, behindauth.service-account-principal