Skip to content

feat(delegation): populate the delegation chain so the delegation.* attributes work #138

Description

@terylt

Description

DelegationExtension is fully modelled, fully extracted into the attribute bag, and never written by any shipped plugin. Every delegation.* attribute is therefore constant across every request, and any policy that reads one is inert while looking correct.

The type carries a chain: Vec<DelegationHop> with derived depth, delegated, origin_subject_id, actor_subject_id and age_seconds (crates/ppe-core/src/extensions/delegation.rs). The CMF bridge extracts all of them (crates/ppe-apl-cmf/src/delegation.rs). The executor has a whole monotonic merge path for the slot, merge_delegation (crates/ppe-core/src/extensions/container.rs:411), which validates that a returned chain extends the canonical one and recomputes depth from the chain rather than trusting the plugin. That merge has never run against a real chain.

There are exactly two writers of ext.delegation in the engine:

  • IdentityPayload::apply_to_extensions (crates/ppe-core/src/identity/payload.rs:369), gated on self.delegation being populated by an identity resolver.
  • DelegationPayload::apply_to_extensions (crates/ppe-core/src/delegation/payload.rs:677), gated on self.delegation_update being populated by a TokenDelegate handler.

Neither field has a shipped writer:

  • identity/jwt does not parse act claims. There is no reference to act or to delegation anywhere in builtins/plugins/identity-jwt/src/, and none of the four presets map it.
  • delegator-oauth never appends a hop. No reference to delegation_update, DelegationHop or append_hop in builtins/plugins/delegator-oauth/src/.

The only callers of with_delegation are crates/ppe-apl-cmf/tests/end_to_end.rs and the doc example at crates/ppe-apl-cmf/src/lib.rs:113, both of which construct a chain by hand to exercise the bridge.

What this means in practice

A rule such as require(delegation.depth <= 2) parses, evaluates, and always passes, because depth is either absent or zero. A rule such as delegation.delegated: deny('no re-delegation', 'delegation.forbidden') never fires. This is the failure mode that is worth fixing first: the attributes are documented and reachable, so a policy author has every reason to believe a depth limit is being enforced.

The two halves

Inbound, in identity/jwt. RFC 8693 puts the actor chain in the act claim, nested: act.sub is the immediate actor, and a nested act.act is the one before it. Walking that nesting produces the chain, with origin_subject_id from the token's own sub and the hops in order. This is a claim mapping change in a plugin that already has a claim mapper, so it is the smaller half.

Worth deciding in the issue: whether an act chain is trusted as presented. It is inside a signed token so it is as trustworthy as the issuer, but depth is exactly the kind of value a policy gates on, which makes it worth being explicit rather than implicit.

Outbound, in delegator-oauth. When the delegator mints a token it should append a hop recording what it did: subject_id, audience, scopes_granted, timestamp, strategy, and from_cache. The plugin has all of this already at the point it builds the RawDelegatedToken (builtins/plugins/delegator-oauth/src/delegator.rs:640), including the cache hit flag, which DelegationHop.from_cache exists to carry.

This half needs the append_delegation capability, which the executor already gates on (crates/ppe-core/src/executor.rs:670), and it exercises the merge_delegation extension path for the first time.

Acceptance criteria

  • identity/jwt parses nested act claims into DelegationExtension.chain and sets it on IdentityPayload.delegation.
  • delegator-oauth appends a DelegationHop for each token it mints, with from_cache reflecting whether the token was served from cache, and declares append_delegation.
  • delegation.depth, delegation.delegated, delegation.origin_subject_id and delegation.age_seconds resolve to real values in an APL predicate.
  • A test covers merge_delegation rejecting a shortened chain, which is currently unreachable in practice.
  • A test covers a depth limit denying a request whose inbound token carries a chain deeper than the limit.
  • Whether an act chain is trusted as presented is recorded as a decision.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions