Skip to main content
Back to blog

A Token Should Declare Intent and Let IAM Decide at Call Time

Amanpreet Kaur
Amanpreet Kaur Engineer · Zop.Dev
7 min read
A Token Should Declare Intent and Let IAM Decide at Call Time

A Token Should Declare Intent and Let IAM Decide at Call Time

A capability checkbox on a token creation screen can mean one of two things, and product teams routinely ship both at once without noticing. Get it wrong across a 289-tool catalogue where 43% of the tools write, and the mistake is not one checkbox.

It can mean this token is permitted to do X, in which case ticking it is an act of authorisation and the screen had better check whether you are allowed. Or it can mean this token will attempt X, in which case ticking it is a declaration of intent and the real check happens somewhere else entirely.

ZopNight’s Personal Access Token matrix was doing the first while the system was doing the second. The frontend disabled any capability cell the caller did not currently hold. The gateway, meanwhile, was already checking every call against the token’s scope, the caller’s live org role, and the org’s MCP write tier. The frontend gate protected nothing. It just made some tokens impossible to mint.

It is gone, and the matrix now lets you select any of the capabilities behind a 289-tool catalogue, 124 of them writes.

A Token Scope Is a Declaration of Intent, Not a Grant

Declare-then-enforce is the model where a credential records what it will attempt, and every attempt is separately checked against the authorisation state that is live at the moment of the call rather than the state that existed when the credential was created.

Under that model a token scope is a ceiling, not a key. It says what this credential may ever try. It says nothing about whether the try will succeed, because the answer to that changes when someone’s role changes, when the org’s write tier changes, or when a policy entry is corrected.

The alternative model, where the checkbox is the grant, has a structural problem that shows up the first time anything changes: the token now encodes a snapshot of an authorisation state that has moved on. If the user’s role was reduced, the token is over-permissioned unless something else re-checks. And if something else re-checks, the checkbox was never the grant in the first place.

RBAC is role-based access control: permissions attach to named roles rather than to individuals, so what a caller may do is whatever the roles they hold at that moment allow, and it changes when the roles do. That is the position ZopNight was in. Two checks existed, only one was authoritative, and the non-authoritative one was in the interface where it looked the most convincing. Duplicating a permission check in the interface and at the gateway is not defence in depth. It is one enforcement point and one thing that drifts.

Mint Time Is the Wrong Moment to Answer Whether Something Is Allowed

Every MCP call is evaluated against a three-way intersection, and each of the three moves independently.

Architecture diagram

Two of the three are not knowable at mint time in any useful sense. A role gets elevated when someone joins the on-call rotation. A write tier gets enabled when a team decides to let agents act. Both of those happen after tokens exist, and neither should require touching a credential.

Freezing an answer at mint time means computing the intersection once, against the state of a Tuesday afternoon, and then carrying that answer around for as long as the token lives. The catalogue those tokens address stands at 289 tools, 124 of them writes across three tiers. A snapshot over a surface that size ages badly.

Mint-Time Gating Produces Token Sprawl at the Worst Moment

The practical cost of gating at mint time is not the blocked checkbox. It is the rotation the block forces later.

SituationUnder mint-time gatingUnder call-time enforcement
A user’s role is elevatedExisting token cannot use the new capability; mint a replacementThe existing token starts working
The org enables a write tierEvery token minted before the change needs replacingExisting tokens gain the tier’s tools
A user’s role is reducedOld token still carries the wider snapshot unless revokedCalls start failing immediately
A token is minted “just in case”Impossible; so people mint broader tokens under broader accountsScope the token to the task, let RBAC decide

Read down the first column and the same failure repeats: a change in the world produces a new credential. Every one of those is a long-lived secret, created by somebody under time pressure, pasted into a configuration file, and never revoked because nobody is certain what stopped depending on it.

The third row is the one that ends badly. Mint-time gating gives its weakest answer exactly where you most want strength, on de-escalation, because a snapshot taken while the role was wide stays wide. Call-time enforcement fails the call the moment the role narrows, with no revocation step and no window.

There is a smaller benefit worth naming: a token can be minted narrow for a task the person is not yet entitled to run, reviewed, and then activated by elevating the role. The credential and the entitlement become two separate decisions, made by two people, at two times. That is a better shape than one person holding both.

A View That Hides Capabilities the Token Holds Is a Governance Failure

The second half of the change is quieter and matters more for anyone doing access reviews. The view-only matrix, the one used to inspect an existing token, was rendering at the org’s current write tier. The write tier is off by default and moves over time.

Token holdsMatrix rendered at current tierMatrix rendered at the highest tier
A tier 1 capability, org at tier 1ShownShown
A tier 3 capability, org now at tier 1HiddenShown, with its tier
A tier 2 capability, org write tier disabledNothing rendered at allShown

The middle row is the problem. A token holding a capability above the org’s current tier displayed as though it did not hold it. The capability is still in the token. If the tier is raised later, the capability becomes live, and nobody reviewing that token in the meantime had any way to see it coming.

An audit surface narrower than the access surface is worse than no audit surface, because it produces confident wrong answers. Somebody reviews the token, sees three capabilities, signs off, and the token holds five. The matrix now renders at the highest tier in both the editable and view-only forms, so what the token holds is always what the screen shows.

Delete the Dead Gate, Do Not Leave It Disabled

The frontend gate was removed rather than switched off. The helper functions behind it were deleted outright instead of being left in place unused, and the column select-all control now fills every grantable cell rather than the subset the caller held.

That is the right disposition for a redundant check, and it is the one teams most often skip. A disabled gate left in the code reads to the next person as a control, gets referenced in a review as though it enforces something, and eventually gets re-enabled by someone fixing an unrelated bug. Dead security code is not neutral. It is a claim the system makes about itself that is no longer true.

What replaced it is a single sentence in the interface: capabilities are verified against your role and your org’s write tier at call time. It renders once, and it stays visible while the request is in flight, which is precisely when a user is wondering whether the thing they just ticked will work.

This works when every call is checked against live state. It fails the moment any layer caches the role decision, because a cached decision is a mint-time snapshot wearing call-time clothes. Telling the user where enforcement happens is the part that makes the whole model legible. A checkbox that silently means “intent” looks identical to a checkbox that means “grant”. The difference only becomes visible when something changes, and by then the person who ticked it has moved on.

Tagged
Amanpreet Kaur

Amanpreet Kaur

Engineer · Zop.Dev

Amanpreet works on Zop.Dev's cloud-cost engine, focused on commitment optimization and right-sizing across AWS, GCP, and Azure. She writes about Savings Plans vs RIs, break-even math, and the gnarly edges of multi-cloud cost data.

Stop watching the waste.
Start cutting it.

See. Find. Fix. Automatic.

Connect your first cloud account in under 5 minutes. See your first remediation in under 7. No credit card required.

CDCR connect detect classify remediate
full audit every action traceable
read-only default access
Multi-cloud automation· Production-ready in 30 min· SOC 2 · ISO 27001· 20–60% off the bill, first month· 4 platforms · 1 console· Multi-cloud automation· Production-ready in 30 min· SOC 2 · ISO 27001· 20–60% off the bill, first month· 4 platforms · 1 console·