Skip to content

feat(service-accounts): Add an endpoint to revoke service account tokens - #130508

Merged
skaasten merged 5 commits into
masterfrom
feat/service-account-token-revoke
Nov 13, 2026
Merged

skaasten merged 5 commits into
masterfrom
feat/service-account-token-revoke

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Adds DELETE /organizations/{org}/service-accounts/{id}/tokens/{token_id}/. This is the last of three small token endpoint PRs, one per endpoint:

  1. List tokens: feat(service-accounts): Add an endpoint to list service account tokens #130483
  2. Create a token: feat(service-accounts): Add an endpoint to create service account tokens #130507
  3. Revoke a token (this PR)

The endpoint needs service_account:write. It locks the token row, sets date_deactivated and saves it, so the token's outbox updates the replica in the org's cell. It returns 204. The token must belong to the account in the URL, and the account must be active and in the URL's org. Otherwise, or if the token is already revoked, it returns 404.

It uses the same guards as create: only a person signed in as themselves can call it, and Seer agent tokens and impersonation sessions are refused.

Each revoke writes a SERVICE_ACCOUNT_TOKEN_REMOVE (227) audit log entry. Its data holds the token name, scopes and account, never the token or its hash.

The settings UI's Revoke button in #130485 calls this endpoint.

Tests: the revoke test drains the token's outbox and checks that the cell's ServiceAccountTokenReplica gets the token's new date_deactivated. This works because #130265, the token replication handler, is merged.

via @skaasten.

--

View Session [Sentry]

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

🚨 Warning: This pull request contains Frontend and Backend changes!

It's discouraged to make changes to Sentry's Frontend and Backend in a single pull request. The Frontend and Backend are not atomically deployed. If the changes are interdependent of each other, they must be separated into two pull requests and be made forward or backwards compatible, such that the Backend or Frontend can be safely deployed independently.

Have questions? Please ask in the #discuss-dev-infra channel.

@sentry

sentry Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Sentry Snapshot Testing

Name Added Removed Changed Renamed Unchanged Skipped Status
sentry-frontend
sentry-frontend
0 0 0 0 934 0 ✅ Unchanged

⚙️ sentry-frontend Snapshot Settings

@sentry-junior
sentry-junior Bot force-pushed the feat/service-account-token-revoke branch from 1331912 to 7493553 Compare November 10, 2026 15:46
skaasten pushed a commit that referenced this pull request Nov 10, 2026
#130483)

Adds a new endpoint for listing service account tokens, gated by service
account feature flag

------

Adds `GET /organizations/{org}/service-accounts/{id}/tokens/`. This is
the first of the small token endpoint PRs, one per endpoint. It's
stacked on #130456, which adds token generation:

1. **List tokens (this PR)**
2. Create a token: #130507
3. Revoke a token: #130508

The endpoint runs in control, is private, and is gated on
`organizations:service-accounts` like the other service account
endpoints. It needs `service_account:read`.

It returns the account's active tokens, newest first: `id`, `name`,
`scopes`, `tokenLastCharacters`, `dateCreated`, `dateLastUsed`,
`dateExpires`. Revoked tokens aren't listed, and the response never
includes a token's value. Pagination works the same way as the service
account list. The account in the URL must be active and in the URL's
org, otherwise it returns 404.

Only a person signed in as themselves can call it. A service account
gets 403 `user-required` (`requires_user`).

The account lookup from the details endpoint moved into a shared
`ServiceAccountScopedEndpointBase`. The details endpoint behaves the
same.

The new URL is added to
`static/app/utils/api/knownSentryApiUrls.generated.ts`, so the frontend
PR (#130485) can type-check against it.

<!-- junior-request-attribution:start -->
via @skaasten.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:slack%3AC0CA89MBDQB%3A1793911581.772709 -->

--

[View
Session](https://junior-prod.sentry.dev/conversations/slack%3AC0CA89MBDQB%3A1793911581.772709)
[[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3AC0CA89MBDQB%3A1793911581.772709/?project=4510944073809921)

<!-- junior-session-footer:end -->

---------

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
@skaasten
skaasten force-pushed the feat/service-account-token-revoke branch from 7493553 to 471213f Compare November 10, 2026 17:18
@skaasten
skaasten force-pushed the feat/service-account-token-revoke branch from 471213f to 94362a8 Compare November 12, 2026 14:32
@skaasten
skaasten marked this pull request as ready for review November 12, 2026 14:33
@skaasten
skaasten requested review from a team as code owners November 12, 2026 14:33
@sentry-junior
sentry-junior Bot force-pushed the feat/service-account-token-revoke branch 2 times, most recently from ed0cd62 to 7a7c2fd Compare November 12, 2026 17:53
@sentry-junior
sentry-junior Bot force-pushed the feat/service-account-token-revoke branch from 7a7c2fd to 31c3901 Compare November 12, 2026 17:58
@skaasten
skaasten force-pushed the feat/service-account-token-revoke branch from 31c3901 to e5e709a Compare November 13, 2026 14:19
Base automatically changed from feat/service-account-token-create to master November 13, 2026 15:22
skaasten pushed a commit that referenced this pull request Nov 13, 2026
…ens (#130507)

Adds a new endpoint for creating a service account access token

We ensure the scopes for the token do not exceed the scopes for the
current user.

------------------


Adds `POST /organizations/{org}/service-accounts/{id}/tokens/`. This is
the second of three small token endpoint PRs, one per endpoint:

1. List tokens: #130483
2. **Create a token (this PR)**
3. Revoke a token: #130508

The endpoint needs `service_account:write` and takes `{name, scopes,
expiresAt}`:

- Each scope must already be granted to the account, otherwise it
returns 400 with an error on `scopes`.
- `expiresAt` defaults to 30 days from now (`DEFAULT_TOKEN_LIFETIME`). A
value you set must be in the future, otherwise it returns 400 with an
error on `expiresAt`. Send `null` for a token that never expires.
- The token is generated with `generate_token(org slug, cell locality
URL)`, using the same cell URL lookup as org auth tokens. Only the hash
and the last 4 characters are stored.
- The 201 response is the list item plus `token`. It's the only response
that includes the token value.

Seer agent tokens and impersonation sessions can't create tokens
(`DisallowAgentTokenWrites`, `DisallowImpersonatedTokenCreation`). A
service account gets 403 `user-required`, the same as on the list
endpoint.

Each create writes a `SERVICE_ACCOUNT_TOKEN_ADD` (226) audit log entry.
Its data holds the token name, scopes and account, never the token or
its hash.

Review notes:
- This stores `date_expires` but doesn't enforce it. Nothing
authenticates service account tokens on requests yet, so the
authenticator that comes later must reject a token once `date_expires`
has passed.
- `ServiceAccountTokenEndpointBase.permission_classes` is a longer tuple
than its parent's, so
`OrganizationServiceAccountEndpointBase.permission_classes` now has an
explicit `tuple[type[BasePermission], ...]` type for mypy.

<!-- junior-request-attribution:start -->
via @skaasten.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:slack%3AC0CA89MBDQB%3A1793911581.772709 -->

--

[View
Session](https://junior-prod.sentry.dev/conversations/slack%3AC0CA89MBDQB%3A1793911581.772709)
[[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3AC0CA89MBDQB%3A1793911581.772709/?project=4510944073809921)

<!-- junior-session-footer:end -->

---------

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
@skaasten
skaasten force-pushed the feat/service-account-token-revoke branch from e5e709a to 3cb50c1 Compare November 13, 2026 15:22
sentry-junior Bot and others added 5 commits November 13, 2026 10:38
DELETE /organizations/{org}/service-accounts/{id}/tokens/{token_id}/ marks the
token deactivated. It stops working once the change replicates to the org's
cell. Each revoke writes a SERVICE_ACCOUNT_TOKEN_REMOVE audit log entry.
#130265 replicates service account tokens through their outbox, so the
revoke test can check the cell replica's date_deactivated instead of
asserting that the replication handler ran.

Co-Authored-By: Shaun Kaasten <900809+skaasten@users.noreply.github.com>
The revocation is committed before replication, so a failed flush shouldn't
turn a successful request into a 500; the outbox drains later. Also use get()
instead of first() to avoid an ORDER BY on the lookup.
Revocation should still flush to the cell right away, but a replication failure
shouldn't fail a revocation that's already committed. Catch OutboxDatabaseError
and OutboxFlushError after the save, like ApiTokensEndpoint.post. The revoke test
no longer wraps the request in outbox_runner, so it checks the flush.
@skaasten
skaasten force-pushed the feat/service-account-token-revoke branch from 3cb50c1 to 5871e01 Compare November 13, 2026 15:38
@skaasten
skaasten enabled stacked auto-merge (squash) November 13, 2026 15:40
@skaasten
skaasten merged commit 3f808a6 into master Nov 13, 2026
107 checks passed
@skaasten
skaasten deleted the feat/service-account-token-revoke branch November 13, 2026 15:44
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 Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants