Skip to content

feat(service-accounts): Add an endpoint to create service account tokens - #130507

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

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

Conversation

@sentry-junior

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

Copy link
Copy Markdown
Contributor

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: feat(service-accounts): Add an endpoint to list service account tokens #130483
  2. Create a token (this PR)
  3. Revoke a token: feat(service-accounts): Add an endpoint to revoke service account tokens #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.

via @skaasten.

--

View Session [Sentry]

@sentry

sentry Bot commented Nov 9, 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

@skaasten
skaasten force-pushed the feat/service-account-token-create branch 2 times, most recently from 8f3e312 to 016d4d5 Compare November 10, 2026 15:33
Base automatically changed from feat/service-account-token-endpoints to master November 10, 2026 17:18
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-create branch from 3067ad7 to 0f26e00 Compare November 10, 2026 17:18
@skaasten
skaasten marked this pull request as ready for review November 10, 2026 17:25
@skaasten
skaasten requested a review from a team as a code owner November 10, 2026 17:25
@skaasten
skaasten force-pushed the feat/service-account-token-create branch from 0f26e00 to 277a4e1 Compare November 12, 2026 14:32
@skaasten
skaasten requested a review from gricha November 12, 2026 17:33
@gricha
gricha removed their request for review November 12, 2026 18:49
@gricha

gricha commented Nov 12, 2026

Copy link
Copy Markdown
Member

I'll remove myself as a reviewer, I'm supportive but I want this to be reviewed by Michelle

@skaasten
skaasten requested a review from markstory November 12, 2026 19:38
POST /organizations/{org}/service-accounts/{id}/tokens/ mints a token that
acts as the service account and returns its value once. The token's scopes
must already be granted to the account.

expiresAt defaults to 30 days from now. A set value must be in the future,
and null creates a token that never expires.

Seer agent tokens and impersonating admins can't create tokens. Each create
writes a SERVICE_ACCOUNT_TOKEN_ADD audit log entry.
A token is a bearer credential returned to whoever creates it, so a manager
could mint a token carrying scopes they lack, such as org:admin, from an
account an owner set up. Requested scopes must now be on the account and
held by the caller.
The token is committed before replication, so a failed flush shouldn't turn a
successful create into a 500 that drops the only copy of the plaintext token.
The outbox drains later and the token reaches the cell then.
Turning off the flush stopped a replication failure from returning a 500, but it
also left the token waiting for the scheduled outbox drain before it reached the
cell. Flush after the commit instead, and log a failure rather than raising it.
Flush in the transaction as usual and catch OutboxDatabaseError and
OutboxFlushError once the token is bound, the same way ApiTokensEndpoint.post
does, instead of a separate post-commit drain helper.
@skaasten
skaasten force-pushed the feat/service-account-token-create branch from c6bc547 to 41c18dd Compare November 13, 2026 14:19
@skaasten
skaasten merged commit afad3b8 into master Nov 13, 2026
98 checks passed
@skaasten
skaasten deleted the feat/service-account-token-create branch November 13, 2026 15:22
skaasten added a commit that referenced this pull request Nov 13, 2026
…ens (#130508)

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: #130483
2. Create a token: #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.

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

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

--

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

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

---------

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: Shaun Kaasten <900809+skaasten@users.noreply.github.com>
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants