Skip to content

Redact script-token bearer secret from read paths (BUG-285640)#1948

Open
yongwen wants to merge 1 commit into
masterfrom
bug-285640-redact-script-token-disclosure
Open

Redact script-token bearer secret from read paths (BUG-285640)#1948
yongwen wants to merge 1 commit into
masterfrom
bug-285640-redact-script-token-disclosure

Conversation

@yongwen
Copy link
Copy Markdown
Member

@yongwen yongwen commented May 13, 2026

Previously, every script-token read path returned the raw bearer secret as part of TokenRolesBean. Anyone with the legacy READ role on an environment could enumerate all tokens via
GET /v1/envs/{env}/token_roles and impersonate the script principal, which effectively allowed privilege escalation since some tokens carry ADMIN role.

Defense-in-depth changes:

Backend (deploy-service)

✅ Rec #1 — Redact token on read operations

  • TokenRolesBean.token is now @JsonProperty(access = WRITE_ONLY) so it is never serialized into HTTP responses. toString() excludes the field to keep logs and exception traces clean.

✅ Rec #2 — Elevate @RolesAllowed on GET endpoints

  • New role MANAGE_SCRIPT_TOKEN (privilege level 15) added between OPERATOR and ADMIN. All EnvTokenRoles and SystemTokenRoles endpoints (read AND mutate) now require this role instead of READ/WRITE/DELETE. In Pastis, only admin and envOwner are granted it; legacyOperator, envMember, deployer, and reader no longer see token metadata.

✅ Rec #3 — Stop using SELECT * for token queries

  • DBTokenRolesDAOImpl selects a SAFE_COLUMNS list (no token) for getByResource / getByNameAndResource. The authentication lookup getByToken still does SELECT * because the bearer comparison needs the column.

✅ Rec #4 — Audit the creation response

  • TokenRoles.create() now returns a new CreatedTokenRolesResponse DTO that includes the raw token. This is the only response shape that contains the secret, and it is disclosed exactly once on the 201 Created reply to the caller.

Test:

  • Added tests to cover token redaction at both DAO and bean levels.

Frontend (deploy-board)

  • Removed the now-broken Show Token button and its get_user_token view/URL; the backend no longer exposes the value.
  • update_users_config captures the one-time token from each POST /token_roles response and returns them to the browser.
  • New non-dismissible modal displays freshly minted tokens once with per-row copy-to-clipboard; values are scrubbed from the DOM on acknowledgement. Deprecation banner updated to make the one-shot disclosure model explicit.
Screenshot 2026-05-13 at 12 26 47 AM Screenshot 2026-05-13 at 12 27 34 AM

Docs

  • Added MANAGE_SCRIPT_TOKEN to the Teletraan auth action enum so external authorizers (Pastis) accept it.

@yongwen yongwen requested a review from a team as a code owner May 13, 2026 04:59
@github-actions github-actions Bot added deploy-service Includes changes to deploy-service deploy-board Includes changes to deploy-board labels May 13, 2026
Previously, every script-token read path returned the raw bearer
secret as part of `TokenRolesBean`. Anyone with the legacy READ role
on an environment could enumerate all tokens via
`GET /v1/envs/{env}/token_roles` and impersonate the script principal,
which effectively allowed privilege escalation since some tokens carry
ADMIN role.

Defense-in-depth changes:

Backend (deploy-service)
- `TokenRolesBean.token` is now `@JsonProperty(access = WRITE_ONLY)` so
  it is never serialized into HTTP responses. `toString()` excludes the
  field to keep logs and exception traces clean.
- `DBTokenRolesDAOImpl` selects a `SAFE_COLUMNS` list (no `token`) for
  `getByResource` / `getByNameAndResource`. The authentication lookup
  `getByToken` still does `SELECT *` because the bearer comparison
  needs the column.
- `TokenRoles.create()` now returns a new `CreatedTokenRolesResponse`
  DTO that includes the raw token. This is the *only* response shape
  that contains the secret, and it is disclosed exactly once on the
  `201 Created` reply to the caller.
- New role `MANAGE_SCRIPT_TOKEN` (privilege level 15) added between
  OPERATOR and ADMIN. All `EnvTokenRoles` and `SystemTokenRoles`
  endpoints (read AND mutate) now require this role instead of
  READ/WRITE/DELETE. In Pastis, only `admin` and `envOwner` are granted
  it; `legacyOperator`, `envMember`, `deployer`, and `reader` no
  longer see token metadata.
- Tests cover token redaction at both DAO and bean levels.

Frontend (deploy-board)
- Removed the now-broken `Show Token` button and its
  `get_user_token` view/URL; the backend no longer exposes the value.
- `update_users_config` captures the one-time token from each
  `POST /token_roles` response and returns them to the browser.
- New non-dismissible modal displays freshly minted tokens once with
  per-row copy-to-clipboard; values are scrubbed from the DOM on
  acknowledgement. Deprecation banner updated to make the one-shot
  disclosure model explicit.

Docs
- Added `MANAGE_SCRIPT_TOKEN` to the Teletraan auth action enum so
  external authorizers (Pastis) accept it.

Co-authored-by: Cursor <cursoragent@cursor.com>
@yongwen yongwen force-pushed the bug-285640-redact-script-token-disclosure branch from 74f99bd to 9ed9d58 Compare May 13, 2026 05:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deploy-board Includes changes to deploy-board deploy-service Includes changes to deploy-service

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant