server/lib/tests/door_reserved_property_floor.rs
//! **The reserved-property ADMIN floor is on BOTH write doors, or it is on
//! neither.**
//!
//! A handful of property keys are not labels describing an artifact — they are
//! inputs the SERVER's own policy reads: the serve-time quarantine flag, the
//! property the promotion gate demands, and the retention pin. Writing one is an
//! admin action, and the gRPC `SetProperty` RPC enforces that.
//!
//! The HTTP `/-/properties` door writes the SAME store, through the same
//! `PropertyStore::set`. It enforced nothing but the plain Writer gate, so a
//! Writer refused at one door simply used the other — setting the `keep`
//! retention pin that `retention --protect-property` is built to police, forging
//! the `signed=true` the promote gate trusts, or clearing a quarantine flag. A
//! policy gate on one of two doors is not a gate.
//!
//! This is a DOOR-level test on purpose. A unit test over the shared
//! `is_reserved_property` rule passes whether or not the door calls it, which is
//! exactly the kind of test that reports a fix nobody wired.
//!
//! Both doors are the `supply-chain` plane (lean cut 2026-10-17): without it the
//! property store and its write door are not built, and the arm in
//! `.nornir/testmatrix-arms.json` runs this file.
use std::io::Write;
use traits::test_support::hold_port;
use holger_server_lib::{read_ron_config, wire_holger, Holger};
struct Running {
_data: tempfile::TempDir,
_cfg: tempfile::NamedTempFile,
base: String,
}
/// A `raw` repository behind a cleartext HTTP door with RBAC ON: a static bearer
/// per role, `writer` mapped to Writer and `boss` to Admin. `raw` because it is
/// the backend that can map a served path to a coordinate, which the quarantine
/// boot gate requires — and because it keeps this test about authorization only.
fn start() -> Running {
let mut last = String::new();
for _ in 0..8 {
let (grpc, http) = (hold_port(), hold_port());
match try_start(grpc, http) {
Ok(r) => return r,
Err(e) => last = e,
}
}
panic!("gateway never started; last error: {last}");
}
fn try_start(grpc: u16, http: u16) -> Result<Running, String> {
let data = tempfile::tempdir().unwrap();
let ron = format!(
r#"(
repositories: [
(ron_name: "raw", ron_repo_type: "raw", ron_upstreams: [], ron_in: None,
ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "default")),
ron_data_dir: Some("{data}")),
],
exposed_endpoints: [
(ron_name: "default", ron_url: "127.0.0.1:{grpc}", ron_http_url: Some("127.0.0.1:{http}")),
],
storage_endpoints: [ (ron_name: "storage", ron_storage_type: "znippy", ron_path: "{data}") ],
auth: Some((
methods: [
StaticToken(subject: "writer", token: Some("w-token")),
StaticToken(subject: "boss", token: Some("a-token")),
],
roles: {{ "writer": Writer, "boss": Admin }},
default_role: Some(Reader),
)),
audit: (dir: "{data}/audit", enabled: true),
)"#,
data = data.path().display(),
);
let mut cfg = tempfile::NamedTempFile::new().unwrap();
cfg.write_all(ron.as_bytes()).unwrap();
let mut holger: Holger = read_ron_config(cfg.path()).map_err(|e| format!("config: {e}"))?;
holger
.instantiate_backends()
.map_err(|e| format!("backends: {e}"))?;
wire_holger(&mut holger).map_err(|e| format!("wire: {e}"))?;
holger.start().map_err(|e| format!("start: {e}"))?;
Ok(Running {
_data: data,
_cfg: cfg,
base: format!("http://127.0.0.1:{http}"),
})
}
async fn wait_healthy(c: &reqwest::Client, base: &str) {
for _ in 0..100 {
if let Ok(r) = c.get(format!("{base}/healthz")).send().await {
if r.status() == 200 {
return;
}
}
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
}
panic!("gateway never became healthy at {base}");
}
fn props_url(base: &str, key: &str, value: Option<&str>) -> String {
let v = value.map(|v| format!("&value={v}")).unwrap_or_default();
format!("{base}/-/properties?repo=raw&name=serde&version=1.0.0&key={key}{v}")
}
/// A Writer may label an artifact and may NOT write the server's policy inputs —
/// on the HTTP door, exactly as on the gRPC one.
#[tokio::test(flavor = "multi_thread")]
async fn a_writer_cannot_set_a_reserved_property_over_http() {
let r = start();
let cl = tls_roots(reqwest::Client::builder())
.build()
.expect("reqwest client");
wait_healthy(&cl, &r.base).await;
// An ordinary label: a Writer's own business, and it must still work. If this
// fails the floor is too broad and the test below proves nothing.
let label = cl
.put(props_url(&r.base, "team", Some("core")))
.bearer_auth("w-token")
.send()
.await
.unwrap();
assert_eq!(
label.status(),
200,
"a Writer must still be able to label an artifact"
);
// `keep` is reserved by default (DEFAULT_RESERVED_PROPERTY_KEYS) — no special
// configuration needed, which is what made this reachable everywhere.
let pin = cl
.put(props_url(&r.base, "keep", Some("true")))
.bearer_auth("w-token")
.send()
.await
.unwrap();
assert_eq!(
pin.status(),
403,
"a Writer must NOT be able to set the retention pin over HTTP"
);
// …and REMOVAL rides the writer-gated PUT, not the admin-gated DELETE: a PUT
// with no `value=` clears the key. The floor has to cover that shape too, or
// it only stops the half of the attack that sets.
let clear = cl
.put(props_url(&r.base, "keep", None))
.bearer_auth("w-token")
.send()
.await
.unwrap();
assert_eq!(
clear.status(),
403,
"removing a reserved key is equally an admin action"
);
// An Admin can, which is the point: the key is gated, not forbidden.
let admin = cl
.put(props_url(&r.base, "keep", Some("true")))
.bearer_auth("a-token")
.send()
.await
.unwrap();
assert_eq!(admin.status(), 200, "an Admin sets the pin");
// The pin really landed, so the refusals above were about authorization and
// not about the write failing for some other reason. Read signed in: an estate build
// gates reads (HCON-5), and this test is about the write floor, not the read gate.
let read = cl
.get(format!(
"{}/-/properties?repo=raw&name=serde&version=1.0.0",
r.base
))
.bearer_auth("w-token")
.send()
.await
.unwrap();
let body = read.text().await.unwrap();
assert!(
body.contains("keep"),
"the admin's write is visible: {body}"
);
}
/// An unauthenticated caller gets 401 rather than 403 on a reserved key, so the
/// door does not tell an anonymous prober that a key exists and is merely gated.
#[tokio::test(flavor = "multi_thread")]
async fn an_anonymous_caller_is_unauthenticated_not_merely_forbidden() {
let r = start();
let cl = tls_roots(reqwest::Client::builder())
.build()
.expect("reqwest client");
wait_healthy(&cl, &r.base).await;
let resp = cl
.put(props_url(&r.base, "keep", Some("true")))
.send()
.await
.unwrap();
assert_eq!(resp.status(), 401);
}
/// reqwest 0.13 with reqwest 0.12 `rustls-tls` behaviour (2026-10-02): the ring
/// provider and the bundled Mozilla roots (webpki), not 0.13's aws-lc-rs + OS
/// trust store. Roots added later on the builder are merged in, as in 0.12.
#[allow(dead_code)]
fn tls_roots(b: reqwest::ClientBuilder) -> reqwest::ClientBuilder {
let _ = rustls::crypto::ring::default_provider().install_default();
b.tls_certs_only(
webpki_root_certs::TLS_SERVER_ROOT_CERTS
.iter()
.filter_map(|c| reqwest::Certificate::from_der(c).ok()),
)
}