resources carry their own kind and id, and authority is checked pair by pair #54

Merged
larandar merged 3 commits from tango/54-resource-kinds into fastlane 2026-09-06 10:24:17 +00:00
Owner

The consumer half of the freeholder's ruling (2026-09-06): "just update the structure, Vedanta is still pre v0.1." The producer already emits it — nixops4-providers#47, merged.

Why

DeployedGrant carried two flat sets, repositories and operations. A grant of A/read plus B/write flattened to {A,B} × {read,write}which is write on A, an authority the grant never gave. The reviewer found it by construction, and the first fix was to refuse grants the flat schema could not represent. The freeholder ruled the structure should change instead, and then improved on the queen's proposal: carry the grant's own vocabulary rather than invent a pairing shape.

"resources": [ { "kind": "forgejo.credential", "id": "swarm-alpha/forge-read", "operations": ["repository:read"] } ]

What lands

  • ResourceGrant { kind, id, operations } replaces the flat pair on LifecycleCommand, DeployedProjection and DeployedGrant. ProjectionFile::parse refuses an unknown kind and a duplicate (kind, id) at load, rather than at use.
  • The validator compares pairwise. Each requested operation is checked against that resource's authorized set, never against a union. Both crates carry a test proving a request for write on A is denied even when the totals match — the exact case a union comparison passes.
  • The plugin's Projection and its Resource and Operation stages are paired the same way, so OpenBao authorizes on the same footing Vedanta enforces.
  • native.rs routes by kind. forgejo.credential and forgejo.repository to the Forge issuer, kanidm.credential to the directory, and an unknown or mixed kind is refused, never defaulted.

A bug this fixed on the way past

Routing used to key off emptiness: no repositories meant "directory credential". Invalidate carries no resources by design — so every invalidation silently went to the directory issuer regardless of which one had minted the token. A Forge token could not be invalidated through that path at all. Kind-based routing takes an Invalidate's kind from the predecessor token's own shape instead, and the class of bug disappears with the heuristic that caused it.

Nobody was looking for that. It fell out of removing an implicit rule.

Evidence

fmt and clippy -D warnings clean. 219 lib tests, 76 in the plugin, and the live bao_dev acceptance test passing un-ignored against a real bao server -dev — re-run by the queen after the push. No estate service was touched.

Three things a reviewer must settle

  1. flake-ops#426's fixture here was hand-converted, because no live producer output was available to copy at the time. It needs verifying against that session's real renderer output — the same producer-consumer round-trip both other fixtures already have.
  2. kanidm.credential is a name the worker chose. No producer emits it today. It may want ratification, or a different name.
  3. forgejo.repository grants still route through queen-only account resolution. The producer documents that kind as a direct-principal Forge token with no seat; minting one would fail until account resolution branches by kind too. No such grant exists yet, so nothing is broken — but the shape now permits one the code cannot serve.
The consumer half of the freeholder's ruling (2026-09-06): *"just update the structure, Vedanta is still pre v0.1."* The producer already emits it — [nixops4-providers#47](https://jo.et0.pw/lar.ad/nixops4-providers/pulls/47), merged. ## Why `DeployedGrant` carried two flat sets, `repositories` and `operations`. A grant of `A/read` plus `B/write` flattened to `{A,B} × {read,write}` — **which is write on A**, an authority the grant never gave. The reviewer found it by construction, and the first fix was to *refuse* grants the flat schema could not represent. The freeholder ruled the structure should change instead, and then improved on the queen's proposal: carry the grant's **own** vocabulary rather than invent a pairing shape. ```json "resources": [ { "kind": "forgejo.credential", "id": "swarm-alpha/forge-read", "operations": ["repository:read"] } ] ``` ## What lands - `ResourceGrant { kind, id, operations }` replaces the flat pair on `LifecycleCommand`, `DeployedProjection` and `DeployedGrant`. `ProjectionFile::parse` refuses an **unknown kind** and a **duplicate `(kind, id)`** at load, rather than at use. - **The validator compares pairwise.** Each requested operation is checked against *that* resource's authorized set, never against a union. Both crates carry a test proving a request for `write` on `A` is denied **even when the totals match** — the exact case a union comparison passes. - The plugin's `Projection` and its Resource and Operation stages are paired the same way, so OpenBao authorizes on the same footing Vedanta enforces. - **`native.rs` routes by kind.** `forgejo.credential` and `forgejo.repository` to the Forge issuer, `kanidm.credential` to the directory, and an unknown or mixed kind is **refused, never defaulted**. ## A bug this fixed on the way past Routing used to key off *emptiness*: no repositories meant "directory credential". `Invalidate` carries no resources by design — so **every invalidation silently went to the directory issuer regardless of which one had minted the token.** A Forge token could not be invalidated through that path at all. Kind-based routing takes an `Invalidate`'s kind from the predecessor token's own shape instead, and the class of bug disappears with the heuristic that caused it. Nobody was looking for that. It fell out of removing an implicit rule. ## Evidence fmt and clippy `-D warnings` clean. **219 lib tests**, 76 in the plugin, and the **live `bao_dev` acceptance test passing un-ignored** against a real `bao server -dev` — re-run by the queen after the push. No estate service was touched. ## Three things a reviewer must settle 1. **`flake-ops#426`'s fixture here was hand-converted**, because no live producer output was available to copy at the time. It needs verifying against that session's real renderer output — the same producer-consumer round-trip both other fixtures already have. 2. **`kanidm.credential` is a name the worker chose.** No producer emits it today. It may want ratification, or a different name. 3. **`forgejo.repository` grants still route through queen-only account resolution.** The producer documents that kind as a direct-principal Forge token with no seat; minting one would fail until account resolution branches by kind too. No such grant exists yet, so nothing is broken — but the shape now permits one the code cannot serve.
DeployedGrant/LifecycleCommand/DeployedProjection carried two flat sets
(repositories, operations) instead of the grant's own {kind, id,
operations} triple (ruling, Larandar 2026-09-06, lar.ad/nixops4-
providers#47 finding 1 take 2). A flat union across resources could not
tell "A: read, B: write" apart from "A: write, B: read" — both hashed to
the same two sets. ResourceGrant replaces the pair; ProjectionFile::parse
refuses an unknown kind and a duplicate (kind, id) at load;
Validator::resources_match compares command against projection resource
by resource, never as a union. The two vendored producer fixtures are
updated to the shape the producer (illustrative-swarm-alpha-drone-forge)
and the deployment renderer now emit.

Co-Authored-By: Teyla <[email protected]>
The adapter used to infer Forge-vs-Kanidm from whether the command named
any repositories at all — backwards for a vocabulary that now names its
kind explicitly, and silently wrong for every Invalidate (which restates
no resources by design and so always looked empty, always routing to
Kanidm regardless of which issuer actually minted the token). Routing now
reads resource_kind off the command's resources for Mint/Rotate — refusing
an empty or mixed-kind set rather than defaulting — and, for Invalidate,
off the predecessor token's own shape (Forgejo: integer id; Kanidm: an
opaque UUID), since that is the one piece of evidence available when no
resource is restated.

Co-Authored-By: Teyla <[email protected]>
Projection::{permitted_repositories, permitted_operations} and
IssueRequest::resource_repositories authorized the same way Vedanta's own
LifecycleCommand used to: two flat sets, unioned across every resource the
grant named. Projection now carries permitted_resources: Vec<ResourceGrant>
(Vedanta's own type) and IssueRequest names one resource_kind/resource_id
pair — a lease request always asks for exactly one resource — so authorize
looks up that one (kind, id) entry and holds the requested operations to
its own set, never the grant's total. build_command and the mirrored
DeployedGrant/Delegation wire type (crate::vedanta) carry the same shape
through to the signed command. The live bao_dev acceptance test still
passes un-ignored.

Co-Authored-By: Teyla <[email protected]>
larandar merged commit 1db600ac26 into fastlane 2026-09-06 10:24:17 +00:00
larandar deleted branch tango/54-resource-kinds 2026-09-06 10:24:17 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Ting/Vedanta!54
No description provided.