Skip to content

Commit bbf90d4

Browse files
committed
feat(rstack): generate hosted ThunderID configuration from product identity 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>
1 parent 588654c commit bbf90d4

122 files changed

Lines changed: 17261 additions & 3613 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/scripts/ci_changes.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@
196196
"registry-mcp",
197197
"registry-messagingctl",
198198
"registry-render",
199+
"registry-rstack",
199200
"registry-schedulingctl",
200201
}
201202
)

‎.github/scripts/test_ci_changes.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -899,8 +899,11 @@ def test_config_check_packages_build_the_programs_the_corpus_runs(self) -> None:
899899
for step in self.workflow_jobs["config-conformance"]["steps"]
900900
}
901901
build = steps["Build configuration check commands"]["run"].split()
902+
launcher = steps["Build the launcher check command"]["run"].split()
903+
self.assertEqual(launcher, "cargo build --locked -p registry-rstack".split())
902904
self.assertEqual(
903-
{build[index + 1] for index, word in enumerate(build) if word == "-p"},
905+
{build[index + 1] for index, word in enumerate(build) if word == "-p"}
906+
| {"registry-rstack"},
904907
CONFIG_CHECK_PACKAGES,
905908
)
906909
for package in sorted(CONFIG_CHECK_PACKAGES):
@@ -2244,6 +2247,7 @@ def test_yaml_reader_boundary_inputs_select_platform_hygiene(self) -> None:
22442247
"products/platform/scripts/test_check_yaml_reader_boundary.py",
22452248
"products/platform/scripts/check-mcp-dependencies.py",
22462249
"products/platform/scripts/test_check_mcp_dependencies.py",
2250+
"products/platform/scripts/check-rstack-dependencies.py",
22472251
):
22482252
with self.subTest(path=path):
22492253
self.assertTrue(classify(self.workspace, (path,))["platform_hygiene"])

‎.github/workflows/ci.yml‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,12 @@ jobs:
379379
- name: Enforce the YAML reader boundary
380380
run: products/platform/scripts/check-yaml-reader-boundary.sh
381381

382+
- name: Test the rstack dependency boundary check
383+
run: python3 -m unittest products/platform/scripts/test_check_rstack_dependencies.py
384+
385+
- name: Keep rstack a plain bootstrap launcher
386+
run: python3 products/platform/scripts/check-rstack-dependencies.py
387+
382388
platform-fuzz:
383389
name: Platform fuzz smoke (${{ matrix.group }})
384390
needs:
@@ -1991,6 +1997,12 @@ jobs:
19911997
-p registry-breg-mcp -p registry-breg-review -p registry-evidence-oid4vci -p registry-mcp
19921998
-p registry-evidence-oid4vp
19931999
2000+
# The launcher is built alone, as the release builds it, so feature
2001+
# unification with the product tools cannot hide a missing feature or
2002+
# turn on the session code.
2003+
- name: Build the launcher check command
2004+
run: cargo build --locked -p registry-rstack
2005+
19942006
- name: Run configuration conformance corpus
19952007
run: products/platform/scripts/run-config-conformance.sh --strict --bin-dir target/debug
19962008

‎Cargo.lock‎

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/registry-breg/src/auth.rs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,24 @@ impl AuthorityClaimConfig {
6666
}
6767
}
6868

69+
/// The claim the caller's principal is read from.
70+
#[must_use]
71+
pub fn principal_claim(&self) -> &str {
72+
&self.principal_claim
73+
}
74+
75+
/// The claim the caller's purpose is read from, when one is configured.
76+
#[must_use]
77+
pub fn purpose_claim(&self) -> Option<&str> {
78+
self.purpose_claim.as_deref()
79+
}
80+
81+
/// The claim the caller's actor kind is read from.
82+
#[must_use]
83+
pub fn actor_kind_claim(&self) -> &str {
84+
&self.contextual_claims.actor_kind
85+
}
86+
6987
#[must_use]
7088
pub fn with_contextual_claims(
7189
mut self,

‎crates/registry-bregctl/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ registry-platform-config.workspace = true
5858
registry-platform-yaml.workspace = true
5959
registry-platform-hooks.workspace = true
6060
registry-platform-httputil.workspace = true
61-
registry-thunderid-tooling.workspace = true
61+
registry-thunderid-tooling = { workspace = true, features = ["session"] }
6262
registry-platform-buildinfo.workspace = true
6363
registry-cli-api.workspace = true
6464
registry-cli-reference.workspace = true

‎crates/registry-bregctl/src/dev/config.rs‎

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2177,24 +2177,12 @@ pub(super) fn runtime(root: &Path, state: &State, clients: &Clients, test: bool)
21772177
}
21782178

21792179
fn local_authority_claims(compiled: &registry_breg::CompiledRegistry) -> Result<Value> {
2180-
let inventory = registry_breg::authority::authority_inventory(compiled)
2181-
.context("cannot inspect the compiled registry's authority claims")?;
2182-
let principal = match inventory
2183-
.principal_claims
2184-
.into_iter()
2185-
.collect::<Vec<_>>()
2186-
.as_slice()
2187-
{
2188-
[claim] => claim.clone(),
2189-
[] => "registry_principal".to_owned(),
2190-
claims => bail!(
2191-
"local development requires one common principal claim across compiled authenticated grants; the compiled registry declares multiple: {}",
2192-
claims.join(", ")
2193-
),
2194-
};
2180+
let claims = crate::identity_facts::authority_claims(compiled).map_err(|message| {
2181+
anyhow::anyhow!("local development requires one common principal claim across compiled authenticated grants; {message}")
2182+
})?;
21952183
Ok(json!({
2196-
"principal": principal,
2197-
"purpose": inventory.purpose_required.then_some("registry_purpose")
2184+
"principal": claims.principal,
2185+
"purpose": claims.purpose_required.then_some("registry_purpose")
21982186
}))
21992187
}
22002188

0 commit comments

Comments
 (0)