Run grants and run tokens
Table of Contents
1. Summary
This page states the target design. It does not describe what is built.
This page is IAM's implementation of the Scheduled Work Acting as a Tenant pattern. It is the same for every component that uses the pattern. A run grant is IAM's form of Grant Management: a person's consent that runs of one resource act in one tenant and one party, with one role. A run token is the result of Token Exchange: a step service presents its own token and a grant id, and receives a token that lasts five minutes. Each step service keeps run tokens in a memory cache.
The pattern pages hold the mechanism, the failure handling, the pitfalls and the sources. This page holds IAM's choices: the entity, the operations, the checks, the claims and the cache rules. A component that uses run grants states only its own choices, which are its role, its audience, its resource name and the moments it grants and revokes. Report execution at scale is the first such component.
2. Detail
2.1. The operations
| Subject | Caller | Authenticated as | What it does |
|---|---|---|---|
iam.v1.run_grants.put |
the owning service | the person, by Token Relay | creates a grant, or returns the active one for the same resource and grantor |
iam.v1.ops.revoke_run_grant |
the owning service | itself, or the person | ends a grant |
iam.v1.run_grants.get |
the owning service, an administrator | itself, or the person | reads a grant and its state |
iam.v1.ops.exchange_run_grant |
a step service in the audience | itself, by Client Credentials | returns a run token |
2.2. The run grant
A run grant is an IAM entity. It holds no secret.
| Field | Meaning |
|---|---|
id |
the grant id; a random uuid |
tenant_id, party_id |
where the runs act |
grantor_account_id |
the person who gave consent |
role |
the role a run token carries; the owning component defines it |
resource |
what the grant serves, as <component>.<entity>/<id> |
audience |
the services that may exchange it |
max_runs |
empty for a standing grant; one for a single manual run |
not_after |
empty for a standing grant; the longest duration of a single run |
revoked_at, revoked_by, revoke_reason |
set when the grant ends |
The grant is a bitemporal row with the audit envelope of every entity. IAM owns it, so IAM never reads another component's table to check it. The owning component stores the grant id beside the work it authorises.
At create, IAM checks that the person holds every permission of the role in the party. A person cannot grant more than they hold. A second create for the same resource and grantor returns the active grant, so a retry is safe.
Figure 1: The run grant: created by a consent, ended by a revocation, by its last run, or by its time limit. A lapsed grantor is a check at each exchange.
Source: run_grant_states.puml. Regenerate with
plantuml -tpng doc/knowledge/security/run_grant_states.puml.
A revocation reaches a run in flight within one token lifetime. That bound is deliberate. A shorter bound needs a check on every request, and the cost of that grows with every call of every run.
2.3. The exchange
A step service calls iam.v1.ops.exchange_run_grant with its own token. The request
carries the grant id, the run id and the tenant from the step headers. IAM
checks, in this order:
- The caller holds
iam::run_grants:exchange. - The caller's service name is in the grant's audience.
- The grant exists, is not revoked, and is inside
not_afterandmax_runs. - The grant's tenant equals the tenant in the request.
- The grantor is active.
The caller's service name is the <Component>Service role its account holds,
so the audience carries role names such as ReportingService and not host
names. A grant's max_runs is the number of distinct run ids the issue log
holds as issued for it, so an exchange that refreshes a token mid-run does not
spend another run.
IAM then computes the permissions: the role's permissions, intersected with the
permissions the grantor holds in the party now. A grantor who loses a permission
loses it for their runs too, and a grantor who loses all of them makes the
exchange answer forbidden with the reason grant_lapsed. IAM appends one row
to the issue log and signs the token.
| Claim | Value |
|---|---|
sub |
the grantor's account id |
username |
the grantor's username, so audit columns name a person |
tenant_id |
the grant's tenant |
party_id |
the grant's party |
visible_party_ids |
the grant's party only |
roles |
the computed permission codes |
act |
the requesting service; the actor claim of RFC 8693 |
grant_id, run_id |
the grant, and the run the token serves |
aud |
the requesting service |
exp |
five minutes after issue |
session_id |
absent; a run token opens no session |
A run token creates no session row, so the sessions table does not grow with the number of runs.
Figure 2: A step obtains the run token from its cache or from IAM, an owner reads as the tenant and the party, and an expired token is exchanged again once.
Source: run_token_exchange.puml. Regenerate with
plantuml -tpng doc/knowledge/security/run_token_exchange.puml.
2.4. How a data owner sees a run token
The step sends the run token as a plain Authorization header. The token is an
originated identity, not a relayed one.
The owner's handler calls make_request_context as it does for any caller. The
context has the tenant, the party, the one visible party and the role's
permissions. The handler checks the permission, and row-level security returns
the party's rows and the shared rows of the system tenant. No owner has a code
path for runs.
An owner that writes stamps modified_by with the grantor's username, as for any
write by that person, and performed_by with its own service account, as for
every write. The change reason names the run. An auditor sees who consented,
which service wrote, and which run caused it. An owner that calls a further
service relays the run token like any caller's token.
2.5. The run token cache
Each step service keeps one cache in memory per process. The cache is the composite's third part, and its rules are these:
- Key. The pair of grant id and run id. Two runs under one grant use two
tokens, so the
run_idclaim stays exact. - Lifetime. A token is used while it has more than 20% of its life left. The next request after that exchanges again. Concurrent misses share one exchange.
- Work longer than a token. A step whose work outlives its token keeps exchanging, so a run of hours lives through many tokens. An actor that cannot re-exchange holds no run token at all and acts under its own identity, as the grid's worker node does in Report execution at scale.
- Expiry in flight. An owner that answers
token_expiredcauses one invalidation, one exchange and one retry of the same request. Any other refusal fails the request. - Eviction. The cache drops a run's entries when the step completes and when the run reaches a terminal state. It is also a bounded LRU, so a missed event costs memory for at most one token lifetime.
- No persistence. The cache is never written to disk, a database, a message or a log. A restart empties it.
- Replicas. Replicas do not share the cache. Each replica exchanges on its first miss.
Figure 3: One entry of the run token cache: exchanged on the first miss, refreshed when stale or refused as expired, and evicted when the step or the run ends.
Source: run_token_cache_states.puml. Regenerate with
plantuml -tpng doc/knowledge/security/run_token_cache_states.puml.
A run outlives many tokens, and the exchange bounds how long a revocation takes to reach it. The sequence below shows a step service exchanging again while it works, and the engine's own work acting without a run token.
Figure 4: A long step re-exchanges while it works, and a worker node uses its own identity. Keyed by grant id and run id, so the whole run spends one of the grant's runs.
Source: run_token_long_run.puml. Regenerate with
plantuml -tpng doc/knowledge/security/run_token_long_run.puml.
2.6. Load on IAM
A run makes one exchange per step service and per five minutes of that service's work. A run of four steps that lasts thirty minutes makes about seven exchanges. An exchange reads one grant and the grantor's permissions, then signs. IAM keeps grants and permissions in memory, and change events on those entities invalidate them. An RS256 signature costs about a millisecond, so one IAM replica serves thousands of exchanges a minute. IAM limits exchanges per service and per tenant, so a fault in one caller cannot exhaust it.
2.7. Trust boundaries
| Threat | What stops it |
|---|---|
| A service exchanges a grant it should not use | the audience; the permission iam::run_grants:exchange |
| A caller guesses a grant id | ids are random; the caller must also be a service in the audience |
| A run reads another party's data | the token's visible_party_ids is one party; row-level security |
| A run writes data its role does not cover | the role; the owner's permission check |
| A person grants more than they hold | IAM checks the grantor at create and at every exchange |
| A token leaks from a log or a row | no token is written; a leaked token expires in five minutes and names its run |
| A grantor leaves the firm | the next exchange answers grant_lapsed |
| A step service is compromised | it can act only for grants that name it, and the issue log records each token |
2.8. What a component that uses run grants states
- The role, with each permission and why the run needs it.
- The resource name of its grants.
- The audience: the step services that exchange.
- When it creates, revokes and re-creates a grant, and what it does on
grant_lapsed.
3. Patterns used
- Scheduled Work Acting as a Tenant — this page is IAM's implementation of it.
- Grant Management — the run grant is IAM's grant.
- Token Exchange —
iam.v1.ops.exchange_run_grantis the exchange, with delegation semantics. - Client Credentials — a step service proves itself to the exchange with its own token.
- Throttling — IAM limits exchanges per service and per tenant.
- Cache-Aside — the run token cache exchanges on a miss and keeps the token until shortly before it expires.
4. See also
- Identity and Access — the structure note that orders this cluster, and where to read this page in it.
- Scheduled Work Acting as a Tenant — the pattern this page implements.
- Report execution at scale — the first component to use run grants.
- The identity an adapter runs as — contradicts: a market data adapter acts as no tenant and needs no grant.