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.

run_grant_states.png

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:

  1. The caller holds iam::run_grants:exchange.
  2. The caller's service name is in the grant's audience.
  3. The grant exists, is not revoked, and is inside not_after and max_runs.
  4. The grant's tenant equals the tenant in the request.
  5. 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.

run_token_exchange.png

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_id claim 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_expired causes 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.

run_token_cache_states.png

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.

run_token_long_run.png

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

4. See also

Emacs 29.3 (Org mode 9.6.15)