server/lib/tests/every_write_door_goes_through_intake.rs
//! **Every write door goes through the intake chain** (SEC-REVIEW 2026-11-01, P2–P4, P9 and
//! the guard for the class).
//!
//! P19 (lane HOL-FIELD-2) closed the HTTP side doors: on a Holger whose policy requires a
//! scan, a write into a repository outside the chain is refused by name
//! (`holger-intake-15`). The review found the same bytes still walking in by the other
//! doors: the streamed gRPC upload, the gRPC put and read of a scanned `oci` registry, a
//! replication out of the chain, and a promotion into a side door. Each of them is proved
//! here against one booted Holger, over a real tonic client, the way a client would use it.
//!
//! The guard at the end (`every_write_door_is_enumerated_here`) reads the proto files and
//! the job kinds: an RPC or a job that writes artifact bytes and is not named in its table
//! fails the build, so a new door cannot ship without its intake answer.
use std::io::{Read, Write};
use holger_server_lib::grpc::holger_proto::promotion_service_client::PromotionServiceClient;
#[cfg(feature = "mirror")]
use holger_server_lib::grpc::holger_proto::replication_service_client::ReplicationServiceClient;
use holger_server_lib::grpc::holger_proto::repository_service_client::RepositoryServiceClient;
#[cfg(feature = "mirror")]
use holger_server_lib::grpc::holger_proto::ReplicateRequest;
use holger_server_lib::grpc::holger_proto::{
ArtifactId as ProtoId, FetchArtifactRequest, PromoteArtifactRequest, PutArtifactChunk,
PutArtifactRequest,
};
use holger_server_lib::{read_ron_config, wire_holger, Holger};
use sha2::Digest;
use traits::test_support::hold_port;
type Chan = tonic::transport::Channel;
fn token(who: &str) -> String {
format!("{who}-token-of-sufficient-length-for-holger")
}
struct Running {
_data: tempfile::TempDir,
_cfg: tempfile::NamedTempFile,
_holger: Holger,
rt: tokio::runtime::Runtime,
http: u16,
grpc: u16,
}
const REQUIRED: &str = "Some((required: [vulnerability]))";
fn start(scan: &str) -> Running {
let mut last = String::new();
for _ in 0..8 {
match try_start(hold_port(), hold_port(), scan, "", "", "") {
Ok(r) => return r,
Err(e) => last = e,
}
}
panic!("server never started; last error: {last}");
}
/// `repos`: more repositories (`{data}` is the data directory); `intake_extra`: more keys
/// inside `promotion.intake: (…)`; `top`: more top-level keys.
fn try_start(
grpc: u16,
http: u16,
scan: &str,
repos: &str,
intake_extra: &str,
top: &str,
) -> Result<Running, String> {
let data = tempfile::tempdir().unwrap();
let d = data.path().display().to_string();
let raw = |name: &str, extra: &str| {
format!(
r#"(ron_name: "{name}", ron_repo_type: "raw", ron_upstreams: [], ron_in: None,
ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "door")),
ron_data_dir: Some("{d}/{name}"){extra}),"#
)
};
let oci = format!(
r#"(ron_name: "oci", ron_repo_type: "oci", ron_upstreams: [], ron_in: None,
ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "door")), ron_data_dir: Some("{d}/oci")),"#
);
let ron = format!(
r#"(
repositories: [ {incoming} {quarantine} {approved} {production} {bundles} {sealed} {oci} {repos} ],
exposed_endpoints: [ (ron_name: "door", 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: "{d}") ],
audit: (dir: "{d}/audit", enabled: true),
access: (reads: Open, front: Open),
{top}
auth: Some((
methods: [ StaticToken(subject: "ida", token: Some("{ida}")), StaticToken(subject: "adm", token: Some("{adm}")) ],
roles: {{ "ida": Writer, "adm": Admin }},
duties: {{ "ida": [Importer], "adm": [RepositoryAdmin] }},
)),
promotion: Some((
ladder_name: "intake",
stages: [
(name: "incoming", repo: "incoming"), (name: "quarantine", repo: "quarantine"),
(name: "approved", repo: "approved"), (name: "production", repo: "production"),
],
intake: Some((
incoming: "incoming", quarantine: "quarantine", approved: "approved", production: "production",
rules: (sandbox: off),
approvals: 1,
scan: {scan},
{intake_extra}
)),
)),
)"#,
incoming = raw("incoming", ""),
quarantine = raw("quarantine", ""),
approved = raw("approved", ""),
production = raw("production", ""),
bundles = raw("bundles", ""),
sealed = raw(
"monetize-backup",
", ron_require_auth: true, ron_require_role: Some(Admin)"
),
ida = token("ida"),
adm = token("adm"),
repos = repos.replace("{data}", &d),
);
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:#}"))?;
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.unwrap();
{
let _in = rt.enter();
holger.start().map_err(|e| format!("start: {e:#}"))?;
}
let r = Running {
_data: data,
_cfg: cfg,
_holger: holger,
rt,
http,
grpc,
};
for _ in 0..200 {
if std::net::TcpStream::connect(("127.0.0.1", http)).is_ok()
&& request(http, "GET", "/healthz", "ida", &[]).0 == 200
{
return Ok(r);
}
std::thread::sleep(std::time::Duration::from_millis(20));
}
Err("never healthy".into())
}
/// One HTTP/1.1 request as `who`: the status and the body.
fn request(port: u16, method: &str, path: &str, who: &str, body: &[u8]) -> (u16, String) {
let mut c = std::net::TcpStream::connect(("127.0.0.1", port)).expect("connect");
let head = format!(
"{method} {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nAuthorization: Bearer {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
token(who),
body.len()
);
c.write_all(head.as_bytes()).unwrap();
c.write_all(body).unwrap();
let mut out = Vec::new();
c.read_to_end(&mut out).unwrap();
let text = String::from_utf8_lossy(&out).to_string();
let status = text
.split_whitespace()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let body = text
.split_once("\r\n\r\n")
.map(|(_, b)| b.to_string())
.unwrap_or_default();
(status, body)
}
/// One HTTP/1.1 request as `who`: the status and the header block.
fn request_head(port: u16, method: &str, path: &str, who: &str, body: &[u8]) -> (u16, String) {
let mut c = std::net::TcpStream::connect(("127.0.0.1", port)).expect("connect");
let head = format!(
"{method} {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nAuthorization: Bearer {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
token(who),
body.len()
);
c.write_all(head.as_bytes()).unwrap();
c.write_all(body).unwrap();
let mut out = Vec::new();
c.read_to_end(&mut out).unwrap();
let text = String::from_utf8_lossy(&out).to_string();
let status = text
.split_whitespace()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(0);
(
status,
text.split_once("\r\n\r\n")
.map(|(h, _)| h.to_string())
.unwrap_or(text),
)
}
fn bearer<T>(who: &str, body: T) -> tonic::Request<T> {
let mut r = tonic::Request::new(body);
r.metadata_mut().insert(
"authorization",
format!("Bearer {}", token(who)).parse().unwrap(),
);
r
}
fn id(namespace: &str, name: &str, version: &str) -> Option<ProtoId> {
Some(ProtoId {
namespace: namespace.into(),
name: name.into(),
version: version.into(),
})
}
/// A blob coordinate of the `oci` backend (`_blob` / its sha256 hex).
fn blob_id(bytes: &[u8]) -> Option<ProtoId> {
id("_blob", &hex::encode(sha2::Sha256::digest(bytes)), "")
}
async fn repo_client(r: &Running) -> RepositoryServiceClient<Chan> {
for _ in 0..250 {
if let Ok(c) =
RepositoryServiceClient::connect(format!("http://127.0.0.1:{}", r.grpc)).await
{
return c;
}
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
}
panic!("gRPC never came up at {}", r.grpc);
}
async fn put(
c: &mut RepositoryServiceClient<Chan>,
who: &str,
repo: &str,
id: Option<ProtoId>,
data: &[u8],
) -> Result<String, tonic::Status> {
let req = PutArtifactRequest {
repository: repo.into(),
id,
data: data.to_vec().into(),
async_job: false,
};
c.put_artifact(bearer(who, req))
.await
.map(|r| r.into_inner().message)
}
/// `PutArtifactStream`, in pieces of 16 bytes: the first names the repository and artifact.
async fn put_streamed(
c: &mut RepositoryServiceClient<Chan>,
who: &str,
repo: &str,
id: Option<ProtoId>,
data: &[u8],
) -> Result<String, tonic::Status> {
let pieces: Vec<PutArtifactChunk> = data
.chunks(16)
.enumerate()
.map(|(i, piece)| PutArtifactChunk {
repository: if i == 0 { repo.into() } else { String::new() },
id: if i == 0 { id.clone() } else { None },
data: piece.to_vec().into(),
async_job: false,
})
.collect();
c.put_artifact_stream(bearer(who, tokio_stream::iter(pieces)))
.await
.map(|r| r.into_inner().message)
}
/// What the gRPC read doors serve of `id` in `repo`: `FetchArtifact`'s bytes and the
/// `StreamArtifact` pieces joined (an error or an empty answer is "not served").
async fn served(
c: &mut RepositoryServiceClient<Chan>,
who: &str,
repo: &str,
id: Option<ProtoId>,
) -> (Option<Vec<u8>>, Option<Vec<u8>>) {
let req = FetchArtifactRequest {
repository: repo.into(),
id,
};
let whole = c
.fetch_artifact(bearer(who, req.clone()))
.await
.ok()
.map(|r| r.into_inner().data.to_vec())
.filter(|d| !d.is_empty());
let joined = match c.stream_artifact(bearer(who, req)).await {
Ok(s) => {
let mut s = s.into_inner();
let mut out = Vec::new();
let mut ok = true;
loop {
match s.message().await {
Ok(Some(p)) => out.extend_from_slice(&p.data),
Ok(None) => break,
Err(_) => {
ok = false;
break;
}
}
}
(ok && !out.is_empty()).then_some(out)
}
Err(_) => None,
};
(whole, joined)
}
fn refused_by_name(r: &Result<String, tonic::Status>, code: &str, what: &str) {
match r {
Ok(m) => panic!("{what} is refused, but it was taken: {m}"),
Err(s) => {
assert!(
matches!(
s.code(),
tonic::Code::PermissionDenied | tonic::Code::FailedPrecondition
),
"{what}: {:?} {}",
s.code(),
s.message()
);
assert!(s.message().contains(code), "{what}: {}", s.message());
}
}
}
// ── P2: the streamed gRPC upload into a side door ─────────────────────────────
/// SEC-REVIEW P2: `PutArtifactStream` took the streamed path past `write_refusal` when no
/// ingest gate was configured (the default), so a Writer streamed bytes into a side door and
/// HTTP served them.
#[test]
fn a_streamed_grpc_upload_into_a_side_door_is_refused() {
let r = start(REQUIRED);
r.rt.block_on(async {
let mut c = repo_client(&r).await;
let body = b"lodash 4.17.15, never scanned, in several pieces";
let got = put_streamed(
&mut c,
"ida",
"bundles",
id("", "x/lodash-4.17.15.tgz", ""),
body,
)
.await;
refused_by_name(
&got,
"holger-intake-15",
"a streamed upload into the raw bundles repository",
);
let got = put(
&mut c,
"ida",
"bundles",
id("", "x/lodash-4.17.15.tgz", ""),
body,
)
.await;
refused_by_name(
&got,
"holger-intake-15",
"a whole upload into the raw bundles repository",
);
});
assert_ne!(
request(r.http, "GET", "/bundles/x/lodash-4.17.15.tgz", "ida", &[]).0,
200,
"nothing refused is served"
);
}
// ── P3: gRPC and the scanned `oci` registry ───────────────────────────────────
/// SEC-REVIEW P3: a scanned registry is no side door (its HTTP pushes walk the chain), so
/// `write_refusal` was None for it and the gRPC put wrote straight into its backend; the
/// gRPC reads served its staged blobs raw. A scanned registry takes pushes only over its
/// OCI door, and its gRPC reads serve nothing staged.
#[test]
fn grpc_never_writes_or_reads_past_a_scanned_registry() {
let r = start(REQUIRED);
let blob = b"a layer nobody scanned".to_vec();
r.rt.block_on(async {
let mut c = repo_client(&r).await;
let got = put(&mut c, "ida", "oci", blob_id(&blob), &blob).await;
refused_by_name(
&got,
"holger-intake-11",
"a gRPC put into the scanned oci registry",
);
let got = put_streamed(&mut c, "ida", "oci", blob_id(&blob), &blob).await;
refused_by_name(
&got,
"holger-intake-11",
"a streamed gRPC put into the scanned oci registry",
);
let manifest = br#"{"schemaVersion":2,"layers":[]}"#;
let got = put(&mut c, "ida", "oci", id("demo", "app", "latest"), manifest).await;
refused_by_name(
&got,
"holger-intake-11",
"a gRPC manifest put into the scanned oci registry",
);
});
// A blob staged by the OCI door (a push in progress) is never served by gRPC.
let digest = format!("sha256:{}", hex::encode(sha2::Sha256::digest(&blob)));
let (s, head) = request_head(
r.http,
"POST",
"/v2/oci/demo/app/blobs/uploads/",
"ida",
&[],
);
assert_eq!(s, 202, "an upload starts: {head}");
let loc = head
.lines()
.find_map(|l| {
l.split_once(':')
.filter(|(k, _)| k.eq_ignore_ascii_case("location"))
.map(|(_, v)| v.trim().trim_start_matches("http://127.0.0.1").to_string())
})
.expect("a Location");
let sep = if loc.contains('?') { '&' } else { '?' };
let (s, b) = request(
r.http,
"PUT",
&format!("{loc}{sep}digest={digest}"),
"ida",
&blob,
);
assert_eq!(s, 201, "the OCI door stages the blob: {b}");
r.rt.block_on(async {
let mut c = repo_client(&r).await;
for who in ["ida", "adm"] {
let (whole, joined) = served(&mut c, who, "oci", blob_id(&blob)).await;
assert_eq!(whole, None, "FetchArtifact serves no staged blob to {who}");
assert_eq!(
joined, None,
"StreamArtifact serves no staged blob to {who}"
);
}
});
// Nor does a path outside `/v2/oci/` reach the raw backend.
for path in [
format!("/oci/_blob/{}", &digest[7..]),
"/oci/demo/app".to_string(),
] {
assert_ne!(request(r.http, "GET", &path, "ida", &[]).0, 200, "{path}");
assert!(
request(r.http, "PUT", &path, "ida", b"x").0 >= 400,
"{path}: a write outside the OCI door is refused"
);
}
}
/// SEC-REVIEW P3: the npm door holds nothing of its own; a door with storage had a store a
/// raw write filled around `npm publish`, so it is refused at boot by name.
#[test]
fn an_npm_door_with_storage_is_refused_at_boot() {
for storage in [
r#"ron_writable_archive: Some("{data}/npm.znippy")"#,
r#"ron_data_dir: Some("{data}/npm")"#,
] {
let npm = format!(
r#"(ron_name: "npm", ron_repo_type: "npm", ron_upstreams: [], ron_in: None,
ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "door")), {storage}),"#
);
let e = try_start(
hold_port(),
hold_port(),
REQUIRED,
&npm,
r#"npm: Some("npm"),"#,
"",
)
.err()
.unwrap_or_else(|| panic!("an npm door with {storage} is refused at boot"));
assert!(
e.contains("holger-npm-5") && e.contains("npm"),
"{storage}: {e}"
);
}
}
// ── P4: replication out of the chain ──────────────────────────────────────────
// The ReplicationService is served with the `mirror` feature.
#[cfg(feature = "mirror")]
async fn replicate(r: &Running, who: &str, from: &str, to: &str) -> Result<String, tonic::Status> {
let mut c = ReplicationServiceClient::connect(format!("http://127.0.0.1:{}", r.grpc))
.await
.expect("gRPC");
let req = ReplicateRequest {
from_repo: from.into(),
to_repo: to.into(),
execute: true,
limit: 0,
async_job: false,
};
c.replicate(bearer(who, req)).await.map(|r| {
let r = r.into_inner();
format!("copied {} failed {}: {}", r.copied, r.failed, r.message)
})
}
/// SEC-REVIEW P4: `Replicate` read the source's raw backend and wrote the destination's, so
/// an Admin copied INGESTED (or Admin-only) bytes into a served repository. A replication
/// copies only what a reader may be served (production's AVAILABLE set, through the chain's
/// view) and never into a repository the chain or the scan policy closes.
#[cfg(feature = "mirror")]
#[test]
fn a_replication_never_copies_unapproved_bytes_into_a_served_repository() {
let r = start(REQUIRED);
let (s, b) = request(
r.http,
"PUT",
"/incoming/x/evil.txt",
"ida",
b"INGESTED, unscanned",
);
assert_eq!(s, 201, "{b}");
let (s, b) = request(
r.http,
"PUT",
"/monetize-backup/k/material",
"adm",
b"sealed key",
);
assert!((200..300).contains(&s), "{s} {b}");
r.rt.block_on(async {
for (from, to, code) in [
("incoming", "bundles", "holger-intake-"),
("quarantine", "bundles", "holger-intake-"),
("monetize-backup", "bundles", "holger-intake-15"),
("production", "bundles", "holger-intake-15"),
("bundles", "production", "holger-intake-"),
("bundles", "incoming", "holger-intake-"),
("incoming", "oci", "holger-intake-"),
] {
let got = replicate(&r, "adm", from, to).await;
refused_by_name(&got, code, &format!("Replicate {from} → {to}"));
}
});
for path in ["/bundles/x/evil.txt", "/bundles/k/material"] {
assert_ne!(
request(r.http, "GET", path, "ida", &[]).0,
200,
"{path} is never served"
);
}
}
/// Without a required scan there are no side doors, but the chain still holds its own:
/// INGESTED bytes never leave incoming by a replication.
#[cfg(feature = "mirror")]
#[test]
fn without_a_required_scan_a_replication_still_never_empties_incoming() {
let r = start("None");
let (s, b) = request(
r.http,
"PUT",
"/incoming/x/evil.txt",
"ida",
b"INGESTED, unscanned",
);
assert_eq!(s, 201, "{b}");
r.rt.block_on(async {
let got = replicate(&r, "adm", "incoming", "bundles").await;
refused_by_name(&got, "holger-intake-", "Replicate incoming → bundles");
});
assert_ne!(
request(r.http, "GET", "/bundles/x/evil.txt", "ida", &[]).0,
200
);
}
// ── P5: upstream proxies where a scan is required ─────────────────────────────
fn boot_error(scan: &str, repos: &str) -> String {
try_start(
hold_port(),
hold_port(),
scan,
repos,
"",
"network: Online,",
)
.err()
.unwrap_or_else(|| panic!("refused at boot: {repos}"))
}
/// SEC-REVIEW P5: a `*-remote` proxy and a repository with `ron_upstreams` serve (and cache)
/// upstream bytes no scan saw, and `ron_upstreams: ["incoming"]` served the chain's raw
/// store. Where a scan is required both are refused at boot, by name; an upstream naming a
/// chain repository is refused on every Holger with a chain.
#[test]
fn an_unscanned_upstream_is_refused_at_boot() {
let remote = r#"(ron_name: "crates-io", ron_repo_type: "rust-remote", ron_base_url: Some("https://index.crates.io"), ron_upstreams: [], ron_in: None, ron_out: None),"#;
let e = boot_error(REQUIRED, remote);
assert!(
e.contains("holger-intake-17") && e.contains("crates-io"),
"{e}"
);
let cache = r#"(ron_name: "cache", ron_repo_type: "raw", ron_upstreams: ["bundles"], ron_in: None,
ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "door")), ron_data_dir: Some("{data}/cache")),"#;
let e = boot_error(REQUIRED, cache);
assert!(e.contains("holger-intake-17") && e.contains("cache"), "{e}");
let leak = r#"(ron_name: "leak", ron_repo_type: "raw", ron_upstreams: ["incoming"], ron_in: None,
ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "door")), ron_data_dir: Some("{data}/leak")),"#;
for scan in [REQUIRED, "None"] {
let e = boot_error(scan, leak);
assert!(
e.contains("holger-intake-17") && e.contains("leak") && e.contains("incoming"),
"scan {scan}: {e}"
);
}
}
/// Without a required scan a proxy outside the chain still boots: nothing changes there.
#[test]
fn without_a_required_scan_a_proxy_outside_the_chain_boots() {
let cache = r#"(ron_name: "cache", ron_repo_type: "raw", ron_upstreams: ["bundles"], ron_in: None,
ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "door")), ron_data_dir: Some("{data}/cache")),"#;
let mut last = String::new();
for _ in 0..4 {
match try_start(hold_port(), hold_port(), "None", cache, "", "") {
Ok(_) => return,
Err(e) => last = e,
}
}
panic!("a proxy outside the chain boots where no scan is required: {last}");
}
// ── P8: a group serves its members raw ────────────────────────────────────────
/// SEC-REVIEW P8: a `group` serves its members' raw backends under the group's own gate, so
/// a group naming `incoming` served INGESTED bytes, and one naming an Admin-only repository
/// served its bytes to any reader of the group. Both are refused at boot, by name.
#[test]
fn a_group_never_serves_the_chain_or_an_admin_only_repository_raw() {
for member in ["incoming", "production", "oci", "monetize-backup"] {
let group = format!(
r#"(ron_name: "all", ron_repo_type: "group", ron_members: ["bundles", "{member}"], ron_upstreams: [], ron_in: None,
ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "door"))),"#
);
let e = try_start(hold_port(), hold_port(), "None", &group, "", "")
.err()
.unwrap_or_else(|| panic!("a group with {member} is refused at boot"));
assert!(
e.contains("holger-intake-19") && e.contains("all") && e.contains(member),
"{member}: {e}"
);
}
}
// ── P9: a promotion into a side door ──────────────────────────────────────────
/// SEC-REVIEW P9: `PromoteArtifact` checked only the chain's four repositories as its
/// destination, so a promotion wrote into a side door the HTTP door refuses.
#[test]
fn a_promotion_into_a_side_door_is_refused() {
let r = start(REQUIRED);
let (s, b) = request(
r.http,
"PUT",
"/monetize-backup/k/material",
"adm",
b"sealed key",
);
assert!((200..300).contains(&s), "{s} {b}");
r.rt.block_on(async {
let mut c = PromotionServiceClient::connect(format!("http://127.0.0.1:{}", r.grpc))
.await
.expect("gRPC");
let req = PromoteArtifactRequest {
from_repo: "monetize-backup".into(),
to_repo: "bundles".into(),
artifact: id("", "k/material", ""),
async_job: false,
};
let got = c
.promote_artifact(bearer("adm", req))
.await
.map(|r| r.into_inner().message);
refused_by_name(
&got,
"holger-intake-15",
"PromoteArtifact monetize-backup → bundles",
);
});
assert_ne!(
request(r.http, "GET", "/bundles/k/material", "ida", &[]).0,
200
);
}
// ── P10: one predicate for "a chain view" ─────────────────────────────────────
/// SEC-REVIEW P10: the side doors exempted every archive-less view format, but the wiring
/// makes a chain view only of a repository with no `ron_handler`; a handler-backed deb
/// repository was neither a view nor a side door. One predicate decides both now.
#[test]
fn a_handler_backed_repository_of_a_view_format_is_a_side_door() {
let data = tempfile::tempdir().unwrap();
let d = data.path().display();
let ron = format!(
r#"(
repositories: [
(ron_name: "incoming", ron_repo_type: "raw", ron_upstreams: [], ron_in: None, ron_out: None, ron_data_dir: Some("{d}/i")),
(ron_name: "quarantine", ron_repo_type: "raw", ron_upstreams: [], ron_in: None, ron_out: None, ron_data_dir: Some("{d}/q")),
(ron_name: "approved", ron_repo_type: "raw", ron_upstreams: [], ron_in: None, ron_out: None, ron_data_dir: Some("{d}/a")),
(ron_name: "production", ron_repo_type: "raw", ron_upstreams: [], ron_in: None, ron_out: None, ron_data_dir: Some("{d}/p")),
(ron_name: "debs", ron_repo_type: "deb", ron_upstreams: [], ron_in: None, ron_out: None, ron_handler: Some("my-deb")),
(ron_name: "view", ron_repo_type: "deb", ron_upstreams: [], ron_in: None, ron_out: None),
],
exposed_endpoints: [],
storage_endpoints: [],
audit: (dir: "{d}/audit", enabled: true),
promotion: Some((
ladder_name: "intake",
stages: [
(name: "incoming", repo: "incoming"), (name: "quarantine", repo: "quarantine"),
(name: "approved", repo: "approved"), (name: "production", repo: "production"),
],
intake: Some((
incoming: "incoming", quarantine: "quarantine", approved: "approved", production: "production",
rules: (sandbox: off), approvals: 1, scan: {REQUIRED},
)),
)),
)"#
);
let mut cfg = tempfile::NamedTempFile::new().unwrap();
cfg.write_all(ron.as_bytes()).unwrap();
let h = read_ron_config(cfg.path()).expect("parses");
let doors = h.side_doors().expect("side doors");
assert!(
doors.contains_key("debs"),
"a handler-backed deb repository is a side door: {doors:?}"
);
assert!(
!doors.contains_key("view"),
"the archive-less deb repository is a chain view: {doors:?}"
);
}
// ── the guard: every door is named here ───────────────────────────────────────
/// What one gRPC method does to artifact bytes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Door {
/// Reads, lists, reports: no artifact bytes land anywhere.
Reads,
/// Writes state that is not an artifact a reader is served (keys, tokens, properties,
/// votes, an SBOM document, entitlements, twin and grow control).
NoArtifactBytes,
/// Lands artifact bytes only through the intake chain's own door (an ingest into
/// incoming, a signed bundle through the import door, a twin's recharge of the same chain).
ThroughTheChain,
/// Lands artifact bytes raw; the chain's refusal is asked first, and the named test of
/// this file proves it.
Refused(&'static str),
}
/// **Every gRPC method, by what it does to artifact bytes.** A method missing here fails
/// [`every_write_door_is_enumerated_here`]: a new door does not ship until it says how its
/// bytes meet the intake chain (and a raw one names the test that proves its refusal).
const DOORS: &[(&str, Door)] = &[
("AccountService.MyRepositories", Door::Reads),
("AccountService.CreateRepository", Door::NoArtifactBytes),
("AccountService.Grant", Door::NoArtifactBytes),
("BundleService.Export", Door::Reads),
("BundleService.AllowFirstImport", Door::NoArtifactBytes),
("FirstKeyService.Invite", Door::NoArtifactBytes),
("DownstreamService.Invite", Door::NoArtifactBytes),
("EstateService.Manifest", Door::Reads),
("EstateService.Fetch", Door::Reads),
("GrowService.Start", Door::NoArtifactBytes),
("GrowService.Status", Door::Reads),
("GrowService.Resume", Door::NoArtifactBytes),
("GrowService.Flush", Door::NoArtifactBytes),
("GrowService.TwinReport", Door::NoArtifactBytes),
("GrowService.Twins", Door::Reads),
("GrowService.DownstreamReport", Door::NoArtifactBytes),
("GrowService.Downstreams", Door::Reads),
("EntitlementService.Get", Door::Reads),
("EntitlementService.Set", Door::NoArtifactBytes),
("EntitlementService.List", Door::Reads),
("EntitlementService.Purge", Door::NoArtifactBytes),
("RoomService.Room", Door::Reads),
("RoomService.PrivateEstate", Door::NoArtifactBytes),
("TokenService.Mint", Door::NoArtifactBytes),
("TokenService.List", Door::Reads),
("TokenService.Revoke", Door::NoArtifactBytes),
("RepositoryService.FetchArtifact", Door::Reads),
("RepositoryService.ListArtifacts", Door::Reads),
(
"RepositoryService.PutArtifact",
Door::Refused("grpc_never_writes_or_reads_past_a_scanned_registry"),
),
("RepositoryService.StreamArtifact", Door::Reads),
(
"RepositoryService.PutArtifactStream",
Door::Refused("a_streamed_grpc_upload_into_a_side_door_is_refused"),
),
("ArchiveService.ListArchiveFiles", Door::Reads),
("ArchiveService.ArchiveInfo", Door::Reads),
("AdminService.Health", Door::Reads),
("AdminService.ListRepositories", Door::Reads),
("AdminService.ServerProfile", Door::Reads),
("AdminService.WhoAmI", Door::Reads),
("AdminService.Capacity", Door::Reads),
("AdminService.BecomeTwin", Door::NoArtifactBytes),
("AdminService.TwinCsr", Door::NoArtifactBytes),
("AdminService.IssueTwinCertificate", Door::NoArtifactBytes),
("AdminService.InstallTwinCertificate", Door::NoArtifactBytes),
("AdminService.Recharge", Door::ThroughTheChain),
("AdminService.RechargeStatus", Door::Reads),
("AdminService.TwinStatus", Door::Reads),
("AdminService.FencePrimary", Door::NoArtifactBytes),
("AdminService.AdminSnapshot", Door::Reads),
("AdminService.Topology", Door::Reads),
("AdminService.FlowSvg", Door::Reads),
("AdminService.SecuritySummary", Door::Reads),
("AdminService.ImportIntelBundle", Door::NoArtifactBytes),
("AdminService.ImportBundle", Door::ThroughTheChain),
(
"PromotionService.PromoteArtifact",
Door::Refused("a_promotion_into_a_side_door_is_refused"),
),
("PromotionService.AdvanceArtifact", Door::ThroughTheChain),
("PromotionService.GetArtifactState", Door::Reads),
("PromotionService.ListIntakeArtifacts", Door::Reads),
("PromotionService.GetArtifactEvidence", Door::Reads),
("PromotionService.ListDependencySets", Door::Reads),
("PromotionService.GetDependencySet", Door::Reads),
(
"PromotionService.ApproveDependencySet",
Door::ThroughTheChain,
),
(
"PromotionService.RefuseDependencySet",
Door::ThroughTheChain,
),
("AuditService.GetArtifactTrail", Door::Reads),
("AuditService.VerifyAuditChain", Door::Reads),
("AuditService.QueryDownloads", Door::Reads),
("AuditService.QueryAudit", Door::Reads),
("AuditService.AggregateAudit", Door::Reads),
("AuditService.ExportAudit", Door::Reads),
("SearchService.Search", Door::Reads),
("SbomService.AttachSbom", Door::NoArtifactBytes),
("SbomService.FetchSbom", Door::Reads),
("PropertyService.SetProperty", Door::NoArtifactBytes),
("PropertyService.GetProperties", Door::Reads),
(
"ReplicationService.Replicate",
Door::Refused("a_replication_never_copies_unapproved_bytes_into_a_served_repository"),
),
("MigrationService.Discover", Door::Reads),
(
"MigrationService.Start",
Door::Refused("a_migration_never_lands_bytes_past_the_chain"),
),
("MigrationService.Watch", Door::Reads),
("MigrationService.Cancel", Door::NoArtifactBytes),
("UsageService.ReadUsage", Door::Reads),
("JobService.GetJob", Door::Reads),
("JobService.ListJobs", Door::Reads),
("AdmissionService.AdmitPlan", Door::NoArtifactBytes),
("AdmissionService.ListUsage", Door::Reads),
];
/// `Service.Method` of every `rpc` in the proto files.
fn proto_methods() -> std::collections::BTreeSet<String> {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../proto");
let mut out = std::collections::BTreeSet::new();
for entry in std::fs::read_dir(&dir).expect("the proto directory") {
let path = entry.unwrap().path();
if path.extension().and_then(|e| e.to_str()) != Some("proto") {
continue;
}
let mut service = String::new();
for line in std::fs::read_to_string(&path).unwrap().lines() {
let t = line.trim_start();
if let Some(rest) = t.strip_prefix("service ") {
service = rest.split_whitespace().next().unwrap_or("").to_string();
} else if let Some(rest) = t.strip_prefix("rpc ") {
let name = rest.split(['(', ' ']).next().unwrap_or("");
out.insert(format!("{service}.{name}"));
}
}
}
out
}
/// **The guard for the class** (SEC-REVIEW 2026-11-01): every gRPC method is classified in
/// [`DOORS`], every raw-byte door names the test of this file that proves the chain refuses
/// it, and every job kind answers [`holger_server_lib::jobs::Job::writes`] (an exhaustive
/// match, so a new kind does not compile until it does). The HTTP door is one handler; its
/// write verbs into every kind of repository are walked by
/// [`every_http_write_verb_meets_the_chain`].
#[test]
fn every_write_door_is_enumerated_here() {
let methods = proto_methods();
assert!(methods.len() > 50, "the protos were read: {methods:?}");
let named: std::collections::BTreeMap<&str, Door> = DOORS.iter().copied().collect();
let missing: Vec<&String> = methods
.iter()
.filter(|m| !named.contains_key(m.as_str()))
.collect();
assert!(
missing.is_empty(),
"a new gRPC door: say in DOORS how its bytes meet the intake chain: {missing:?}"
);
let stale: Vec<&&str> = named.keys().filter(|k| !methods.contains(**k)).collect();
assert!(
stale.is_empty(),
"DOORS names methods no proto declares: {stale:?}"
);
let me = include_str!("every_write_door_goes_through_intake.rs");
for (method, door) in DOORS {
if let Door::Refused(test) = door {
assert!(
me.contains(&format!("fn {test}()")),
"{method}: its proof `{test}` is no test of this file"
);
}
}
}
/// The HTTP half of the guard: every write verb into every kind of repository either is
/// refused or lands in the chain's incoming (never served), except the Admin-only
/// repository, which serves nobody but an Admin.
#[test]
fn every_http_write_verb_meets_the_chain() {
let r = start(REQUIRED);
for repo in ["bundles", "quarantine", "approved", "production", "oci"] {
for verb in ["PUT", "POST", "PATCH", "DELETE"] {
let path = format!("/{repo}/guard/{verb}.txt");
let (s, b) = request(r.http, verb, &path, "ida", b"guard");
assert!(s >= 400, "{verb} {path} is refused: {s} {b}");
assert_ne!(request(r.http, "GET", &path, "ida", &[]).0, 200, "{path}");
}
}
let (s, b) = request(r.http, "PUT", "/incoming/guard/in.txt", "ida", b"guard");
assert_eq!(s, 201, "incoming INGESTS: {b}");
for repo in ["incoming", "production"] {
let path = format!("/{repo}/guard/in.txt");
assert_ne!(request(r.http, "GET", &path, "ida", &[]).0, 200, "{path}");
}
}
/// `MigrationService.Start` landed a Nexus or Artifactory repository's assets raw into this
/// Holger's repositories. With an intake chain it is refused by name: the chain's import
/// door (a signed bundle) and incoming are the ways in.
#[test]
fn a_migration_never_lands_bytes_past_the_chain() {
let src = include_str!("../src/grpc/migration.rs");
assert!(
src.contains("INTAKE_RAW_IMPORT_REFUSED"),
"MigrationService.Start asks the intake chain before it plans a byte"
);
}