Repository navigation
feat(rstack): generate hosted ThunderID configuration from product identity facts - #2578
Conversation
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ef1d3e964e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 583f607771
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
583f607 to
52aa9db
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 52aa9dbcdf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
52aa9db to
127ac4a
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 127ac4a81b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
127ac4a to
c9d493e
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c9d493ef56
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
c9d493e to
b531dec
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b531dec0cd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
b531dec to
98cc5b3
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 98cc5b3f54
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…entity facts - `rstack identity generate thunderid` writes hosted ThunderID configuration from each product's own identity facts plus one operator deployment file. `rstack identity check` checks either document offline. - `bregctl`, `caseworkctl`, `evidencectl` and `messagingctl` each gain `generate identity-facts`, which writes a `registry.identity-facts/v1` document from that product's own configuration. - Two new platform formats, `registry.identity-facts/v1` and `registry.identity-deployment/v1`, with generated JSON Schemas in `products/platform/schemas`, examples, and editor mappings. - `caseworkctl generate identity-facts` marks the profiles Casework admits only for a person as requiring a human actor, and refuses a customised human-identity claim; `bregctl` refuses a renamed purpose claim; `evidencectl` refuses a renamed actor-kind, purpose, approver or grant claim, and two task authorities whose issuers derive one assertion issuer id. Hosted generation does not support either. - Each access profile in the identity facts states the actor kind its product pins (`actorKind`) and lists the claims its product refuses a token without (`requiredClaims`). Generation and `rstack identity check` refuse a machine client or user granted the profile without them, and a browser application that does not project them. On a profile reached by token exchange, `actorKind` applies to the client's own token. - An explicit actor kind on a client, user or selector must be one the runtimes parse (`human`, `agent` or `service`), whatever the profile states. - Access profiles may state the actor kinds they admit (`admittedActorKinds`) and the claims that refuse them (`refusedClaims`). Evidence states both, so generation refuses an agent on a standing profile and a principal carrying the actor claim on a profile that is not delegated. - BReg facts state Action and Proposal profiles and the claims verified-claim lookups read, and `bregctl` refuses a row boundary on a boolean or integer field. - Every set list in both identity formats refuses a repeated item. - Release manifests pin external images with an explicit `kind: image-pin`. Every release after 0.45.0 must carry the ThunderID pin. - The operator guide adds a page: Generate identity configuration. - Remove the retired Notary config schema (`schemas/registry-notary.config.schema.json`); no gate or product reads it. BREAKING CHANGE: identity generation derives every principal `sub` from the issuer, in a separate namespace per kind. Every `sub` changes once when a deployment moves to the generator. Update stored `sub` values, token mappings and scope references from `identity-receipt.json`. There is no compatibility shim. BREAKING CHANGE: `registry-thunderid-tooling` refuses a machine-client JWKS that carries private or symmetric key members, in development mode too. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
98cc5b3 to
edd3238
Compare
Summary
rstack identity generate thunderidwrites hosted ThunderID configuration from each product's own identity facts plus one operator deployment file.rstack identity checkchecks either document offline.bregctl,caseworkctl,evidencectlandmessagingctleach gaingenerate identity-facts, which writes aregistry.identity-facts/v1document from that product's own configuration.registry.identity-facts/v1andregistry.identity-deployment/v1, with generated JSON Schemas inproducts/platform/schemas, examples, and editor mappings.kind: image-pin. Every release after 0.45.0 must carry the ThunderID pin.Also in this branch, unrelated to identity: one commit removes the retired Notary config schema (
schemas/registry-notary.config.schema.json). No gate or product reads it. Only the historical v0.12.0 release notes mention it, and a docs test writes its own copy.BREAKING
subfrom the issuer, in a separate namespace per kind. Everysubchanges once when a deployment moves to the generator. Update storedsubvalues, token mappings and scope references fromidentity-receipt.json. There is no compatibility shim.registry-thunderid-toolingrefuses a machine-client JWKS that carries private or symmetric key members, in development mode too.Security review notes
This change generates hosted ThunderID configuration from each product's own identity facts and one operator deployment file (
rstack identity generate thunderid,rstack identity check), adds identity-facts writers tobregctl,caseworkctl,evidencectlandmessagingctl, and pins external release images. The branch had an independent security review and three fix rounds, and every finding was rechecked against each round.Trust model: the operator who writes the deployment file and the product configurations is trusted. The generator's job is to refuse inputs whose meaning differs from what the grants say, to keep secrets out of everything it writes, and to fail visibly with no partial output.
Authorization meaning
allowedClientsmeans unrestricted in every writer, matching the runtime verifier. An empty requester list is refused rather than read as "nobody". Evidence maps its admission gate by variant, so a future variant becomes a compile error instead of being read as unrestricted.selectedBy). Evidence states its actor kind and requester tags. Messaging states its scopes when it reads them from a renamed scope claim.userTypeslist it. One per-user rule decides this for both the grant check and the selection check, so a grant no such application can present is refused.registry_actor_kindfollows the principal. Users are alwayshuman, and a machine client is neverhuman:service.userssection.registry_purposefollows the grants for users as for machine clients: a stated purpose must be one a granted profile lists, one granted purpose is set automatically, and several need an explicit choice. Only products that read the claim count, so Evidence (which takes its purpose from the request) no longer makes a token ambiguous.registry_actor_kindwhenever a product reads it, listed or not, as machine-client tokens already carry their derived claims. BReg refuses a user token without it. They also carryregistry_purposewhen their product requires a purpose. An application that already lists the maximum of 16 token attributes without one of these is refused, andrstack identity checkreports the same refusal.requiredClaims;subis never listed). BReg states a principal claim other thansuband every row-boundary claim of the profile; Casework and Messaging a principal claim other thansub; Evidenceregistry_actor_kind, its audience claim, its requester-tags claim and a principal claim other thansub. Generation andrstack identity checkrefuse a machine client or user granted the profile without each claim (identity.deployment.required-claim-missing), and a browser application whose token attributes leave one out (identity.deployment.required-claim-not-projected). A derived actor kind or purpose counts as carried. Merged facts may disagree on whether a claim is required, but not on its type.registry_actor_kindon a machine client, a user or a factsselectedBylist must behuman,agentorservice, the kinds every product's runtime parses, whatever the granted profile states (identity.deployment.unknown-actor-kind).admittedActorKinds) and the claims that refuse them (refusedClaims). Evidence states[human, service]for a standing profile that names no kind, because an agent without a task grant reaches only profiles that name agents, and states its actor claim as refused on every profile that is not delegated. Generation refuses a principal of a kind a granted profile does not admit (identity.deployment.profile-refuses-actor-kind), stated or derived, and a principal carrying a refused claim (identity.deployment.profile-refuses-claim).bregctlrefuses a row boundary on a boolean or integer field, because generated tokens carry every claim as a string and BReg would refuse it at authentication.selectedByvalues, required, admitted and refused lists; grant profiles, redirect URIs, token attributes, user types and user-type attributes) refuses a repeated item, and the schemas declareuniqueItems.scopeClaimfor hosted generation, because hosted tokens carry scopes inscope. Casework also refuses a customized human-identity claim, BReg a renamed purpose claim, and Evidence a renamed actor-kind, purpose, approver or grant claim, because generated tokens carry only the default ones. Evidence also refuses two task authorities whose issuers differ but derive the same assertion issuer id, and merging refuses two products that state different issuers under one id.Subject identifiers (BREAKING, one-time re-key)
{seed}:{kind}:{id}, with a separate namespace per kind: machine clients, browser applications, users, extension user types, connections, organization units, and roles per kind. The seed comes from the issuer. An identifier of one kind can no longer equal one of another kind, and the generator asserts that all derived identifiers are distinct.subchanges once when a deployment moves to the generator. Update storedsubvalues, token mappings and scope references fromidentity-receipt.json. There is no compatibility shim.subis derived from the exact issuer string, so changing the issuer (even a trailing slash) re-keys every principal. The operator guide treats an issuer change as a migration.Secrets and placeholders
secret:env/NAME, unique per deployment. Deployment output carries{{.NAME}}placeholders and never a value. No secret file is opened, and the schema directory carries no placeholder.{{and}}are refused in every string and member name of the facts and deployment documents, read after YAML parsing so an escaped delimiter is found too. After rendering, the generator checks that each{{.NAME}}placeholder for a variable the deployment names appears exactly once, and that the output holds no other template delimiter.rstackruns each product tool with a cleared environment, a fixed allowlist of variables and no stdin. Facts are written to a private scratch directory that is removed on success and on failure.Public keys
kid,use,alg,key_ops). Private members (d,p,q,dp,dq,qi), symmetric keys (k), additional primes (oth), unknown types and unknown members are refused.keys; any other top-level member is refused and named.Process and filesystem
rstackholds the selected release generation's lease until generation finishes, so a concurrent cleanup cannot remove a tool between its version check and its run.rstack's. It guards against a mismatch, not against a substituted binary. A binary named throughRSTACK_<PRODUCT>CTL_BINis trusted by the operator who names it, and the guide says so.identity-receipt.json, moves in last, so an output without it is incomplete. The operator guide says not to use such an output. The order holds against an interrupted process; the generator does not sync the directory, so it is not guaranteed across a power loss.rstack identity checkreports an input-caused refusal with its reason instead of as an internal error.rstack identity generatechooses its exit class by error type, not message text: an input refusal or an existing output exits 1, and a filesystem or internal rendering failure exits 3.Dependency boundary
rstacklinks no signing, OIDC or token-session code. The tooling crate'ssessionfeature carries those dependencies, andrstackbuilds without it.rstack's normal dependency tree and fails on the platform crypto, OIDC and HTTP utility crates, FIPS or AWS-LC libraries, JWT and elliptic-curve signing crates, and async HTTP stacks. It runs on every Rust or manifest change. CI also buildsrstackalone, as the release does.Release provenance
kind: image-pinexplicitly. Declaringimageswithout that kind, any other kind, orimageson a documentation product is refused.Stated limits
{issuer}/.well-known/jwks.json. The task-authority issuer must be Casework's public base URL.httpissuer for development. Deployment generation refuses any issuer that is nothttps.authentication.oidc.claims. A deployment that renames those claims cannot use hosted generation yet.selectedBy, because the exchanged token takes its claims from the assertion. ItsactorKindis checked against the client's own token.actorKindand the derived claim, not throughrequiredClaims. Casework's task-status profile requires a service and its assertion profile an agent.other, which cannot be told apart from a member actually namedother. Both are diagnostics only; no action is planned.Not verified
{{.NAME}}substitution: whether it runs before or after YAML parsing, and over which fields.rstack identity generate thunderidrun driving all four product tools from an installed release.rstackand the product tools.Open decision
email,name,preferred_usernameand others), so a product cannot declare one as its own claim. A BReg deployment that reads its principal from one of those claims cannot generate facts today: the generator fails closed. Whether to allow that case is an open product decision.Checks run
On the final tree, all passing:
cargo fmt --check;cargo clippy --all-targets -D warningsforregistry-thunderid-tooling(default features, andschema,session),registry-rstack,registry-bregctl,registry-caseworkctl,registry-messagingctlandregistry-evidencectl;registry-rstackbuilt alone;registry-thunderid-tooling: 201 withschema,session(including the committed-schema check);registry-rstack: 115;registry-bregctl: 942;registry-caseworkctl: 263;registry-messagingctl: 137;registry-evidencectl: 990;rstackdependency-boundary gate.Not re-run on the final tree: the strict full conformance corpus (left to CI), the BReg and Messaging contract checks (passed on the previous round's tree), and the
registry-thunderid-toolingdefault-feature test run (its tests are a subset of theschema,sessionrun).On the first fix round's tree, all passing:
cargo clippy --all-targets -D warningsacross the ten changed crates;tests:
registry-evidenceregistry-evidencectlregistry-caseworkctlregistry-bregctlregistry-messagingctlregistry-cli-docsthe Evidence contract and source-neutrality checks, the Casework and Messaging schema checks, and the BReg source-neutrality check;
release tooling tests and the release source-model check;
CI change-selection tests;
the gate inventory;
the YAML reader boundary check;
cargo deny(advisories, bans, licenses, sources);the docs release-metadata test.
The strict configuration conformance corpus passes on the second fix round's tree: 6301 cells, 4671 pass, 3 expected failures, 0 fail. Later rounds change no configuration check; their new refusals are in facts and configuration generation only.
Later rounds change only the tooling crate,
rstack, the four product tools' facts writers, the identity-facts schema and examples, changelogs, the Casework README and the operator guide, with no manifest or lockfile change. Those crates were re-run on the final tree (above); the first round's results carry over for the other crates.Squashed onto current
main(conflicts only in changelogs; the generated identifiers catalogue was regenerated and its check passes). On that tree,cargo check --locked --all-targetspasses forregistry-rstack,registry-thunderid-tooling,registry-bregctl,registry-caseworkctl,registry-evidencectl,registry-messagingctlandregistry-breg, and theregistry-thunderid-toolingandregistry-rstacktests pass.Follow-up
kind: image-pin.