Skip to content

fix(token): pre-flight master-token guard on token create/refresh - #647

Merged
padak merged 3 commits into
mainfrom
fix/599-token-create-master-guard
Aug 22, 2026
Merged

fix(token): pre-flight master-token guard on token create/refresh#647
padak merged 3 commits into
mainfrom
fix/599-token-create-master-guard

Conversation

@padak

@padak padak commented Aug 22, 2026

Copy link
Copy Markdown
Member

What

The remaining follow-up from the #599 root-cause investigation: token create now pre-flights the acting token and fails fast with a clean local error when it is not a master (admin) Storage token, instead of letting the Storage API answer a generic 500 Application error..

  • New TokenService._require_master_token() mirrors the existing config oauth-url guard: client.get_project_info() → check isMasterToken → raise KeboolaApiError(status_code=403, error_code=MISSING_MASTER_TOKEN) before any write. The message names the acting token (id + description), explains that canManageTokens alone is not enough (CreateTokenVoter LogicException), and gives the remedy: kbagent project edit --project ALIAS --token <MASTER>.
  • The guard lives in the service layer, so kbagent serve's /token/create route gets it for free. The importable SDK facade (Client.create_scoped_token) intentionally stays unguarded (documented in sdk.md).
  • token list / token delete are deliberately not guarded — the token create: persistent upstream 500 on POST /v2/storage/tokens surfaces as a blind retry + generic error, no actionable guidance #599 A/B verified they work with a non-master canManageTokens token, and a regression test pins that.

Docs wording fix

Every surface claiming the acting token "must carry canManageTokens" for the mint now states the real requirement (master/admin token; canManageTokens is necessary but not sufficient): services/token_service.py, client/tokens.py, commands/token.py, commands/context.py (AGENT_CONTEXT), CLAUDE.md, docs/sdk.md, plugins/.../commands-reference.md, gotchas.md (including the stale issues/<TBD> pointer in the oauth-url gotcha, whose suggested upstream fix #599 proved would 500), and agents/keboola-expert.md (48.9 KB, well under the 70 KB budget).

Versioning

Rebased on main after #643 took the 0.89.0 bump: this PR no longer bumps anything — its changelog entries join the existing (unreleased) 0.89.0 block, and the (since v0.89.0) doc markers resolve against it, satisfying version-gate-check.

Resolved: the guard is scoped to create only

An earlier revision mirrored the guard onto token refresh (the issue's status comment asked for both). Reading RefreshTokenVoter.php (via gh api on keboola/connection) showed that endpoint has no LogicException — it is a well-behaved voter: any token may refresh itself, and refreshing another token needs only canManageTokens, which is why the #599 A/B table shows refresh working with org-setup tokens "used in production for months".

So guarding refresh converted a today-working call into MISSING_MASTER_TOKEN while fixing nothing — and the call it broke is the incident path: rotating a leaked device token from an org-setup project token, which is the last place to add a "go find a master token first" step. @padak decided to scope the guard to the mint; that is what this PR now does (d3a37af). refresh / list / delete do not even pay for the pre-flight verify, which a test asserts.

Trap recorded along the way (pre-existing, unchanged by this PR, now documented in gotchas.md / commands-reference.md / context.py / the command docstring): token refresh prints the new secret but never writes it back to config.json, so rotating the very token an alias uses leaves that alias holding a dead value until kbagent project edit --project ALIAS --token <NEW> runs. Worth a follow-up issue — either persist it or warn.

Tests

TDD (guard tests written first, watched fail): non-master token → MISSING_MASTER_TOKEN, 403, no write reaches the API, client closed; error message carries token id + remedy; master token passes; delete/list remain unguarded. The shared mock fixture now returns explicit master-token info so the pass-path is deliberate rather than a truthy-MagicMock accident. Full make check green (5871 passed).

Closes #599

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Open in Devin Review

Comment thread src/keboola_agent_cli/services/token_service.py Outdated
padak added 2 commits August 22, 2026 23:22
A non-master token carrying canManageTokens is CreateTokenVoter's
'impossible state' -- POST /v2/storage/tokens throws a LogicException
surfaced as a generic 500 'Application error.'. That is exactly the token
shape org setup / project refresh mint, so every project onboarded that
way hit the 500 on token create. Mirror the existing config oauth-url
guard: check isMasterToken via GET /v2/storage/tokens/verify before any
write and raise MISSING_MASTER_TOKEN (403, exit 3) with the project-edit
remedy in the message. token list / token delete stay unguarded -- they
work with canManageTokens alone (verified live in #599).

Also fix the misleading 'acting token must carry canManageTokens' wording
across token_service.py, client/tokens.py, commands/token.py, context.py,
CLAUDE.md, sdk.md and the plugin skill/agent references: the real
requirement for create/refresh is a master/admin token.

Bump to 0.89.0 with changelog entries (the version-gate check requires
the '(since v0.89.0)' doc markers to resolve to a changelog entry).
@padak
padak force-pushed the fix/599-token-create-master-guard branch from 0202df0 to fb3d765 Compare August 22, 2026 21:25
Review decision: the CreateTokenVoter defect #599 is about lives only on
the create endpoint. RefreshTokenVoter lets any token rotate itself and a
canManageTokens token rotate another, so guarding `token refresh` blocked
a path that works today -- most sharply the incident one: rotating a
leaked device token from an org-setup project token. Guarding it fixed
nothing and cost that.

Drops the guard call from refresh_token (and the now single-use `command`
parameter), replaces the two refresh-guard tests with one asserting
refresh/list/delete stay unguarded and do not even pay for the pre-flight
verify, and corrects every doc surface that claimed refresh was guarded.

Also records the pre-existing trap the review surfaced: `token refresh`
prints the new secret but never writes it back to config.json, so
rotating the alias's own token leaves it holding a dead value until
`project edit --token` runs.
@padak
padak merged commit aa3d113 into main Aug 22, 2026
4 checks passed
@padak
padak deleted the fix/599-token-create-master-guard branch August 22, 2026 21:53
padak added a commit that referenced this pull request Aug 23, 2026
…es (#651)

Release prep for 0.89.0: adds the changelog entries for everything merged since v0.88.0 (#620, #642, #643, #644, #645, #646, #647, #648, #649, #650, #517, #586, #641), resolves every vNEXT placeholder left by feature PRs to v0.89.0 per the new #648 release process, closes the 10 gaps a full doc-surface audit found across the kbagent plugin (SKILL.md triggers, commands-reference, gotchas, workflow files, keboola-expert.md, AGENT_CONTEXT, CLAUDE.md), and records the live e2e verification evidence. Version files were already at 0.89.0 (bumped by #643); make version-sync is a no-op.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

token create: persistent upstream 500 on POST /v2/storage/tokens surfaces as a blind retry + generic error, no actionable guidance

1 participant