nordisk/gunnar-ui
crates/gunnar-ui-server/src/anonymous.rs
//! **The unprivileged reader, driven end to end against a gunnar that enforces
//! visibility.**
//!
//! This module is tests and nothing else. It exists as its own file because
//! what it asserts is not a property of any one handler — it is a property of
//! the *console*, and specifically of the seam between two credentials:
//!
//! ```text
//! browser (no cookie) ──▶ require_auth ──▶ api::Reader ──┬─▶ App::gunnar (ADMIN key)
//! browser (viewer) ──▶ │
//! browser (operator) ──▶ └─▶ App::guest (no grants)
//! ```
//!
//! Two of those three rows arrive at the guest, and the third at the console's
//! own key. The file was written for the first row alone, and its own warning
//! below turned out to describe the hole in the second: see
//! `a_viewers_session_reads_with_the_guest_key_and_never_the_consoles`.
//!
//! # The failure this is here to catch
//!
//! The console signs every control-plane call with **one** key, and
//! `gunnar-ui-server enroll` gives that key a server-wide `Admin` grant,
//! because the console administers the server. The moment any route is served
//! without a session, that key becomes a confused deputy: the visitor is
//! anonymous, the credential is not, and gunnar answers the credential.
//! `Repos.List` on it returns every private repository on the box.
//!
//! A test that only asserted "the public repository shows up" would be **green
//! on that hole**, because a public repository shows up either way. So every
//! assertion below is about a *private* repository, and about the absence of
//! disclosure rather than the presence of content.
//!
//! # Why the fake gunnar authorises by fingerprint
//!
//! [`FakeGunnar`] reads the `gunnar-authorization` header off each request,
//! pulls the fingerprint out of it, and answers as the real gunnar would for
//! that principal: the console's key sees everything, the guest key sees only
//! `Visibility::Public`. That is what makes these tests able to tell *which key
//! the console used* — which is the actual question, and one that no amount of
//! asserting on status codes against a permissive fake could answer.
//!
//! It is a fake and not the real thing (this crate deliberately does not depend
//! on any gunnar crate; see `tests/contract.rs`), so it proves the console's
//! half and not gunnar's. gunnar's half — that a grantless principal is narrowed
//! to the public subset rather than refused, that the account gate outranks
//! visibility — is proved in `gunnar-cli/tests/public_listing.rs` against the
//! real `AuthTable`.
/// **Gunnar's MCP connector through the console** (TASKS-13, `mcp`): the
/// default build serves none of it; the feature build serves its metadata and
/// answers `/mcp` with the login pointer, past the session gate.
mod mcp;
/// **The rendered file view's own end-to-end tests.** A child module so it can
/// use this one's fake gunnar and console harness without a second copy of
/// either, and its own file so that what it asserts — who gets a typeset page,
/// who gets a PDF, whose choice is remembered — can be read without scrolling
/// through the credential seam above it.
mod rendered_files;
/// **Paid signup, driven the way a browser with no session drives it.** A
/// second child module, for the same reason [`rendered_files`] is one: it needs
/// this module's console harness, and what it asserts — which of these routes a
/// stranger can reach at all, what the pages say, and that a name is settled
/// before any money moves — reads better away from the credential seam above.
///
/// `metered` only, because the routes it drives are: an open build does not
/// contain `auth::signup` at all, which is what
/// `tests/open_build_speaks_no_money.rs` exists to hold it to.
#[cfg(feature = "metered")]
mod paid_signup;
/// **The private-gunnar buy page, driven by a visitor with no session**: the
/// white page, and its three calls straight through to monetize's
/// `EstateOrder` — sizes out, monetize's words and picture back.
#[cfg(feature = "metered")]
mod private_order;
/// **The ONE sign-up page's contract** (owner 2026-10-07): the left half is
/// the front page's, the heading follows the situation, one − / +, the
/// picture, and nothing priced or sized by the page itself.
#[cfg(feature = "metered")]
mod signup_page;
/// **The login page's company field is gunnar's to offer** (round 5.3): the
/// status says `sso_offered` only where an account has an identity provider.
mod sso_offered;
/// **Company sign-in is its own default-off feature** (A34-15): the default build
/// serves none of its doors.
mod sso_switch;
/// **gunnar.rs's public face** (TASKS-12, `public-site`): the default build
/// serves none of it, and the feature build serves it to a visitor through the
/// guest key, naming no private repository.
mod public_site;
/// **GUN-GUEST**: one address scheme, the console's document for everyone, guests
/// read-only, and (public-site) the page's content pre-rendered into it.
mod console_address;
/// **GUN-SEO-GUEST** (owner 2026-10-31): what a crawler reads at the site's www origin is
/// the console's own copy-text form, at one www URL, and nothing private.
#[cfg(feature = "public-site")]
mod seo_guest;
thread_local! {
/// The relying party the next [`console_waiting`] on this thread builds, as
/// `(rp_id, origin)`; `None` is `console.example`. GUN-SEO-GUEST sets the shape a live
/// site has: the bare site as the relying party, `https://www.<site>` as the origin.
static RELYING_PARTY: std::cell::RefCell<Option<(&'static str, &'static str)>> = const { std::cell::RefCell::new(None) };
}
/// **AIRGAP: forge import is off unless built (`forges`) and switched on (`GUNNAR_UI_FORGES`).**
mod forges_switch;
/// **Nobody sidesteps the throttle** (GUN-GUEST, owner 2026-10-30): every route of the console,
/// guest or signed in, passes the one limiter (`crate::throttle`).
mod throttled;
/// **A repository's owner writes on their own repository** (2026-10-18): the
/// issue, pull request and branch-protection writes, owner / stranger / visitor.
mod repo_owner_writes;
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use tonic::{Request, Response, Status};
use crate::pb;
use crate::pb::refs_server::{Refs, RefsServer};
use crate::pb::repos_server::{Repos, ReposServer};
use crate::pb::usage_server::{Usage, UsageServer};
/// The console's own principal: the one `enroll` gives a server-wide `Admin`.
const CONSOLE_SEED: [u8; 32] = [7u8; 32];
/// The guest principal: enrolled, holding no grants at all.
const GUEST_SEED: [u8; 32] = [23u8; 32];
const NS: &str = "nordisk";
/// A GiB, for the admin disk view's fixture.
const DISK_GIB: u64 = 1 << 30;
const PUBLIC: &str = "gunnar";
const PRIVATE: &str = "payroll";
/// A blob the private repository contains, and which must never be rendered to
/// a caller with no session. Distinctive so a `contains` assertion over a whole
/// response body is meaningful.
const SECRET_BYTES: &[u8] = b"the-private-blob-nobody-anonymous-may-read";
/// **The namespace's traffic, which is nobody's business but the tenant's.**
///
/// A wire-byte count for one day of [`NS`], picked so no other number in this
/// module or in an HTTP status line can spell it: a `contains` over the whole
/// response body is only evidence if the string it looks for could not have got
/// there any other way. It is the byte total, the millisecond stamp and the
/// day key that a leaked `Usage.Activity` answer would carry.
const SECRET_BYTES_MOVED: u64 = 748_912_336_571;
/// The day the traffic above happened on: midnight UTC, and a distinctive one.
const SECRET_DAY_MS: i64 = 20_694 * 86_400_000;
/// When the namespace was last pushed to. The single most operationally
/// interesting number on the whole activity answer, and the one a competitor
/// reading it would want most: it says whether the tenant is still alive.
const SECRET_LAST_PUSH_MS: i64 = SECRET_DAY_MS + 41_237_000;
/// The public repository's README, and the heading a rendered one must name.
///
/// Distinctive so that "the README was served" is a `contains` a test can make
/// against a whole response body, and so that the same string appearing under
/// the PRIVATE repository's refusal would be unmistakable.
const README_HEADING: &str = "The Public Repository";
const README_MARKDOWN: &str =
"# The Public Repository\n\nA line of prose, and [the guide](docs/guide.pdf).\n";
/// The object id the tree below points README.md at.
///
/// Both repositories are `HashKind::Sha256` in this fake, so a tree entry holds
/// **32** raw bytes and `git::parse_tree` hex-encodes them back into this. A
/// sha1 fake would read the wrong bytes as a name and the wrong name as an id,
/// which is the exact failure `crate::git` warns about.
const README_OID: &str = "aa00000000000000000000000000000000000000000000000000000000000011";
/// A third public repository, whose README is too big to typeset.
///
/// `MAX_SVG_BYTES` is 4 MiB and a typeset page costs about 105 bytes per
/// character, so this is the size at which the SVG grade gives way to the
/// sanitised-HTML one — the failure grade that is otherwise unreachable
/// through the router, because `readme::emit` cannot produce markup the
/// typesetter refuses.
const OVERSIZE: &str = "encyclopaedia";
/// The object id the oversize repository's tree points README.md at.
const HUGE_OID: &str = "bb00000000000000000000000000000000000000000000000000000000000022";
/// A README past the size a browser is handed a page for.
fn huge_readme() -> Vec<u8> {
let mut out = String::from("# An Encyclopaedia\n\n");
// 60 KB of prose, which typesets to roughly 6 MB of glyph paths.
for _ in 0..1_100 {
out.push_str("lorem ipsum dolor sit amet consectetur adipiscing elit\n\n");
}
out.into_bytes()
}
/// The blob oid of the tree's `guide.pdf`, which is a **Git LFS pointer** and
/// not a PDF — the shape gunnar's own `docs/gunnar-fast-setup.pdf` has.
const POINTER_OID: &str = "cc00000000000000000000000000000000000000000000000000000000000033";
/// The sha256 that pointer names: the key the LFS store holds the file under.
const LFS_OID: &str = "dd00000000000000000000000000000000000000000000000000000000000044";
/// The file itself, which lives only in the LFS store. A real PDF header, so a
/// test asserting "the PDF was served" is asserting about the file and not
/// about a string that could have come from anywhere.
const LFS_BODY: &[u8] = b"%PDF-1.7\nthe burned guide, whole\n%%EOF\n";
fn pointer_blob() -> Vec<u8> {
format!(
"version https://git-lfs.github.com/spec/v1\noid sha256:{LFS_OID}\nsize {}\n",
LFS_BODY.len()
)
.into_bytes()
}
/// A tree holding `README.md` and the LFS-backed `guide.pdf`, encoded the way
/// gunnar hands one back: the undecorated payload, with no `"tree <n>\0"`
/// header and no zlib frame. Entries are in git's own order, which is why
/// `README.md` comes first.
fn readme_tree(first: u8, last: u8) -> Vec<u8> {
let mut tree = Vec::new();
tree.extend_from_slice(b"100644 README.md\0");
let mut oid = [0u8; 32];
oid[0] = first;
oid[31] = last;
tree.extend_from_slice(&oid);
tree.extend_from_slice(b"100644 guide.pdf\0");
let mut pointer = [0u8; 32];
pointer[0] = 0xcc;
pointer[31] = 0x33;
tree.extend_from_slice(&pointer);
tree
}
/// GUN-GUEST's `docs/` (see [`FakeGunnar::with_docs`]): its tree, `docs/v2`'s, and the files.
const DOCS_TREE_OID: &str = "d000000000000000000000000000000000000000000000000000000000000001";
const DOCS_V2_OID: &str = "d000000000000000000000000000000000000000000000000000000000000002";
const DOCS_README_OID: &str = "d000000000000000000000000000000000000000000000000000000000000003";
const DOCS_APP_OID: &str = "d000000000000000000000000000000000000000000000000000000000000004";
/// The heading `docs/v2/README.md` carries, distinctive for a `contains`.
#[cfg(feature = "public-site")]
const DOCS_HEADING: &str = "The Second Edition Of The Docs";
const DOCS_README: &str = "# The Second Edition Of The Docs\n\nRead [the first](../README.md) too.\n";
/// A hex object id as the 32 raw bytes a sha256 tree entry holds.
fn raw_oid(hex: &str) -> [u8; 32] {
let mut out = [0u8; 32];
for (i, b) in out.iter_mut().enumerate() {
*b = u8::from_str_radix(&hex[2 * i..2 * i + 2], 16).expect("hex");
}
out
}
/// The object id `badge.svg` is stored under, in [`badge_tree`].
const BADGE_OID: &str = "5b00000000000000000000000000000000000000000000000000000000000077";
/// A small badge, the shape bestpractices.dev answers.
const BADGE_SVG: &[u8] = b"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"130\" height=\"20\"><text x=\"5\" y=\"14\">openssf best practices</text></svg>";
/// [`readme_tree`] with `badge.svg` between `README.md` and `guide.pdf` (git's order).
fn badge_tree(first: u8, last: u8) -> Vec<u8> {
let mut tree = Vec::new();
tree.extend_from_slice(b"100644 README.md\0");
let mut oid = [0u8; 32];
oid[0] = first;
oid[31] = last;
tree.extend_from_slice(&oid);
tree.extend_from_slice(b"100644 badge.svg\0");
let mut badge = [0u8; 32];
badge[0] = 0x5b;
badge[31] = 0x77;
tree.extend_from_slice(&badge);
tree.extend_from_slice(b"100644 guide.pdf\0");
let mut pointer = [0u8; 32];
pointer[0] = 0xcc;
pointer[31] = 0x33;
tree.extend_from_slice(&pointer);
tree
}
// ───────────────────────────────────────────────────────────────────────────
// A gunnar that authorises
// ───────────────────────────────────────────────────────────────────────────
/// What the fake knows about one repository.
#[derive(Clone)]
struct Held {
visibility: pb::Visibility,
/// oid -> bytes, the whole object store for this repository.
objects: BTreeMap<String, Vec<u8>>,
/// sha256 -> bytes: the LFS store, which is a different store from the
/// object database and holds the files the tree only points at.
lfs: BTreeMap<String, Vec<u8>>,
}
/// A file in the public repository with a great many distinct commits — the
/// shape an attacker picks, and the cheapest thing in the world to ask for.
const CROWDED_FILE: &str = "the-file-everybody-has-touched.rs";
/// How many distinct commits that file's lines come from. Well past the
/// console's ceiling, so the ceiling is what the test measures rather than the
/// fixture's size.
const CROWDED_FILE_COMMITS: usize = 500;
/// A tenant who pays for this console, and the private repository in the
/// namespace their own gunnar principal owns.
///
/// gunnar's rule, modelled below: *a principal is intrinsically admin of the
/// namespace named after it* — `control_plane::add_principal` creates the
/// account namespace WITH the principal and rolls the principal back if the
/// namespace is taken, so the two names are one claim. So `vera` reads
/// `vera/ledger` and nobody else does.
const TENANT: &str = "vera";
const TENANT_REPO: &str = "ledger";
/// A second tenant, who is the control: the same role, the same console, and
/// no business whatever with the first one's namespace.
const OTHER_TENANT: &str = "viggo";
/// A blob only [`TENANT`] may read. Distinctive, for the same reason
/// [`SECRET_BYTES`] is.
const LEDGER_BYTES: &[u8] = b"the-ledger-only-vera-paid-for";
/// The fake's issue threads, by (repository, number) (clippy: one name for the shape).
type IssueThreads = Arc<Mutex<BTreeMap<(String, u64), Vec<pb::ForgeEvent>>>>;
/// A gunnar that answers two principals differently, by the same rule the real
/// one uses: `Visibility::Public` is readable with no grant, everything else
/// needs one, and only the console's key has one.
#[derive(Clone)]
struct FakeGunnar {
repos: Arc<Mutex<BTreeMap<String, Held>>>,
/// Fingerprints seen, in order. The direct evidence of WHICH key signed a
/// call — an assertion on this is the only way to prove the console did not
/// merely get lucky with a permissive answer.
seen: Arc<Mutex<Vec<String>>>,
/// How many calls arrived at all. Reads the cache bypass: a request served
/// out of `Caches::repos` never reaches here.
calls: Arc<AtomicUsize>,
/// How many `Admin.VerifyChain` walks one HTTP request cost gunnar. The
/// amplification factor, counted where it lands.
verifications: Arc<AtomicUsize>,
/// Whether this gunnar knows `Refs.ReadLfsObject` at all. `false` is an
/// OLDER gunnar than the console — the skew the console is deployed under
/// by design, and the one `tests/contract.rs` exists for.
serves_lfs: bool,
/// Whether `Refs.ReadLfsObject` reads RANGES (`offset`/`length`, gunnar
/// 2026-10-15). `false` is a gunnar from before them: it ignores the two
/// fields and answers whole-or-refused under its 4 MiB ceiling.
lfs_ranges: bool,
/// **gunnar's identity table**: the principals that exist, and which
/// principal each registered key fingerprint belongs to.
///
/// Without this the console could enrol a per-tenant key and nothing would
/// be able to tell that it had — every call would still arrive as an
/// unknown fingerprint and be answered as a stranger, so the test would be
/// green on a console that never enrolled anything. `Access.AddKey` writes
/// here and [`FakeGunnar::may_read`] reads it, which is what makes "whose
/// key signed this, and what may that principal see" one question.
principals: Arc<Mutex<BTreeMap<String, pb::Principal>>>,
/// fingerprint -> principal id.
keys: Arc<Mutex<BTreeMap<String, String>>>,
/// The credential SCHEME each call arrived under, in the same order as
/// [`FakeGunnar::seen`].
schemes: Arc<Mutex<Vec<String>>>,
/// Whether `Admin.Capacity` / `StorageInfo` / `Health` answer (the admin
/// disk view). Off by default: the machine test drives an OLDER gunnar
/// that serves none of them, and must keep doing so.
admin_disks: bool,
/// **gunnar's event bus**, for `Events.Watch`: what [`FakeGunnar::emit`]
/// publishes, every watcher hears — narrowed per watcher the way gunnar's
/// "mine" scope narrows it.
bus: tokio::sync::broadcast::Sender<pb::Event>,
/// Every `Events.Watch`, as `<who> namespaces=[…]`.
watches: Arc<Mutex<Vec<String>>>,
/// **Whether any account has an identity provider** — `Sso.Discover`'s
/// `any_provider` ([`sso_offered`]). Off: a box where nobody configured one.
sso_providers: Arc<std::sync::atomic::AtomicBool>,
/// How many `Sso.Discover` calls arrived — what the status's cache saves.
discovers: Arc<AtomicUsize>,
/// `Sso.Begin`'s answer: `Ok(redirect_url)` or a refusal's message.
sso_begin: Arc<Mutex<Result<String, String>>>,
/// The `client` every `Sso.Begin` named (A34-6).
sso_clients: Arc<Mutex<Vec<String>>>,
/// **The forge log's issues** (`Forge.Append` / `ListIssues`), for
/// [`repo_owner_writes`].
issues: Arc<Mutex<Vec<pb::IssueState>>>,
/// Each issue's thread after its opening (`Forge.GetIssue`'s events), by
/// `(repo, number)`, for the public site's issue pages (PUB-ISSUES).
issue_threads: IssueThreads,
/// **gunnar's generic settings** (`Admin.GetSetting` / `SetSetting`), where the console
/// keeps its administrator's switches (`public-site` and `mcp` builds).
settings: Arc<Mutex<BTreeMap<String, Vec<u8>>>>,
/// The one commit's message `Refs.Log` answers (GUN-GUEST: a push changes it).
subject: Arc<Mutex<String>>,
}
impl FakeGunnar {
fn new() -> FakeGunnar {
let mut repos = BTreeMap::new();
repos.insert(
format!("{NS}/{PUBLIC}"),
Held {
visibility: pb::Visibility::Public,
objects: BTreeMap::from([
("pub-oid".to_owned(), b"a public blob".to_vec()),
// The tree `Refs.Log` below points at, and the README in
// it: without these the browse and readme routes could
// only ever answer 404 for a repository a visitor CAN see,
// and "refused" and "empty" would look the same.
("tree-oid".to_owned(), readme_tree(0xaa, 0x11)),
(README_OID.to_owned(), README_MARKDOWN.as_bytes().to_vec()),
// The tree's `guide.pdf`: a POINTER, which is all a tree
// ever holds for an LFS file.
(POINTER_OID.to_owned(), pointer_blob()),
]),
lfs: BTreeMap::from([(LFS_OID.to_owned(), LFS_BODY.to_vec())]),
},
);
repos.insert(
format!("{NS}/{OVERSIZE}"),
Held {
visibility: pb::Visibility::Public,
objects: BTreeMap::from([
("tree-oid".to_owned(), readme_tree(0xbb, 0x22)),
(HUGE_OID.to_owned(), huge_readme()),
]),
lfs: BTreeMap::new(),
},
);
repos.insert(
format!("{NS}/{PRIVATE}"),
Held {
visibility: pb::Visibility::Private,
objects: BTreeMap::from([
("sec-oid".to_owned(), SECRET_BYTES.to_vec()),
("tree-oid".to_owned(), readme_tree(0xee, 0x55)),
(POINTER_OID.to_owned(), pointer_blob()),
]),
// The same pointer, the same sha256, a file nobody anonymous
// may have: "the console followed a pointer it should not have"
// has somewhere to show up.
lfs: BTreeMap::from([(LFS_OID.to_owned(), SECRET_BYTES.to_vec())]),
},
);
// The tenants' own namespaces. Private, so "vera sees her ledger" can
// only ever be true because gunnar answered HER principal — a public
// one would be green on the guest key and prove nothing.
repos.insert(
format!("{TENANT}/{TENANT_REPO}"),
Held {
visibility: pb::Visibility::Private,
objects: BTreeMap::from([("ledger-oid".to_owned(), LEDGER_BYTES.to_vec())]),
lfs: BTreeMap::new(),
},
);
repos.insert(
format!("{OTHER_TENANT}/diary"),
Held {
visibility: pb::Visibility::Private,
objects: BTreeMap::from([("diary-oid".to_owned(), b"viggo's diary".to_vec())]),
lfs: BTreeMap::new(),
},
);
// Both tenants already have a gunnar principal — the paid signup's
// account, made before this console was asked anything. The console's
// own and the guest's are NOT here: they are keys the deployment
// enrolled, and this fake knows them by seed rather than by table.
let principals = [TENANT, OTHER_TENANT]
.into_iter()
.map(|name| {
(
name.to_owned(),
pb::Principal {
id: name.to_owned(),
name: name.to_owned(),
kind: pb::PrincipalKind::User as i32,
created_at_unix_ms: 1,
},
)
})
.collect();
FakeGunnar {
repos: Arc::new(Mutex::new(repos)),
seen: Arc::new(Mutex::new(Vec::new())),
calls: Arc::new(AtomicUsize::new(0)),
verifications: Arc::new(AtomicUsize::new(0)),
serves_lfs: true,
lfs_ranges: true,
principals: Arc::new(Mutex::new(principals)),
keys: Arc::new(Mutex::new(BTreeMap::new())),
schemes: Arc::new(Mutex::new(Vec::new())),
admin_disks: false,
bus: tokio::sync::broadcast::channel(64).0,
watches: Arc::new(Mutex::new(Vec::new())),
sso_providers: Arc::new(std::sync::atomic::AtomicBool::new(false)),
discovers: Arc::new(AtomicUsize::new(0)),
sso_begin: Arc::new(Mutex::new(Ok("https://idp.acme.example/authorize?state=s".to_owned()))),
sso_clients: Arc::new(Mutex::new(Vec::new())),
issues: Arc::new(Mutex::new(Vec::new())),
issue_threads: Arc::new(Mutex::new(BTreeMap::new())),
settings: Arc::new(Mutex::new(BTreeMap::new())),
subject: Arc::new(Mutex::new("the one commit".to_owned())),
}
}
/// **A `docs/` beside the README** (GUN-GUEST): `docs/v2/README.md` at depth two,
/// and `docs/app.js` (a file whose name ends like one of the console's assets).
fn with_docs(self) -> FakeGunnar {
{
let mut repos = self.repos.lock().unwrap();
let held = repos.get_mut(&format!("{NS}/{PUBLIC}")).expect("the public repository");
let mut root = readme_tree(0xaa, 0x11);
// git's order: `README.md`, `docs`, `guide.pdf`.
let at = root.windows(9).position(|w| w == b"100644 gu").expect("guide.pdf");
let mut docs = b"40000 docs\0".to_vec();
docs.extend_from_slice(&raw_oid(DOCS_TREE_OID));
root.splice(at..at, docs);
held.objects.insert("tree-oid".to_owned(), root);
let mut docs = b"100644 app.js\0".to_vec();
docs.extend_from_slice(&raw_oid(DOCS_APP_OID));
docs.extend_from_slice(b"40000 v2\0");
docs.extend_from_slice(&raw_oid(DOCS_V2_OID));
held.objects.insert(DOCS_TREE_OID.to_owned(), docs);
let mut v2 = b"100644 README.md\0".to_vec();
v2.extend_from_slice(&raw_oid(DOCS_README_OID));
held.objects.insert(DOCS_V2_OID.to_owned(), v2);
held.objects.insert(DOCS_README_OID.to_owned(), DOCS_README.as_bytes().to_vec());
held.objects.insert(DOCS_APP_OID.to_owned(), b"console.log('the docs script');\n".to_vec());
}
self
}
/// What gunnar holds under a generic setting's name.
fn setting(&self, name: &str) -> Option<Vec<u8>> {
self.settings.lock().unwrap().get(name).cloned()
}
/// Publish one ref update on gunnar's bus.
fn emit(&self, namespace: &str, repo: &str, ref_name: &str, new_oid: &str) {
let _ = self.bus.send(pb::Event {
seq: 1,
at_unix_ms: 1,
kind: pb::EventKind::RefUpdated as i32,
payload: Some(pb::event::Payload::RefUpdated(pb::RefUpdated {
repo: Some(pb::RepoPath {
namespace: namespace.to_owned(),
repo: repo.to_owned(),
}),
ref_name: ref_name.to_owned(),
new_oid: new_oid.to_owned(),
..Default::default()
})),
});
}
/// gunnar's "mine" for a watcher that named no namespaces: the console's
/// key hears everything; a principal hears its own account's repositories
/// (it is inside it); nobody here made their events public, so that is all.
fn mine(who: &str, event: &pb::Event) -> bool {
if who == "console" {
return true;
}
match &event.payload {
Some(pb::event::Payload::RefUpdated(e)) => {
e.repo.as_ref().is_some_and(|r| r.namespace == who)
}
_ => false,
}
}
/// The same gunnar, serving the three reads the admin disk view makes —
/// to the console's key only, as gunnar gates them (server-wide Read).
fn with_admin_disks(mut self) -> FakeGunnar {
self.admin_disks = true;
self
}
/// Server-wide Read, or the refusal gunnar gives without it.
fn server_read(&self, fingerprint: &str, verb: &str) -> Result<(), Status> {
if Self::granted(fingerprint) {
Ok(())
} else {
Err(Status::permission_denied(format!("principal has no server read grant ({verb})")))
}
}
/// The same gunnar, one release older: it has an LFS store and no RPC that
/// reads it.
fn without_the_lfs_rpc(mut self) -> FakeGunnar {
self.serves_lfs = false;
self
}
/// A gunnar with `Refs.ReadLfsObject` and no ranged reads: the release
/// before 2026-10-15.
fn without_lfs_ranges(mut self) -> FakeGunnar {
self.lfs_ranges = false;
self
}
/// Put `body` in the public repository's LFS store as the file the tree's
/// `guide.pdf` pointer names, with a pointer stating its size.
fn with_the_guide_in_lfs(self, body: Vec<u8>) -> FakeGunnar {
{
let mut repos = self.repos.lock().unwrap();
let held = repos
.get_mut(&format!("{NS}/{PUBLIC}"))
.expect("the public repository");
held.objects.insert(
POINTER_OID.to_owned(),
format!(
"version https://git-lfs.github.com/spec/v1\noid sha256:{LFS_OID}\nsize {}\n",
body.len()
)
.into_bytes(),
);
held.lfs.insert(LFS_OID.to_owned(), body);
}
self
}
/// **`badge.svg` in the public and the private repository** (BADGE-7): the
/// same tree as [`readme_tree`] with an SVG picture beside the README, the
/// shape `nordisk/ci-status` has for the OpenSSF badges gunnar.rs shows.
fn with_a_badge(self) -> FakeGunnar {
{
let mut repos = self.repos.lock().unwrap();
for (key, first, last) in [(format!("{NS}/{PUBLIC}"), 0xaa, 0x11), (format!("{NS}/{PRIVATE}"), 0xee, 0x55)] {
let held = repos.get_mut(&key).expect("the repository");
held.objects.insert("tree-oid".to_owned(), badge_tree(first, last));
held.objects.insert(BADGE_OID.to_owned(), BADGE_SVG.to_vec());
}
}
self
}
/// The fingerprint that signed this request, from the credential header.
///
/// The console mints a fresh `gunnar-sshsig-v2 <fingerprint> …` per call
/// (see [`crate::signer`]), so the fingerprint is the second whitespace-
/// separated field; [`FakeGunnar::admits`] refuses any other scheme. The signature itself is not verified here: this fake is
/// standing in for gunnar's ACL, not for its cryptography, and
/// `tests/contract.rs` already pins the credential format against gunnar's
/// own constants.
fn who<T>(&self, request: &Request<T>) -> String {
self.calls.fetch_add(1, Ordering::SeqCst);
let header = request
.metadata()
.get(crate::signer::CREDENTIAL_METADATA_KEY)
.and_then(|value| value.to_str().ok())
.unwrap_or_default()
.to_owned();
let fingerprint = header
.split_whitespace()
.nth(1)
.unwrap_or("(unsigned)")
.to_owned();
// The scheme every call arrived under, kept beside the fingerprint. A
// credential that names no method can be replayed against a different
// one, and a key minted per PERSON is the last place that may happen:
// the whole point of it is that gunnar answers it differently.
self.schemes.lock().expect("the scheme log").push(
header
.split_whitespace()
.next()
.unwrap_or("(none)")
.to_owned(),
);
self.seen
.lock()
.expect("the fingerprint log")
.push(fingerprint.clone());
fingerprint
}
/// **The credential let in, as a gunnar started with require_bound lets it
/// in** (stinky #24): its fingerprint, or the refusal.
fn admits<T>(&self, request: &Request<T>) -> Result<String, Status> {
let scheme = request
.metadata()
.get(crate::signer::CREDENTIAL_METADATA_KEY)
.and_then(|value| value.to_str().ok())
.and_then(|header| header.split_whitespace().next().map(str::to_owned));
if scheme.is_some_and(|scheme| scheme != crate::signer::CREDENTIAL_SCHEME) {
self.who(request);
return Err(Status::unauthenticated("a credential that names no method is refused"));
}
Ok(self.who(request))
}
/// Does this fingerprint hold a server-wide grant? Only the console's does.
fn granted(fingerprint: &str) -> bool {
fingerprint == signer(CONSOLE_SEED).fingerprint()
}
/// The principal a registered key belongs to, or `None` for a key this
/// table never saw — the guest's, or one the console never enrolled.
fn principal_of_key(&self, fingerprint: &str) -> Option<String> {
self.keys
.lock()
.expect("the key table")
.get(fingerprint)
.cloned()
}
/// The `SHA256:…` of the key the console enrolled for `principal`, if it
/// enrolled one.
///
/// The whole point of the tenant tests: an assertion that the fingerprint
/// on the wire is THIS one is an assertion about which private key the
/// console reached for, which is the only question worth asking here.
fn key_of_principal(&self, principal: &str) -> Option<String> {
self.keys
.lock()
.expect("the key table")
.iter()
.find(|(_, owner)| owner.as_str() == principal)
.map(|(fingerprint, _)| fingerprint.clone())
}
/// The real `AuthTable::may`, in one line: a grant, or the repository is
/// public.
///
/// **Plus the account rule**, which is gunnar's and not this fake's
/// invention: a principal is intrinsically admin of the namespace named
/// after it (`gunnar_core::AuthTable::owns_account`, and
/// `control_plane::add_principal` makes the two names one claim by creating
/// the namespace with the principal). That is what a tenant's own key buys
/// them, and it is the difference between "this console signs as the
/// tenant" and "this console signs as somebody with no grants at all".
///
/// `NotFound` and `PermissionDenied` are BOTH answered as
/// `PermissionDenied` for a caller with no grant, exactly as gunnar does —
/// telling a stranger apart "does not exist" from "you may not see it"
/// turns the port into an existence oracle for private repository names.
fn may_read(&self, fingerprint: &str, path: &pb::RepoPath) -> Result<Held, Status> {
let key = format!("{}/{}", path.namespace, path.repo);
let owns_account = self
.principal_of_key(fingerprint)
.is_some_and(|principal| principal == path.namespace);
let repos = self.repos.lock().expect("the catalogue");
let held = repos.get(&key);
match held {
Some(held)
if Self::granted(fingerprint)
|| owns_account
|| held.visibility == pb::Visibility::Public =>
{
Ok(held.clone())
}
// **The console's own key is told the truth**: a name gunnar does
// not hold is `NotFound` to a caller whose grant covers it, as the
// real control plane answers (`get_repo` → "… is not in the
// catalog"). Only a caller with no grant gets the uniform
// `PermissionDenied`. Modelled because the console's hidden-name
// layer (`api::absent`) has to fold BOTH into one answer.
None if Self::granted(fingerprint) => Err(Status::not_found(format!(
"repository {key} is not in the catalog"
))),
_ => Err(Status::permission_denied(format!(
"principal has no read grant on repo:{key}"
))),
}
}
/// Does gunnar's catalog hold this namespace? Every namespace a
/// repository here lives in, and every principal's own account namespace.
fn namespace_exists(&self, namespace: &str) -> bool {
self.repos
.lock()
.expect("the catalogue")
.keys()
.any(|key| key.split_once('/').is_some_and(|(ns, _)| ns == namespace))
|| self
.principals
.lock()
.expect("the principals")
.contains_key(namespace)
}
fn fingerprints(&self) -> Vec<String> {
self.seen.lock().expect("the fingerprint log").clone()
}
fn schemes(&self) -> Vec<String> {
self.schemes.lock().expect("the scheme log").clone()
}
}
/// The refusal every unmodelled RPC below returns.
///
/// They refuse rather than answering an empty `Ok`, because a stub that
/// succeeded would make a handler calling the wrong verb look like it worked.
///
/// Written out one method at a time rather than generated by a macro, and not
/// for want of trying: `#[tonic::async_trait]` rewrites each `async fn` into a
/// boxed future, and an attribute macro sees the impl block *before* a
/// function-like macro inside it expands — so macro-generated methods keep a
/// bare `async fn` signature the trait no longer has, and every one of them
/// fails to compile with `lifetimes do not match method in trait`.
fn unmodelled(verb: &'static str) -> Status {
Status::unimplemented(format!(
"{verb} is not modelled by the anonymous-access fake"
))
}
#[tonic::async_trait]
impl Repos for FakeGunnar {
// Encrypted repositories (gunnar 2026-10-05): not modelled by this fake.
async fn set_encrypted(
&self,
_: tonic::Request<pb::SetEncryptedRequest>,
) -> Result<tonic::Response<pb::SetEncryptedResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("Repos.SetEncrypted"))
}
async fn recipient_keys(
&self,
_: tonic::Request<pb::RecipientKeysRequest>,
) -> Result<tonic::Response<pb::RecipientKeysResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("Repos.RecipientKeys"))
}
async fn list(
&self,
request: Request<pb::ListReposRequest>,
) -> Result<Response<pb::ListReposResponse>, Status> {
let who = self.admits(&request)?;
// **The narrowing gunnar itself performs** (see
// `gunnar-cli/tests/public_listing.rs`): a caller with no grant is
// reduced to what it may read, not refused. Modelled here because it is
// what the console now depends on.
let repos = self
.repos
.lock()
.expect("the catalogue")
.iter()
.filter(|(_, held)| {
FakeGunnar::granted(&who) || held.visibility == pb::Visibility::Public
})
.map(|(key, held)| {
let (namespace, repo) = key.split_once('/').expect("ns/name");
pb::Repo {
path: Some(pb::RepoPath {
namespace: namespace.to_owned(),
repo: repo.to_owned(),
}),
visibility: held.visibility as i32,
hash_kind: pb::HashKind::Sha256 as i32,
default_branch: "refs/heads/main".to_owned(),
..Default::default()
}
})
.collect();
Ok(Response::new(pb::ListReposResponse {
repos,
next_page_token: String::new(),
}))
}
async fn get(
&self,
request: Request<pb::GetRepoRequest>,
) -> Result<Response<pb::GetRepoResponse>, Status> {
let who = self.admits(&request)?;
let path = request.into_inner().path.unwrap_or_default();
let held = self.may_read(&who, &path)?;
Ok(Response::new(pb::GetRepoResponse {
repo: Some(pb::Repo {
path: Some(path),
visibility: held.visibility as i32,
hash_kind: pb::HashKind::Sha256 as i32,
default_branch: "refs/heads/main".to_owned(),
..Default::default()
}),
}))
}
// `ArchiveOut`: the generated default stub answers Unimplemented.
async fn create_namespace(
&self,
_: Request<pb::CreateNamespaceRequest>,
) -> Result<Response<pb::CreateNamespaceResponse>, Status> {
Err(unmodelled("Repos.CreateNamespace"))
}
async fn list_namespaces(
&self,
_: Request<pb::ListNamespacesRequest>,
) -> Result<Response<pb::ListNamespacesResponse>, Status> {
Err(unmodelled("Repos.ListNamespaces"))
}
async fn delete_namespace(
&self,
_: Request<pb::DeleteNamespaceRequest>,
) -> Result<Response<pb::DeleteNamespaceResponse>, Status> {
Err(unmodelled("Repos.DeleteNamespace"))
}
async fn create(
&self,
_: Request<pb::CreateRepoRequest>,
) -> Result<Response<pb::CreateRepoResponse>, Status> {
Err(unmodelled("Repos.Create"))
}
async fn delete(
&self,
_: Request<pb::DeleteRepoRequest>,
) -> Result<Response<pb::DeleteRepoResponse>, Status> {
Err(unmodelled("Repos.Delete"))
}
async fn undelete(
&self,
_: Request<pb::UndeleteRepoRequest>,
) -> Result<Response<pb::UndeleteRepoResponse>, Status> {
Err(unmodelled("Repos.Undelete"))
}
async fn purge(
&self,
_: Request<pb::PurgeRepoRequest>,
) -> Result<Response<pb::PurgeRepoResponse>, Status> {
Err(unmodelled("Repos.Purge"))
}
async fn gc(
&self,
_: Request<pb::GcRepoRequest>,
) -> Result<Response<pb::GcRepoResponse>, Status> {
Err(unmodelled("Repos.Gc"))
}
async fn gc_status(
&self,
_: Request<pb::GcStatusRequest>,
) -> Result<Response<pb::GcStatusResponse>, Status> {
Err(unmodelled("Repos.GcStatus"))
}
async fn list_deleted(
&self,
_: Request<pb::ListDeletedRequest>,
) -> Result<Response<pb::ListDeletedResponse>, Status> {
Err(unmodelled("Repos.ListDeleted"))
}
async fn list_orphans(
&self,
_: Request<pb::ListOrphansRequest>,
) -> Result<Response<pb::ListOrphansResponse>, Status> {
Err(unmodelled("Repos.ListOrphans"))
}
async fn reclaim_orphan(
&self,
_: Request<pb::ReclaimOrphanRequest>,
) -> Result<Response<pb::ReclaimOrphanResponse>, Status> {
Err(unmodelled("Repos.ReclaimOrphan"))
}
async fn set_default_branch(
&self,
_: Request<pb::SetDefaultBranchRequest>,
) -> Result<Response<pb::SetDefaultBranchResponse>, Status> {
Err(unmodelled("Repos.SetDefaultBranch"))
}
async fn set_visibility(
&self,
_: Request<pb::SetVisibilityRequest>,
) -> Result<Response<pb::SetVisibilityResponse>, Status> {
Err(unmodelled("Repos.SetVisibility"))
}
}
#[tonic::async_trait]
impl Refs for FakeGunnar {
async fn list(
&self,
request: Request<pb::ListRefsRequest>,
) -> Result<Response<pb::ListRefsResponse>, Status> {
let who = self.admits(&request)?;
let repo = request.into_inner().repo.unwrap_or_default();
self.may_read(&who, &repo)?;
Ok(Response::new(pb::ListRefsResponse {
refs: vec![pb::RefTarget {
ref_name: "refs/heads/main".to_owned(),
oid: "commit-oid".to_owned(),
..Default::default()
}],
..Default::default()
}))
}
async fn get(
&self,
request: Request<pb::GetRefRequest>,
) -> Result<Response<pb::GetRefResponse>, Status> {
let who = self.admits(&request)?;
let repo = request.into_inner().repo.unwrap_or_default();
self.may_read(&who, &repo)?;
Ok(Response::new(pb::GetRefResponse {
ref_target: Some(pb::RefTarget {
ref_name: "HEAD".to_owned(),
oid: "commit-oid".to_owned(),
..Default::default()
}),
}))
}
async fn log(
&self,
request: Request<pb::LogRequest>,
) -> Result<Response<pb::LogResponse>, Status> {
let who = self.admits(&request)?;
let repo = request.into_inner().repo.unwrap_or_default();
self.may_read(&who, &repo)?;
Ok(Response::new(pb::LogResponse {
commits: vec![pb::Commit {
oid: "commit-oid".to_owned(),
message: self.subject.lock().expect("the subject").clone(),
tree_oid: "tree-oid".to_owned(),
..Default::default()
}],
..Default::default()
}))
}
async fn read_object(
&self,
request: Request<pb::ReadObjectRequest>,
) -> Result<Response<pb::ReadObjectResponse>, Status> {
let who = self.admits(&request)?;
let inner = request.into_inner();
let repo = inner.repo.unwrap_or_default();
let held = self.may_read(&who, &repo)?;
let data = held
.objects
.get(&inner.oid)
.cloned()
.ok_or_else(|| Status::not_found("no such object"))?;
Ok(Response::new(pb::ReadObjectResponse {
oid: inner.oid,
size: data.len() as u64,
data,
..Default::default()
}))
}
/// The LFS store, keyed by the sha256 a pointer names — authorised through
/// the same `may_read` every other read goes through, because that is the
/// claim worth testing: an LFS object is as private as the repository that
/// points at it, and the console must not have found a way around that.
async fn read_lfs_object(
&self,
request: Request<pb::ReadLfsObjectRequest>,
) -> Result<Response<pb::ReadLfsObjectResponse>, Status> {
if !self.serves_lfs {
return Err(unmodelled("Refs.ReadLfsObject"));
}
let who = self.admits(&request)?;
let inner = request.into_inner();
let repo = inner.repo.unwrap_or_default();
let held = self.may_read(&who, &repo)?;
let data = held
.lfs
.get(&inner.oid)
.cloned()
.ok_or_else(|| Status::not_found("no such LFS object"))?;
// gunnar's rules: a 4 MiB ceiling per answer, lowered by `max_bytes`;
// whole-or-refused without a range; with one (and a gunnar that reads
// them) the ceiling bounds the range and an offset past the end is
// refused.
const CEILING: u64 = 4 * 1024 * 1024;
let ceiling = match inner.max_bytes {
0 => CEILING,
n => n.min(CEILING),
};
let size = data.len() as u64;
if self.lfs_ranges && (inner.offset != 0 || inner.length != 0) {
let (offset, length) = (inner.offset, inner.length);
if offset > size || (offset == size && size > 0) {
return Err(Status::invalid_argument(format!(
"offset {offset} is past the end of LFS object {}, which is {size} bytes",
inner.oid
)));
}
if length > ceiling {
return Err(Status::resource_exhausted(format!(
"a range of {length} bytes is over the {ceiling}-byte ceiling for this request"
)));
}
let want = if length == 0 { ceiling } else { length }.min(size - offset);
let range = data[offset as usize..(offset + want) as usize].to_vec();
return Ok(Response::new(pb::ReadLfsObjectResponse {
oid: inner.oid,
size,
data: range,
offset,
}));
}
if size > ceiling {
return Err(Status::resource_exhausted(format!(
"LFS object {} is {size} bytes, over the {ceiling}-byte ceiling for this request",
inner.oid
)));
}
Ok(Response::new(pb::ReadLfsObjectResponse {
oid: inner.oid,
size,
data,
offset: 0,
}))
}
async fn diff(
&self,
_: Request<pb::DiffRequest>,
) -> Result<Response<pb::DiffResponse>, Status> {
Err(unmodelled("Refs.Diff"))
}
/// **A file whose lines come from as many distinct commits as the caller
/// asked for.**
///
/// Modelled rather than refused because the amplification GUI-025 is about
/// is counted in DISTINCT COMMITS, and the file is chosen by whoever makes
/// the request — so the fake has to be able to produce the expensive shape
/// on demand, exactly as a long-lived real file does.
async fn blame(
&self,
request: Request<pb::BlameRequest>,
) -> Result<Response<pb::BlameResponse>, Status> {
let who = self.admits(&request)?;
let asked = request.into_inner();
self.may_read(&who, &asked.repo.unwrap_or_default())?;
let commits = if asked.path == CROWDED_FILE {
CROWDED_FILE_COMMITS
} else {
3
};
let lines = (0..commits)
.map(|i| pb::BlameLine {
commit: format!("{i:040x}"),
author: "alice".to_owned(),
line: format!("line {i}").into_bytes(),
})
.collect();
Ok(Response::new(pb::BlameResponse { lines }))
}
async fn search(
&self,
_: Request<pb::SearchRequest>,
) -> Result<Response<pb::SearchResponse>, Status> {
Err(unmodelled("Refs.Search"))
}
}
/// **A gunnar that verifies a chain, and counts how often it was asked to.**
///
/// `Admin.VerifyChain` is a walk of history with its own budget on gunnar's
/// side, and the blame route calls it once per distinct commit in a file the
/// CALLER names. Only `verify_chain` is modelled: what the counter is for is
/// the amplification factor of one HTTP request, and every other verb here
/// would be a different question.
#[tonic::async_trait]
impl crate::pb::admin_server::Admin for FakeGunnar {
async fn verify_chain(
&self,
request: Request<pb::VerifyChainRequest>,
) -> Result<Response<pb::VerifyChainResponse>, Status> {
let who = self.admits(&request)?;
self.verifications.fetch_add(1, Ordering::SeqCst);
let asked = request.into_inner();
self.may_read(&who, &asked.repo.unwrap_or_default())?;
Ok(Response::new(pb::VerifyChainResponse {
links: vec![pb::ChainLink {
id: "signature".to_owned(),
verdict: pb::ChainLinkVerdict::ChainLinkProven as i32,
detail: "signed by alice".to_owned(),
}],
..Default::default()
}))
}
async fn health(
&self,
request: Request<pb::HealthRequest>,
) -> Result<Response<pb::HealthResponse>, Status> {
if !self.admin_disks {
return Err(unmodelled("Admin.Health"));
}
let who = self.admits(&request)?;
self.server_read(&who, "Admin.Health")?;
Ok(Response::new(pb::HealthResponse {
serving: true,
twins: vec![pb::TwinHeartbeat {
principal: "twin".to_owned(),
address: "10.13.8.122:40000".to_owned(),
disk_total_bytes: DISK_GIB * 400,
disk_used_bytes: DISK_GIB * 100,
fill_permille: 250,
state: "ok".to_owned(),
..Default::default()
}],
..Default::default()
}))
}
async fn stats(
&self,
_: Request<pb::StatsRequest>,
) -> Result<Response<pb::StatsResponse>, Status> {
Err(unmodelled("Admin.Stats"))
}
async fn storage_info(
&self,
request: Request<pb::StorageInfoRequest>,
) -> Result<Response<pb::StorageInfoResponse>, Status> {
if !self.admin_disks {
return Err(unmodelled("Admin.StorageInfo"));
}
let who = self.admits(&request)?;
self.server_read(&who, "Admin.StorageInfo")?;
Ok(Response::new(pb::StorageInfoResponse {
total_explode_bytes: DISK_GIB * 20,
total_explode_budget_bytes: DISK_GIB * 40,
..Default::default()
}))
}
async fn capacity(
&self,
request: Request<pb::CapacityRequest>,
) -> Result<Response<pb::CapacityResponse>, Status> {
if !self.admin_disks {
return Err(unmodelled("Admin.Capacity"));
}
let who = self.admits(&request)?;
self.server_read(&who, "Admin.Capacity")?;
// The disk report only when asked, as gunnar answers it.
let disks = request.into_inner().disks.then(|| pb::DiskSnapshot {
measured_at_unix_ms: 1_790_000_000_000,
mounts: vec![
pb::DiskMount {
source: "/dev/vda2".to_owned(),
mount_point: "/".to_owned(),
fs_type: "ext4".to_owned(),
labels: vec!["system".to_owned()],
measured: true,
total_bytes: DISK_GIB * 10,
free_bytes: DISK_GIB * 6,
available_bytes: DISK_GIB * 6,
used_bytes: DISK_GIB * 4,
..Default::default()
},
pb::DiskMount {
source: "/dev/md0".to_owned(),
mount_point: "/data".to_owned(),
fs_type: "xfs".to_owned(),
labels: vec!["data".to_owned()],
measured: true,
total_bytes: DISK_GIB * 300,
free_bytes: DISK_GIB * 200,
available_bytes: DISK_GIB * 200,
used_bytes: DISK_GIB * 100,
floor_bytes: DISK_GIB * 15,
servable_bytes: DISK_GIB * 185,
..Default::default()
},
],
devices: vec![
pb::BlockDevice {
name: "md0".to_owned(),
size_bytes: DISK_GIB * 300,
members: vec!["vdb1".to_owned(), "vdc1".to_owned()],
raid_level: "raid0".to_owned(),
..Default::default()
},
pb::BlockDevice {
name: "vdb".to_owned(),
size_bytes: DISK_GIB * 151,
partitions: vec![pb::BlockPartition { name: "vdb1".to_owned(), size_bytes: DISK_GIB * 150 }],
..Default::default()
},
pb::BlockDevice {
name: "vdc".to_owned(),
size_bytes: DISK_GIB * 151,
partitions: vec![pb::BlockPartition { name: "vdc1".to_owned(), size_bytes: DISK_GIB * 150 }],
..Default::default()
},
],
..Default::default()
});
Ok(Response::new(pb::CapacityResponse {
root: "/data".to_owned(),
total_bytes: DISK_GIB * 300,
free_bytes: DISK_GIB * 200,
floor_bytes: DISK_GIB * 15,
servable_bytes: DISK_GIB * 185,
data_set: "2×150G raid0 xfs".to_owned(),
disks,
..Default::default()
}))
}
async fn grow_start(
&self,
_: Request<pb::GrowStartRequest>,
) -> Result<Response<pb::GrowStatusResponse>, Status> {
Err(unmodelled("Admin.GrowStart"))
}
async fn grow_status(
&self,
_: Request<pb::GrowStatusRequest>,
) -> Result<Response<pb::GrowStatusResponse>, Status> {
Err(unmodelled("Admin.GrowStatus"))
}
async fn grow_resume(
&self,
_: Request<pb::GrowResumeRequest>,
) -> Result<Response<pb::GrowStatusResponse>, Status> {
Err(unmodelled("Admin.GrowResume"))
}
async fn get_setting(
&self,
request: Request<pb::GetSettingRequest>,
) -> Result<Response<pb::GetSettingResponse>, Status> {
// Not counted in `seen` or `calls`: the console asks its switches once at start, and
// the tests that count a visitor's calls must not see that ask.
let value = self.setting(&request.into_inner().name);
Ok(Response::new(pb::GetSettingResponse { present: value.is_some(), value: value.unwrap_or_default() }))
}
async fn set_setting(
&self,
request: Request<pb::SetSettingRequest>,
) -> Result<Response<pb::SetSettingResponse>, Status> {
let pb::SetSettingRequest { name, value } = request.into_inner();
let previous = self.settings.lock().unwrap().insert(name, value);
Ok(Response::new(pb::SetSettingResponse { had: previous.is_some(), previous: previous.unwrap_or_default() }))
}
}
/// **A gunnar that counts a namespace's traffic, and guards it with the same
/// grant it guards everything else with.**
///
/// `Usage.Read` and `Usage.Activity` both take `Read` **on the namespace**, so
/// a principal with no grant gets `PermissionDenied` from both — modelled here
/// exactly as [`FakeGunnar::may_read`] models the repository rule, and for the
/// same reason: a permissive fake could not tell which key the console used.
///
/// The point of serving these at all is that the numbers must be *reachable*.
/// A fake that refused the RPC outright would make the anonymous test below
/// green against a console that happily proxied the route — the body would be
/// an error either way. So there is a real, distinctive ledger behind it, and
/// what is asserted is that not one digit of it reaches a visitor.
#[tonic::async_trait]
impl Usage for FakeGunnar {
async fn read(
&self,
request: Request<pb::ReadUsageRequest>,
) -> Result<Response<pb::ReadUsageResponse>, Status> {
let who = self.admits(&request)?;
let namespace = request.into_inner().namespace;
if !FakeGunnar::granted(&who) {
return Err(Status::permission_denied(format!(
"principal has no read grant on namespace:{namespace}"
)));
}
if !self.namespace_exists(&namespace) {
return Err(Status::not_found(format!(
"namespace {namespace} is not in the catalog"
)));
}
Ok(Response::new(pb::ReadUsageResponse {
usage: Some(pb::NamespaceUsage {
namespace,
bytes_in_today: SECRET_BYTES_MOVED,
last_push_unix_ms: SECRET_LAST_PUSH_MS,
..Default::default()
}),
allowance: Some(pb::Allowance::default()),
..Default::default()
}))
}
async fn read_all(
&self,
_: Request<pb::ReadAllUsageRequest>,
) -> Result<Response<pb::ReadAllUsageResponse>, Status> {
Err(unmodelled("Usage.ReadAll"))
}
async fn activity(
&self,
request: Request<pb::ActivityRequest>,
) -> Result<Response<pb::ActivityResponse>, Status> {
let who = self.admits(&request)?;
let namespace = request.into_inner().namespace;
if !FakeGunnar::granted(&who) {
return Err(Status::permission_denied(format!(
"principal has no read grant on namespace:{namespace}"
)));
}
if !self.namespace_exists(&namespace) {
return Err(Status::not_found(format!(
"namespace {namespace} is not in the catalog"
)));
}
Ok(Response::new(pb::ActivityResponse {
days: vec![pb::DayActivity {
day_unix_ms: SECRET_DAY_MS,
bytes_in: SECRET_BYTES_MOVED,
bytes_out: SECRET_BYTES_MOVED,
pushes: 1,
fetches: 1,
last_push_unix_ms: SECRET_LAST_PUSH_MS,
..Default::default()
}],
retained_days: 400,
}))
}
async fn admit_plan(
&self,
request: Request<pb::AdmitPlanRequest>,
) -> Result<Response<pb::AdmitPlanResponse>, Status> {
let who = self.admits(&request)?;
let namespace = request.into_inner().namespace;
if !FakeGunnar::granted(&who) {
return Err(Status::permission_denied(format!(
"principal has no read grant on namespace:{namespace}"
)));
}
Ok(Response::new(pb::AdmitPlanResponse {
policy: "usage-share".into(),
window_days: 30,
used_bytes: SECRET_BYTES_MOVED,
..Default::default()
}))
}
}
/// **gunnar's identity table, as much of it as a tenant credential needs.**
///
/// Four verbs are modelled and the rest refuse. `AddKey` is the one that
/// matters: it is how the console enrols the key it has minted for a tenant,
/// and the row it writes is what [`FakeGunnar::may_read`] later consults to
/// decide what that key may see. A fake that accepted `AddKey` and remembered
/// nothing would answer every tenant read as a stranger's, and the test would
/// be green on a console that had enrolled nothing at all.
///
/// `AddPrincipal` creates the account namespace with the principal the way
/// gunnar's own `control_plane::add_principal` does — and refuses when the name
/// is taken, which is the case the console has to survive rather than paper
/// over.
#[tonic::async_trait]
impl crate::pb::events_server::Events for FakeGunnar {
async fn watch(
&self,
request: Request<pb::WatchRequest>,
) -> Result<Response<tonic::codegen::BoxStream<pb::Event>>, Status> {
let fingerprint = self.admits(&request)?;
let who = if Self::granted(&fingerprint) {
"console".to_owned()
} else {
self.principal_of_key(&fingerprint)
.unwrap_or_else(|| "guest".to_owned())
};
self.watches.lock().expect("the watch log").push(format!(
"{who} namespaces={:?}",
request.get_ref().namespaces
));
let mut rx = self.bus.subscribe();
let stream = async_stream::stream! {
loop {
match rx.recv().await {
Ok(event) if FakeGunnar::mine(&who, &event) => yield Ok(event),
Ok(_) | Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
}
}
};
Ok(Response::new(Box::pin(stream)))
}
}
#[tonic::async_trait]
impl crate::pb::access_server::Access for FakeGunnar {
/// gunnar's `Access.Check`, as this fake decides: the console's key holds
/// everything, a principal everything in its own account, and anybody a
/// READ of a public repository.
async fn check(
&self,
request: Request<pb::CheckRequest>,
) -> Result<Response<pb::CheckResponse>, Status> {
let who = self.admits(&request)?;
let asked = request.into_inner();
let owns = Self::granted(&who)
|| self.principal_of_key(&who).is_some_and(|p| p == asked.namespace);
let public_read = asked.level == pb::GrantLevel::Read as i32
&& self
.repos
.lock()
.expect("the catalogue")
.get(&format!("{}/{}", asked.namespace, asked.repo))
.is_some_and(|h| h.visibility == pb::Visibility::Public);
let allowed = owns || public_read;
Ok(Response::new(pb::CheckResponse {
allowed,
reason: if allowed { String::new() } else { format!("principal holds no grant on {}", asked.namespace) },
}))
}
async fn add_principal(
&self,
request: Request<pb::AddPrincipalRequest>,
) -> Result<Response<pb::AddPrincipalResponse>, Status> {
self.admits(&request)?;
let asked = request.into_inner();
let mut principals = self.principals.lock().expect("the identity table");
if principals.contains_key(&asked.name) {
return Err(Status::already_exists(format!(
"a principal named {:?} already exists",
asked.name
)));
}
// gunnar's ids are the name (`Principal::new(name, name, …)`), which is
// a fact this console has to live with and one the report says out
// loud: pinning an id pins the row that was seen, not an identity that
// survives a delete and a recreate under the same name.
let principal = pb::Principal {
id: asked.name.clone(),
name: asked.name.clone(),
kind: asked.kind,
created_at_unix_ms: 2,
};
principals.insert(asked.name, principal.clone());
Ok(Response::new(pb::AddPrincipalResponse {
principal: Some(principal),
}))
}
async fn list_principals(
&self,
request: Request<pb::ListPrincipalsRequest>,
) -> Result<Response<pb::ListPrincipalsResponse>, Status> {
self.admits(&request)?;
Ok(Response::new(pb::ListPrincipalsResponse {
principals: self
.principals
.lock()
.expect("the identity table")
.values()
.cloned()
.collect(),
next_page_token: String::new(),
..Default::default()
}))
}
/// Nobody here holds a passkey yet (thin layer L: passkeys are gunnar's).
async fn list_credentials(
&self,
request: Request<pb::ListCredentialsRequest>,
) -> Result<Response<pb::ListCredentialsResponse>, Status> {
self.admits(&request)?;
Ok(Response::new(pb::ListCredentialsResponse {
credentials: Vec::new(),
}))
}
async fn add_key(
&self,
request: Request<pb::AddKeyRequest>,
) -> Result<Response<pb::AddKeyResponse>, Status> {
self.admits(&request)?;
let asked = request.into_inner();
if !self
.principals
.lock()
.expect("the identity table")
.contains_key(&asked.principal_id)
{
return Err(Status::not_found(format!(
"no principal {:?} to register a key against",
asked.principal_id
)));
}
// The fingerprint is gunnar's to derive from the key, never the
// caller's to choose — the same rule `api::account` states.
let parsed = ssh_key::PublicKey::from_openssh(&asked.openssh_public_key)
.map_err(|e| Status::invalid_argument(format!("not an OpenSSH public key: {e}")))?;
let fingerprint = parsed.fingerprint(ssh_key::HashAlg::Sha256).to_string();
self.keys
.lock()
.expect("the key table")
.insert(fingerprint.clone(), asked.principal_id.clone());
Ok(Response::new(pb::AddKeyResponse {
key: Some(pb::PublicKey {
fingerprint,
algo: "ssh-ed25519".to_owned(),
principal_id: asked.principal_id,
comment: asked.comment,
added_at_unix_ms: 3,
last_used_unix_ms: None,
expires_at_unix_ms: asked.expires_at_unix_ms,
}),
}))
}
async fn revoke_key(
&self,
request: Request<pb::RevokeKeyRequest>,
) -> Result<Response<pb::RevokeKeyResponse>, Status> {
self.admits(&request)?;
let asked = request.into_inner();
self.keys
.lock()
.expect("the key table")
.remove(&asked.fingerprint);
Ok(Response::new(pb::RevokeKeyResponse {}))
}
async fn list_keys(
&self,
request: Request<pb::ListKeysRequest>,
) -> Result<Response<pb::ListKeysResponse>, Status> {
self.admits(&request)?;
let asked = request.into_inner();
Ok(Response::new(pb::ListKeysResponse {
keys: self
.keys
.lock()
.expect("the key table")
.iter()
.filter(|(_, owner)| owner.as_str() == asked.principal_id)
.map(|(fingerprint, owner)| pb::PublicKey {
fingerprint: fingerprint.clone(),
algo: "ssh-ed25519".to_owned(),
principal_id: owner.clone(),
comment: String::new(),
added_at_unix_ms: 3,
last_used_unix_ms: None,
expires_at_unix_ms: None,
})
.collect(),
}))
}
async fn rename_principal(
&self,
_: Request<pb::RenamePrincipalRequest>,
) -> Result<Response<pb::RenamePrincipalResponse>, Status> {
Err(unmodelled("Access.RenamePrincipal"))
}
async fn remove_principal(
&self,
_: Request<pb::RemovePrincipalRequest>,
) -> Result<Response<pb::RemovePrincipalResponse>, Status> {
Err(unmodelled("Access.RemovePrincipal"))
}
async fn grant(
&self,
_: Request<pb::GrantRequest>,
) -> Result<Response<pb::GrantResponse>, Status> {
Err(unmodelled("Access.Grant"))
}
async fn revoke(
&self,
_: Request<pb::RevokeRequest>,
) -> Result<Response<pb::RevokeResponse>, Status> {
Err(unmodelled("Access.Revoke"))
}
async fn list_grants(
&self,
_: Request<pb::ListGrantsRequest>,
) -> Result<Response<pb::ListGrantsResponse>, Status> {
Err(unmodelled("Access.ListGrants"))
}
async fn add_group_member(
&self,
_: Request<pb::AddGroupMemberRequest>,
) -> Result<Response<pb::AddGroupMemberResponse>, Status> {
Err(unmodelled("Access.AddGroupMember"))
}
async fn remove_group_member(
&self,
_: Request<pb::RemoveGroupMemberRequest>,
) -> Result<Response<pb::RemoveGroupMemberResponse>, Status> {
Err(unmodelled("Access.RemoveGroupMember"))
}
async fn list_group_members(
&self,
_: Request<pb::ListGroupMembersRequest>,
) -> Result<Response<pb::ListGroupMembersResponse>, Status> {
Err(unmodelled("Access.ListGroupMembers"))
}
async fn list_group_membership_events(
&self,
_: Request<pb::ListGroupMembershipEventsRequest>,
) -> Result<Response<pb::ListGroupMembershipEventsResponse>, Status> {
Err(unmodelled("Access.ListGroupMembershipEvents"))
}
}
// ───────────────────────────────────────────────────────────────────────────
// The console under test
// ───────────────────────────────────────────────────────────────────────────
/// ★ **The fake answers a method-blind credential as gunnar does with
/// require_bound on** (stinky #24, on since 5.3.3): a v1 header is refused, so
/// a console that regressed to sending one is red here instead of green on a
/// fake that never looked; the v2 header the console sends is let in.
#[test]
fn the_fake_refuses_a_method_blind_credential() {
let fake = FakeGunnar::new();
let console = signer(CONSOLE_SEED);
let with = |header: String| {
let mut request = Request::new(());
request.metadata_mut().insert(crate::signer::CREDENTIAL_METADATA_KEY, header.parse().expect("an ascii header"));
request
};
let v1 = console.credential(1_700_000_000_000, "nonce-0123456789ab").expect("signs");
let refused = fake.admits(&with(v1)).expect_err("a method-blind credential is refused");
assert_eq!(refused.code(), tonic::Code::Unauthenticated, "{refused:?}");
let v2 = console.credential_for(1_700_000_000_000, "nonce-0123456789ab", "Repos.List").expect("signs");
assert_eq!(fake.admits(&with(v2)).expect("a bound credential is let in"), console.fingerprint());
}
fn signer(seed: [u8; 32]) -> crate::signer::Signer {
crate::signer::Signer::new(ssh_key::PrivateKey::from(
ssh_key::private::Ed25519Keypair::from_seed(&seed),
))
.expect("a fixed seed makes a signer")
}
/// A console pointed at [`FakeGunnar`], in the posture that matters.
///
/// `admin_token: Some(_)` is **load-bearing**, and is the trap this harness
/// exists to avoid. With neither a passkey nor an admin token configured,
/// `require_auth` takes THE ONE BYPASS and runs every request unauthenticated —
/// which, on a console holding a server-wide admin grant, means every request
/// runs as admin. A test written in that posture would show a private
/// repository being served anonymously and would be *right to*, proving
/// nothing about the guest identity at all.
///
/// `guest` is a parameter because half of what is asserted here is what happens
/// when there is no guest identity: the anonymous surface must not exist, and
/// must not fall back to the console's key.
///
/// The passkey subsystem is returned beside the router so a test can mint a
/// REAL session cookie for it ([`signed_in`]). That is what extends this
/// harness past the anonymous path the module doc warns about: a session minted
/// here goes through `require_auth`, the role floor and `api::Reader` exactly as
/// a browser's does, and the fingerprint log says which key the console reached
/// for on the other side.
///
/// The event hub comes back too, so a test can put a line on the live feed and
/// watch who is shown it.
async fn console(
fake: FakeGunnar,
with_guest: bool,
) -> (
axum::Router,
Arc<crate::auth::Webauthn>,
crate::sse::EventHub,
) {
console_with(fake, with_guest.then_some(GUEST_SEED), None).await
}
/// The same console, with the guest identity's KEY chosen by the caller.
///
/// One test needs a guest key that is not grantless: the deployment mistake
/// `auth::routes::public_repos` has always warned about is pointing
/// `GUNNAR_UI_GUEST_IDENTITY` at a key that holds a grant — the console's own,
/// copied because it was to hand. Every other caller wants [`console`].
/// `monetize` is the third credential and it is `None` for every fixture here
/// but one lane: [`paid_signup`] needs it, because the routes it drives are
/// registered only on a console that HAS it (`App::signup_offered`), and a
/// harness that could not hand one over could only ever assert their absence.
/// It arrives as an address rather than a `Monetize`, so the caller owns the
/// fake server and its call log.
// An open build has no third credential to take, so the parameter is unused
// there — the same shape `App::monetize` itself has.
#[cfg_attr(not(feature = "metered"), allow(unused_variables))]
async fn console_with(
fake: FakeGunnar,
guest_seed: Option<[u8; 32]>,
monetize_addr: Option<String>,
) -> (
axum::Router,
Arc<crate::auth::Webauthn>,
crate::sse::EventHub,
) {
console_provided_by(fake, guest_seed, monetize_addr, None, None).await
}
/// [`console_with`], its monetize handle told whose shop its purchases are:
/// `Some(site)` is a SOLD (provided) Gunnar (one shop, 2026-10-07); `product`
/// the product id its config names (G15; `None`: unset).
#[cfg_attr(not(feature = "metered"), allow(unused_variables))]
async fn console_provided_by(
fake: FakeGunnar,
guest_seed: Option<[u8; 32]>,
monetize_addr: Option<String>,
provider: Option<&str>,
product: Option<&str>,
) -> (
axum::Router,
Arc<crate::auth::Webauthn>,
crate::sse::EventHub,
) {
console_waiting(fake, guest_seed, monetize_addr, provider, product, None, None).await
}
/// How long a fixture's monetize is waited on; `None` is the handle's own
/// default. Nothing to wait on in the open build.
#[cfg(feature = "metered")]
type Waits = Option<crate::monetize::Deadlines>;
#[cfg(not(feature = "metered"))]
type Waits = Option<std::convert::Infallible>;
/// [`console_provided_by`] (`product` as there), its monetize waited on for `deadlines` — THIS
/// console's, and no other's (stinky #21).
#[cfg_attr(not(feature = "metered"), allow(unused_variables))]
async fn console_waiting(
fake: FakeGunnar,
guest_seed: Option<[u8; 32]>,
monetize_addr: Option<String>,
provider: Option<&str>,
product: Option<&str>,
deadlines: Waits,
ui_dir: Option<std::path::PathBuf>,
) -> (
axum::Router,
Arc<crate::auth::Webauthn>,
crate::sse::EventHub,
) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("a loopback port");
let addr = listener.local_addr().expect("its address");
let serving = fake.clone();
tokio::spawn(async move {
tonic::transport::Server::builder()
.add_service(ReposServer::new(serving.clone()))
.add_service(RefsServer::new(serving.clone()))
.add_service(crate::pb::admin_server::AdminServer::new(serving.clone()))
.add_service(crate::pb::access_server::AccessServer::new(serving.clone()))
.add_service(crate::pb::events_server::EventsServer::new(serving.clone()))
.add_service(crate::pb::sso_server::SsoServer::new(serving.clone()))
.add_service(crate::pb::forge_server::ForgeServer::new(serving.clone()))
.add_service(crate::pb::pull_requests_server::PullRequestsServer::new(serving.clone()))
.add_service(crate::pb::policy_server::PolicyServer::new(serving.clone()))
.add_service(UsageServer::new(serving))
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
.await
});
let endpoint = format!("http://{addr}");
let gunnar = crate::grpc::Gunnar::connect_lazy(&endpoint, Arc::new(signer(CONSOLE_SEED)))
.expect("a lazy channel");
let guest = guest_seed.map(|seed| {
crate::grpc::Gunnar::connect_lazy(&endpoint, Arc::new(signer(seed)))
.expect("a lazy guest channel")
});
let store = crate::state::StateStore::open(
tempfile::Builder::new()
.suffix(".redb")
.tempfile()
.expect("a temp file")
.into_temp_path()
.keep()
.expect("keep the path"),
)
.expect("a state store");
// A real passkey subsystem, so `require_auth` runs the role ladder and a
// session cookie can be minted for it. It does NOT retire the break-glass
// token below — that happens only once an ADMIN passkey exists, and the
// sessions minted here are a viewer's and an operator's.
let rp = RELYING_PARTY.with(|r| r.borrow_mut().take()).unwrap_or(("console.example", "https://console.example"));
let webauthn = Arc::new(
crate::auth::Webauthn::new(
crate::auth::RelyingParty {
rp_id: rp.0.to_owned(),
origin: rp.1.to_owned(),
rp_name: "gunnar".to_owned(),
},
store.clone(),
)
.expect("a passkey subsystem"),
);
let feed = crate::sse::EventHub::detached();
let router = crate::serve::router_around(Arc::new(crate::app::App {
webauthn: Some(webauthn.clone()),
// The metered build has a third credential. Every fixture here has none
// but the paid-signup lane's, whose routes do not exist without it.
#[cfg(feature = "metered")]
monetize: monetize_addr.map(|addr| {
let m = crate::monetize::Monetize::connect_lazy(&addr, crate::auth::signup::tests::BEARER, "gunnar")
.expect("a lazy monetize channel");
let m = match deadlines {
Some(deadlines) => m.with_deadlines(deadlines),
None => m,
};
let m = if provider.is_some() { m.with_provider(provider) } else { m };
match product {
Some(id) => m.with_product(id).expect("a product id"),
None => m,
}
}),
// See the doc comment: without this the whole suite is vacuous.
admin_token: Some("break-glass".to_owned()),
// Detached from gunnar's stream and published into by hand: what the
// feed test asserts is who is SHOWN a line, not where it came from.
events: feed.clone(),
gunnar,
twin: None,
guest,
reviewers: Arc::new(crate::reviewer::ReviewerKeys::new(
endpoint.clone(),
store.clone(),
)),
tenants: Arc::new(crate::tenant::TenantKeys::new(endpoint, store.clone())),
forges: Default::default(),
imports: Default::default(),
forge_apps: Default::default(),
ui_dir,
caches: crate::api::Caches::default(),
objects: Default::default(),
// A real store, so the README route in this harness takes the same
// cached path a live console does — and so that the second read of a
// public README is asserted to cost no second walk of the tree.
readmes: Arc::new(crate::readme::Cache::new(Some(store))),
}), throttled::FRONT.with(|f| f.borrow_mut().take()));
(router, webauthn, feed)
}
/// **The guests' pages open** where a switch holds them (GUN-GUEST): a `public-site` build
/// shows a visitor only `/` while its public site is off, so a test of the guest's pages
/// turns it on first, as an administrator would. Nothing to do in any other build.
#[cfg_attr(not(feature = "public-site"), allow(unused_variables))]
async fn guests_on(app: &axum::Router, passkeys: &crate::auth::Webauthn) {
#[cfg(feature = "public-site")]
{
use tower::ServiceExt;
let admin = passkeys.make_session("root", crate::role::Role::Admin, true);
let put = http::Request::put("/api/public-site")
.header(http::header::COOKIE, format!("{}={admin}", crate::auth::SESSION_COOKIE))
.header(http::header::CONTENT_TYPE, "application/json")
.header(http::header::ORIGIN, passkeys.origin())
.body(axum::body::Body::from("{\"on\":true}"))
.expect("a request");
assert_eq!(app.clone().oneshot(put).await.expect("an answer").status(), http::StatusCode::OK);
}
}
/// A request with **no credential of any kind**: no cookie, no bearer token.
/// This is the visitor.
async fn anon(app: &axum::Router, uri: &str) -> (http::StatusCode, String) {
send(app, http::Method::GET, uri, None).await
}
/// The same request signed by the console's break-glass admin token, for the
/// "and an operator still sees it" half of each assertion.
async fn admin(app: &axum::Router, uri: &str) -> (http::StatusCode, String) {
send(app, http::Method::GET, uri, Some("break-glass")).await
}
async fn send(
app: &axum::Router,
method: http::Method,
uri: &str,
token: Option<&str>,
) -> (http::StatusCode, String) {
use tower::ServiceExt;
let mut request = http::Request::builder().method(method).uri(uri);
if let Some(token) = token {
request = request.header(http::header::AUTHORIZATION, format!("Bearer {token}"));
}
let response = app
.clone()
.oneshot(request.body(axum::body::Body::empty()).expect("a request"))
.await
.expect("the console answers");
let status = response.status();
let body = axum::body::to_bytes(response.into_body(), 1 << 20)
.await
.expect("a readable body");
(status, String::from_utf8_lossy(&body).into_owned())
}
/// **The same request, carrying a REAL session cookie for a person of `role`.**
///
/// Minted by the console's own passkey subsystem and presented as a browser
/// presents it, so the request crosses `require_auth`, `role_floor` and
/// `api::Reader` by the production path. Nothing here inserts a `Caller` by
/// hand: a test that did would prove the handler works once the gate has
/// already been passed, which is the assertion nobody needs.
async fn signed_in(
app: &axum::Router,
keys: &crate::auth::Webauthn,
user: &str,
role: crate::role::Role,
uri: &str,
) -> (http::StatusCode, String) {
use tower::ServiceExt;
let cookie = keys.make_session(user, role, true);
let request = http::Request::builder()
.method(http::Method::GET)
.uri(uri)
.header(
http::header::COOKIE,
format!("{}={cookie}", crate::auth::SESSION_COOKIE),
)
.body(axum::body::Body::empty())
.expect("a request");
let response = app
.clone()
.oneshot(request)
.await
.expect("the console answers");
let status = response.status();
let body = axum::body::to_bytes(response.into_body(), 1 << 20)
.await
.expect("a readable body");
(status, String::from_utf8_lossy(&body).into_owned())
}
/// Every read a visitor is allowed to attempt, against ONE repository.
///
/// A list rather than a test each, because what is being asserted is a property
/// of the whole surface: *no route on it leaks a private repository*. Adding a
/// route to `is_anonymous_read` and not to this list is the mistake worth
/// catching, and `the_anonymous_surface_is_exactly_this_list` below is what
/// catches it.
fn read_routes(namespace: &str, name: &str) -> Vec<String> {
let base = format!("/api/repos/{namespace}/{name}");
vec![
base.clone(),
format!("{base}/refs"),
format!("{base}/head"),
format!("{base}/log"),
// History's picker and its "all branches" view (2026-10-18): the same
// route, so the same rule — public repositories only.
format!("{base}/log?ref=refs/heads/main"),
format!("{base}/log?all=1"),
format!("{base}/browse?rev=&path="),
format!("{base}/object?oid=sec-oid"),
format!("{base}/readme"),
format!("{base}/raw?path=README.md"),
]
}
// ───────────────────────────────────────────────────────────────────────────
// The tests
// ───────────────────────────────────────────────────────────────────────────
/// ★ **RUNTIME LOGS (2026-09-30): the front's access line names WHO called.**
/// The front's :443 wraps this console in an access log (gunnar-front
/// web/serve.rs); the caller is decided by `require_auth`, so the answer
/// carries it as [`crate::auth::middleware::AccessPrincipal`] — a passkey
/// session's user, `admin-token` for the break-glass bearer — and nothing
/// when there is no caller (the front says `anonymous`). Never the bearer
/// itself. Asked of the router, not of `require_auth`'s source (stinky #22).
#[tokio::test]
async fn the_answer_names_the_caller_for_the_fronts_access_line() {
use tower::ServiceExt;
let (app, keys, _) = console(FakeGunnar::new(), true).await;
let named = |credential: Option<(http::HeaderName, String)>| {
let app = app.clone();
async move {
let mut request = http::Request::builder().uri("/api/repos");
if let Some((name, value)) = credential {
request = request.header(name, value);
}
let answer = app.oneshot(request.body(axum::body::Body::empty()).expect("a request")).await.expect("the console answers");
(answer.status(), answer.extensions().get::<crate::auth::middleware::AccessPrincipal>().map(|p| p.0.clone()))
}
};
let (status, who) = named(Some((http::header::AUTHORIZATION, "Bearer break-glass".to_owned()))).await;
assert_eq!((status, who.as_deref()), (http::StatusCode::OK, Some("admin-token")));
let cookie = keys.make_session("olle", crate::role::Role::Viewer, true);
let (status, who) = named(Some((http::header::COOKIE, format!("{}={cookie}", crate::auth::SESSION_COOKIE)))).await;
assert_eq!((status, who.as_deref()), (http::StatusCode::OK, Some("olle")));
let (_, who) = named(None).await;
assert_eq!(who, None, "a visitor with no credential is named by nobody here");
}
/// **The whole point, in one assertion: a private repository is not named on
/// the page served to people who are not logged in.**
#[tokio::test]
async fn the_login_page_lists_public_repositories_and_no_others() {
let fake = FakeGunnar::new();
let (app, _passkeys, _feed) = console(fake.clone(), true).await;
let (status, body) = anon(&app, "/login/repos").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(
body.contains(&format!("{NS}/{PUBLIC}")),
"the public repository must be listed: {body}"
);
assert!(
!body.contains(PRIVATE),
"a PRIVATE repository was named on the login page: {body}"
);
// …and it was the guest key that asked. Without this the test above is
// green on a console that used its admin key against a gunnar that happened
// to be asked a question with a harmless answer.
let guest_fingerprint = signer(GUEST_SEED).fingerprint().to_owned();
assert_eq!(
fake.fingerprints(),
vec![guest_fingerprint],
"the login page must be served by the GUEST key, never the console's"
);
}
/// **The direct read, not merely the absence from a listing.** Knowing the path
/// of a private repository must buy a stranger nothing.
#[tokio::test]
async fn a_private_repository_is_refused_on_every_anonymous_read_route() {
let (app, _passkeys, _feed) = console(FakeGunnar::new(), true).await;
for route in read_routes(NS, PRIVATE) {
let (status, body) = anon(&app, &route).await;
// 404, not 403, and the one body `api::absent` answers for a name that
// is not there: a private repository must be indistinguishable from
// none (see `an_unknown_namespace_is_404_and_a_private_one_is_the_same_404`).
assert_eq!(
status,
http::StatusCode::NOT_FOUND,
"{route} answered {status} instead of refusing: {body}"
);
assert_eq!(body, crate::api::absent::body().to_string(), "{route}");
assert!(
!body.contains(std::str::from_utf8(SECRET_BYTES).expect("utf8")),
"{route} leaked the private repository's contents: {body}"
);
}
}
/// The same routes on a PUBLIC repository are served — otherwise the test above
/// would pass on a console that refused everything, which is not the feature.
#[tokio::test]
async fn a_public_repository_is_readable_with_no_session() {
let fake = FakeGunnar::new();
let (app, _passkeys, _feed) = console(fake.clone(), true).await;
for route in read_routes(NS, PUBLIC) {
let (status, body) = anon(&app, &route).await;
// `object?oid=sec-oid` names an object this repository does not hold,
// so it is a 404 here rather than a 200 — which is exactly right, and
// is a different answer from the 403 the private repository gives.
assert!(
status == http::StatusCode::OK || status == http::StatusCode::NOT_FOUND,
"{route} was refused for a PUBLIC repository: {status} {body}"
);
}
// Every call reached gunnar as the guest, and not one as the console.
let guest = signer(GUEST_SEED).fingerprint().to_owned();
let console_key = signer(CONSOLE_SEED).fingerprint().to_owned();
let seen = fake.fingerprints();
assert!(!seen.is_empty(), "no call reached gunnar at all");
assert!(
seen.iter().all(|f| *f == guest),
"an anonymous read used a key other than the guest's: {seen:?} (the console's is \
{console_key})"
);
}
/// **The catalogue an anonymous visitor gets is the narrowed one, and it does
/// not come out of the shared cache.**
///
/// The cache is the subtle half. `Caches::repos` holds one value for the whole
/// server with no key for who asked; a guest reading it would be handed the
/// admin's answer without a gRPC call being made at all. So this warms it as
/// the admin first — which is the ordering that would actually happen on a
/// live console, where an operator is logged in somewhere — and then asserts
/// the visitor still gets the narrow answer.
#[tokio::test]
async fn an_anonymous_catalogue_never_comes_out_of_the_admin_cache() {
let fake = FakeGunnar::new();
let (app, _passkeys, _feed) = console(fake.clone(), true).await;
let (status, admin_body) = admin(&app, "/api/repos").await;
assert_eq!(status, http::StatusCode::OK, "{admin_body}");
assert!(
admin_body.contains(PRIVATE),
"an admin must still see private repositories: {admin_body}"
);
let (status, anon_body) = anon(&app, "/api/repos").await;
assert_eq!(status, http::StatusCode::OK, "{anon_body}");
assert!(
anon_body.contains(PUBLIC),
"the public repository must be listed: {anon_body}"
);
assert!(
!anon_body.contains(PRIVATE),
"the visitor was served the ADMIN's cached catalogue: {anon_body}"
);
}
/// ★ **A logged-in VIEWER reads with the guest key, and never with the
/// console's.**
///
/// The hole this module's own doc warned about, one step up from the anonymous
/// path: `api::Reader` used to hand `App::gunnar` — the server-wide admin key —
/// to *any* request that carried a `Caller`, whatever its role, and every GET
/// is floored at `Role::Viewer` whichever namespace the path names. So the
/// lowest rung the console mints, the one a paid signup hands out, read every
/// private repository on the box.
///
/// Seen red before the fix, on this harness: the six private read routes
/// answered **200** to `viewer-vera`, the body carried `SECRET_BYTES`, and the
/// fingerprint log showed the CONSOLE's key. Asserted the way the anonymous
/// tests are — on which key reached gunnar, not on the status code — because a
/// permissive fake would answer a viewer the same either way.
///
/// The operator at the end is the control that keeps the rest honest: this must
/// be a narrowing of the tenant role, not a console that refuses everybody.
///
/// Since the tenant credential, this is the case where the console cannot name
/// the caller at gunnar: `viewer-vera`'s passkey is bound to no principal, so
/// there is no credential that would be hers and the guest is the honest
/// answer. A BOUND viewer is
/// `a_tenants_read_is_signed_by_their_own_key_and_shows_them_their_own_repo`,
/// and the two together are the whole of what `api::Reader` decides.
#[tokio::test]
async fn a_viewers_session_reads_with_the_guest_key_and_never_the_consoles() {
use crate::role::Role;
let fake = FakeGunnar::new();
let (app, keys, _feed) = console(fake.clone(), true).await;
let console_key = signer(CONSOLE_SEED).fingerprint().to_owned();
let guest = signer(GUEST_SEED).fingerprint().to_owned();
// Warm the shared catalogue cache as an admin first, which is the ordering
// a live console really has — somebody is logged in somewhere — and which
// is what makes the `/api/repos` assertion below about the cache as well as
// about the channel.
let (status, body) = admin(&app, "/api/repos").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(body.contains(PRIVATE), "an admin still sees it all: {body}");
let before = fake.fingerprints().len();
for route in read_routes(NS, PRIVATE) {
let (status, body) = signed_in(&app, &keys, "viewer-vera", Role::Viewer, &route).await;
assert_eq!(
status,
http::StatusCode::NOT_FOUND,
"{route} answered a VIEWER {status} about a private repository: {body}"
);
assert!(
!body.contains(std::str::from_utf8(SECRET_BYTES).expect("utf8")),
"{route} leaked the private repository to a viewer: {body}"
);
}
let (status, body) = signed_in(&app, &keys, "viewer-vera", Role::Viewer, "/api/repos").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(body.contains(PUBLIC), "{body}");
assert!(
!body.contains(PRIVATE),
"the viewer was served the ADMIN's cached catalogue: {body}"
);
let seen: Vec<String> = fake.fingerprints().split_off(before);
assert!(!seen.is_empty(), "no call reached gunnar at all");
assert!(
seen.iter().all(|f| *f == guest),
"a viewer's read was signed with a key other than the guest's: {seen:?} (the console's \
is {console_key})"
);
// The control. An operator asks for the same private repository and gets
// it, with the console's key — so the refusals above are a narrowing of the
// tenant role and not a console that has stopped answering.
let (status, body) = signed_in(
&app,
&keys,
"operator-olof",
Role::Operator,
&format!("/api/repos/{NS}/{PRIVATE}"),
)
.await;
assert_eq!(
status,
http::StatusCode::OK,
"an operator must still read a private repository: {body}"
);
assert!(
fake.fingerprints().contains(&console_key),
"the operator's read did not use the console's key"
);
}
/// ★ **A tenant's read is signed by the TENANT's own key — not the guest's, not
/// the console's — and shows that tenant their own private repository.**
///
/// The consequence of the commit above, and the reason it could not ship on its
/// own: answering a viewer with the grantless guest key stopped the leak and
/// took the paying tenant's own repositories with it. A console that cannot
/// show somebody what they are paying for is not a fixed console.
///
/// So the third credential. The console mints an ed25519 key per console user,
/// enrols it on THAT person's gunnar principal with `Access.AddKey` — which is
/// what its server-wide Admin is for — and signs that session's reads with it.
/// gunnar then applies the tenant's own grants, on gunnar's side of the wire,
/// where a mistake in this crate cannot reach them.
///
/// Every assertion here is about WHICH KEY signed, because that is the only
/// question a permissive fake cannot answer by accident:
///
/// * `vera` reads `vera/ledger` — **private** — and the fingerprint log for
/// that request holds her key and no other. Not the guest's, which would see
/// nothing; not the console's, which would see everything and is the hole
/// this whole branch exists to close.
/// * `viggo`, the same role on the same console, is refused the same
/// repository. The refusal is gunnar's: it is answering a different
/// principal, and the fingerprint proves the console asked as him.
/// * `vera` is still refused `nordisk/payroll`, which is nobody's account
/// namespace but the console's business. A tenant credential that read the
/// whole box would be the old hole wearing a new key.
///
/// Red before the tenant credential: `vera`'s read of her own ledger was
/// answered 403 — the guest key, signed by a principal with no grants — and
/// `fake.key_of_principal("vera")` was `None`, because the console had never
/// enrolled anything for her.
#[tokio::test]
async fn a_tenants_read_is_signed_by_their_own_key_and_shows_them_their_own_repo() {
use crate::role::Role;
let fake = FakeGunnar::new();
let (app, keys, _feed) = console(fake.clone(), true).await;
let guest = signer(GUEST_SEED).fingerprint().to_owned();
let console_key = signer(CONSOLE_SEED).fingerprint().to_owned();
// Bound, which is what registration does now: the passkey carries the id
// of the principal it acts for, and nothing resolves a name afterwards.
keys.plant_bound_credential_for_tests(TENANT, Role::Viewer, TENANT);
keys.plant_bound_credential_for_tests(OTHER_TENANT, Role::Viewer, OTHER_TENANT);
let before = fake.fingerprints().len();
let (status, body) = signed_in(
&app,
&keys,
TENANT,
Role::Viewer,
&format!("/api/repos/{TENANT}/{TENANT_REPO}"),
)
.await;
assert_eq!(
status,
http::StatusCode::OK,
"a paying tenant was refused their own private repository: {body}"
);
let tenant_key = fake
.key_of_principal(TENANT)
.expect("the console never enrolled a key for the tenant at all");
assert_ne!(tenant_key, guest, "the tenant's key IS the guest's");
assert_ne!(tenant_key, console_key, "the tenant's key IS the console's");
let seen: Vec<String> = fake.fingerprints().split_off(before);
assert!(!seen.is_empty(), "no call reached gunnar at all");
assert!(
seen.iter().all(|f| *f == tenant_key || *f == console_key),
"a tenant's read was signed by a key that is neither theirs nor the \
console's administration of it: {seen:?}"
);
assert!(
!seen.contains(&guest),
"a tenant's read was signed by the grantless guest, which is what left \
them unable to see what they pay for: {seen:?}"
);
// **And it is signed the way every other credential this console makes is.**
// A per-person key is the last place a method-blind credential may survive:
// it is the one gunnar answers differently, so a credential of its that
// named no method could be replayed from one RPC onto another.
assert!(
fake.schemes()
.iter()
.all(|scheme| scheme == crate::signer::CREDENTIAL_SCHEME),
"a call arrived under a credential scheme that is not the method-bound one: {:?}",
fake.schemes()
);
// The console's admin key may appear here and only for one reason: the
// enrolment itself. It must never be what a repository was READ with.
let read_calls: Vec<&String> = seen.iter().filter(|f| **f == console_key).collect();
assert!(
read_calls.len() <= 3,
"the console's admin key signed {} calls for one tenant read; it is \
supposed to sign the provisioning and nothing else: {seen:?}",
read_calls.len()
);
// The other tenant, same role, same console, same repository.
let before = fake.fingerprints().len();
let (status, body) = signed_in(
&app,
&keys,
OTHER_TENANT,
Role::Viewer,
&format!("/api/repos/{TENANT}/{TENANT_REPO}"),
)
.await;
assert_eq!(
status,
http::StatusCode::NOT_FOUND,
"one tenant read another tenant's private repository: {body}"
);
assert!(
!body.contains(std::str::from_utf8(LEDGER_BYTES).expect("utf8")),
"the ledger leaked to the wrong tenant: {body}"
);
let other_key = fake
.key_of_principal(OTHER_TENANT)
.expect("the console never enrolled a key for the other tenant");
assert_ne!(
other_key, tenant_key,
"both tenants were signed with ONE key, so the refusal above was luck"
);
let seen: Vec<String> = fake.fingerprints().split_off(before);
assert!(
seen.contains(&other_key),
"the refused read was not signed as the tenant it was refused for: {seen:?}"
);
// And a tenant credential is not a console credential: the namespace that
// belongs to neither of them is still closed.
let (status, body) = signed_in(
&app,
&keys,
TENANT,
Role::Viewer,
&format!("/api/repos/{NS}/{PRIVATE}"),
)
.await;
assert_eq!(
status,
http::StatusCode::NOT_FOUND,
"a tenant key read a private repository outside their own namespace: {body}"
);
assert!(
!body.contains(std::str::from_utf8(SECRET_BYTES).expect("utf8")),
"a tenant key leaked the server's private repository: {body}"
);
}
/// ★ **A passkey bound to a gunnar principal keeps acting for THAT principal,
/// whatever the console user happens to be called.**
///
/// GUI-001's durable half. The binding session -> principal was
/// `.find(|p| p.name == user)` — a string comparison against a name the
/// registrant chose, redone on every call — so whichever principal came to hold
/// that name became the identity the session acted for, at any later time, with
/// gunnar unable to object because the caller was the console and the console
/// is Admin.
///
/// Here the console user is called `vera`, gunnar holds a principal called
/// `vera`, and the two are **not** the same identity: the passkey was pinned at
/// registration to `viggo`. A console that still resolved by name would read
/// `vera/ledger` and this test would say so. What it must do instead is read
/// `viggo/diary` — the principal it was bound to — and be refused the namespace
/// whose name it shares.
///
/// Red before `StoredCred::principal_id`: there was no pin to disagree with,
/// `vera` read `vera/ledger`, and the first assertion below failed with a 200.
#[tokio::test]
async fn a_passkey_acts_for_the_principal_it_was_bound_to_and_not_for_its_namesake() {
use crate::role::Role;
let fake = FakeGunnar::new();
let (app, keys, _feed) = console(fake.clone(), true).await;
keys.plant_bound_credential_for_tests(TENANT, Role::Viewer, OTHER_TENANT);
let (status, body) = signed_in(
&app,
&keys,
TENANT,
Role::Viewer,
&format!("/api/repos/{TENANT}/{TENANT_REPO}"),
)
.await;
assert_eq!(
status,
http::StatusCode::NOT_FOUND,
"a session acted for the principal that merely SHARES its name: {body}"
);
assert!(
!body.contains(std::str::from_utf8(LEDGER_BYTES).expect("utf8")),
"the namesake's ledger was served: {body}"
);
let (status, body) = signed_in(
&app,
&keys,
TENANT,
Role::Viewer,
&format!("/api/repos/{OTHER_TENANT}/diary"),
)
.await;
assert_eq!(
status,
http::StatusCode::OK,
"a session did not act for the principal it was BOUND to: {body}"
);
// And the key that read it is enrolled on the bound principal, which is
// what makes the 200 above evidence rather than a coincidence.
assert!(
fake.key_of_principal(OTHER_TENANT).is_some(),
"nothing was enrolled on the bound principal"
);
assert!(
fake.key_of_principal(TENANT).is_none(),
"the console enrolled a key on the namesake principal after all"
);
}
/// ★ **The live feed shows a viewer the repositories they may see, and not the
/// whole server's.**
///
/// The pump holds one unfiltered subscription on the console's admin key — the
/// comment beside it says so: *"Everything, unfiltered: the console filters in
/// the browser."* Filtering in the browser is not a permission check; the bytes
/// were already on the wire. `<namespace>/<repo>` alone tells a viewer which
/// private repositories exist and when each tenant last pushed, and the
/// summaries carry principal ids, pull-request titles and ref names.
///
/// Red before the per-subscriber narrowing: the viewer's stream carried the
/// PRIVATE repository's line verbatim, `payroll` and all. Red again before
/// 2026-10-02: an OPERATOR's stream carried it too (the console's own key, so
/// nothing was narrowed). The admin at the end is the control — narrowing a
/// feed until it says nothing would pass the first assertion and break the
/// console. The backlog and the live tail go through the same `may_see`, and
/// `the_backlog_a_non_admin_is_handed_is_narrowed_too` holds the first.
#[tokio::test]
async fn the_live_feed_shows_a_viewer_only_what_that_viewer_may_read() {
use crate::role::Role;
let fake = FakeGunnar::new();
let (app, keys, feed) = console(fake.clone(), true).await;
let line = |repo: &str, summary: &str| crate::sse::FeedItem {
seq: 1,
at_unix_ms: 1,
kind: "ref_updated".to_owned(),
repo: Some(format!("{NS}/{repo}")),
summary: summary.to_owned(),
dropped: None,
};
// The private line goes out FIRST, so "the public one arrived" cannot be
// green while the private one is still queued ahead of it.
let private = "refs/heads/salaries moved to 0123456789";
let public = "refs/heads/main moved to abcdef0123";
// **Only an administrator's feed is the whole server's** (owner,
// 2026-10-02: the Overview's "latest events" showed pushes to private
// repositories to people who cannot read them). An operator is narrowed
// like a viewer — to what THEIR OWN credential reads — and the admin at the
// end is the control.
// Since the owner's final navigation (2026-10-02, "no feed reachable by a
// non-admin may carry another account's events"), a session below admin
// is not told even about ANOTHER account's PUBLIC repository: `nordisk` is
// none of these three people's account. Their own account's lines are
// `an_accounts_feed_carries_its_own_repositories_and_no_other_accounts`.
for (user, role, may_see_private) in [
("viewer-vera", Role::Viewer, false),
("operator-olof", Role::Operator, false),
("admin-astrid", Role::Admin, true),
] {
let heard = feed_lines(&app, &keys, user, role, || {
// The administrator's feed is the console's shared pump; everybody
// else's is their own `Events.Watch` at gunnar. Both carry the
// same two pushes.
feed.publish(line(PRIVATE, private));
feed.publish(line(PUBLIC, public));
fake.emit(NS, PRIVATE, "refs/heads/salaries", "0123456789abcdef");
fake.emit(NS, PUBLIC, "refs/heads/main", "abcdef0123456789");
})
.await;
assert!(heard.contains("connected"), "{user}'s feed never opened: {heard:?}");
assert_eq!(
heard.contains(public),
may_see_private,
"{user}'s feed and another account's PUBLIC repository: {heard:?}"
);
assert_eq!(
heard.contains(private),
may_see_private,
"{user}'s feed carried {PRIVATE}: {heard:?}"
);
assert_eq!(
heard.contains(PRIVATE),
may_see_private,
"{user}'s feed named a repository they may not read: {heard:?}"
);
}
// ★ **Below the administrator, the feed is asked of gunnar as the person**
// (thin layer, owner 2026-10-04) — here the guest's key, as these two
// sessions are bound to no principal — with no namespaces named, so gunnar's
// "mine" decides. Never one console-keyed stream filtered here.
let watches = fake.watches.lock().expect("the watch log").clone();
assert_eq!(
watches,
vec!["guest namespaces=[]".to_owned(), "guest namespaces=[]".to_owned()],
"who asked gunnar for a feed: {watches:?}"
);
}
/// ★ **The console's backlog is the administrator's** (owner, 2026-10-02; thin
/// layer 2026-10-04). A non-admin connecting AFTER a push to a private
/// repository is not handed the console's shared backlog at all — their recent
/// history is gunnar's replay of their OWN watch; the admin is handed it.
#[tokio::test]
async fn the_backlog_a_non_admin_is_handed_is_narrowed_too() {
use crate::role::Role;
let fake = FakeGunnar::new();
let (app, keys, feed) = console(fake.clone(), true).await;
let line = |repo: &str, summary: &str| crate::sse::FeedItem {
seq: 1,
at_unix_ms: 1,
kind: "ref_updated".to_owned(),
repo: Some(format!("{NS}/{repo}")),
summary: summary.to_owned(),
dropped: None,
};
let private = "refs/heads/bonus moved to 9876543210";
let public = "refs/heads/docs moved to fedcba9876";
// Published BEFORE anybody connects: this is the backlog.
feed.publish(line(PRIVATE, private));
feed.publish(line(PUBLIC, public));
for (user, role, may_see_private) in [
("viewer-vera", Role::Viewer, false),
("operator-olof", Role::Operator, false),
("admin-astrid", Role::Admin, true),
] {
let heard = feed_lines(&app, &keys, user, role, || {}).await;
assert!(heard.contains("connected"), "{user}'s feed never opened: {heard:?}");
assert_eq!(heard.contains(public), may_see_private, "{user}'s backlog and another account's public repository: {heard:?}");
assert_eq!(heard.contains(private) || heard.contains(PRIVATE), may_see_private, "{user}'s backlog: {heard:?}");
}
}
/// ★ **An account's feed carries its own repositories and no other
/// account's** — and since 2026-10-04 that is gunnar's answer to vera's own
/// `Events.Watch` (owner, 2026-10-02, the final navigation: events by context —
/// a repository's on its Activity tab, the account's under You, the whole
/// server's only under Administration → Overview). vera, bound to her own
/// principal, hears `vera/ledger` and neither `nordisk/gunnar` (another
/// account's PUBLIC repository) nor `nordisk/payroll`; `?repo=` narrows any
/// feed to one repository, and never widens a non-admin's; the admin hears
/// everything, and is the control.
#[tokio::test]
async fn an_accounts_feed_carries_its_own_repositories_and_no_other_accounts() {
use crate::role::Role;
let fake = FakeGunnar::new();
let (app, keys, feed) = console(fake.clone(), true).await;
keys.plant_bound_credential_for_tests(TENANT, Role::Viewer, TENANT);
let line = |path: String, summary: &str| crate::sse::FeedItem {
seq: 1,
at_unix_ms: 1,
kind: "ref_updated".to_owned(),
repo: Some(path),
summary: summary.to_owned(),
dropped: None,
};
let own = "refs/heads/main moved to 1111111111";
let others_public = "refs/heads/main moved to 2222222222";
let others_private = "refs/heads/main moved to 3333333333";
feed.publish(line(format!("{NS}/{PUBLIC}"), others_public));
feed.publish(line(format!("{NS}/{PRIVATE}"), others_private));
feed.publish(line(format!("{TENANT}/{TENANT_REPO}"), own));
// What gunnar's bus carries while vera watches: the same three pushes.
let pushes = || {
fake.emit(NS, PUBLIC, "refs/heads/main", "2222222222ffff");
fake.emit(NS, PRIVATE, "refs/heads/main", "3333333333ffff");
fake.emit(TENANT, TENANT_REPO, "refs/heads/main", "1111111111ffff");
};
let mine = feed_lines_at(&app, &keys, TENANT, Role::Viewer, "/api/events", own, pushes).await;
assert!(mine.contains(own), "vera was not told about her own repository: {mine:?}");
assert!(!mine.contains(others_public) && !mine.contains(&format!("{NS}/{PUBLIC}")), "vera was told about another account's public repository: {mine:?}");
assert!(!mine.contains(others_private), "vera was told about another account's private repository: {mine:?}");
// ★ Asked of gunnar with HER OWN key and no namespaces: gunnar's "mine".
let her = fake.key_of_principal(TENANT).expect("the console enrolled vera's key");
assert!(!her.is_empty());
assert_eq!(
fake.watches.lock().expect("the watch log").clone(),
vec![format!("{TENANT} namespaces=[]")],
"vera's feed was not her own watch"
);
let tab = feed_lines_at(&app, &keys, TENANT, Role::Viewer, &format!("/api/events?repo={TENANT}/{TENANT_REPO}"), own, pushes).await;
assert!(tab.contains(own), "her repository's Activity tab is empty: {tab:?}");
let wider = feed_lines_at(&app, &keys, TENANT, Role::Viewer, &format!("/api/events?repo={NS}/{PUBLIC}"), others_public, pushes).await;
assert!(!wider.contains(others_public), "?repo= widened a non-admin's feed to another account: {wider:?}");
let all = feed_lines_at(&app, &keys, "admin-astrid", Role::Admin, "/api/events", own, || {}).await;
assert!(all.contains(own) && all.contains(others_public) && all.contains(others_private), "the admin's feed is not the whole server's: {all:?}");
let one = feed_lines_at(&app, &keys, "admin-astrid", Role::Admin, &format!("/api/events?repo={NS}/{PRIVATE}"), others_private, || {}).await;
assert!(one.contains(others_private) && !one.contains(own) && !one.contains(others_public), "?repo= did not narrow the admin's feed to one repository: {one:?}");
}
/// ★ **One session cannot hold the console's whole fan-out open.**
///
/// Every live feed pins a `broadcast::Receiver`, and a receiver that is not
/// read holds its place in a ring of `FANOUT_CAPACITY` items until it is
/// dropped. Nothing counted them: not per session and not in total, so one
/// logged-in viewer with a loop that opens `/api/events` and never reads it
/// could hold as many as they liked, and every one of them made the fan-out a
/// little slower for everybody else.
///
/// The cap is per SESSION, which is the half this asserts: the ninth feed for
/// one cookie is refused with a 429 that says to close a tab, a DIFFERENT
/// session is admitted at the same moment — so this is a bucket and not a
/// global ceiling wearing one — and closing one connection gives the place
/// back rather than burning it.
///
/// Red before the census: the ninth, the ninetieth and the nine-hundredth were
/// all 200, and `live_feeds()` did not exist to count them.
#[tokio::test]
async fn one_session_may_not_hold_the_whole_fan_out_open() {
use crate::role::Role;
let fake = FakeGunnar::new();
let (app, keys, feed) = console(fake.clone(), true).await;
let cap = crate::sse::PER_SESSION_FEEDS;
// Held, not dropped: a response whose body is still alive is a connection
// that is still open, which is exactly what the census counts.
let mine = keys.make_session("operator-olof", Role::Operator, true);
let mut open = Vec::new();
for nth in 1..=cap {
let response = feed_response(&app, &mine).await;
assert_eq!(
response.status(),
http::StatusCode::OK,
"feed {nth} of {cap} was refused before the cap was reached"
);
open.push(response);
}
assert_eq!(feed.live_feeds(), cap, "the census did not count them");
let refused = feed_response(&app, &mine).await;
assert_eq!(
refused.status(),
http::StatusCode::TOO_MANY_REQUESTS,
"a session opened feed {} of {cap}",
cap + 1
);
assert_eq!(
refused
.headers()
.get(http::header::RETRY_AFTER)
.and_then(|v| v.to_str().ok()),
Some("5"),
"a refusal that clears on its own must say so, or a client reads it as never"
);
// A different session, at the same moment: the cap is a bucket per session
// and not one ceiling for the console.
let theirs = keys.make_session("viewer-vera", Role::Viewer, true);
let other = feed_response(&app, &theirs).await;
assert_eq!(
other.status(),
http::StatusCode::OK,
"one session's tabs refused another session's first feed"
);
drop(other);
// And a place given back is a place usable again.
open.pop();
// The guard is dropped with the response body, which happens on the task
// that owns it; one yield is enough for that to have run.
tokio::task::yield_now().await;
let again = feed_response(&app, &mine).await;
assert_eq!(
again.status(),
http::StatusCode::OK,
"closing a feed did not give its place back"
);
}
/// One `GET /api/events` as the holder of `cookie`, response not consumed.
async fn feed_response(app: &axum::Router, cookie: &str) -> http::Response<axum::body::Body> {
use tower::ServiceExt;
let request = http::Request::get("/api/events")
.header(
http::header::COOKIE,
format!("{}={cookie}", crate::auth::SESSION_COOKIE),
)
.body(axum::body::Body::empty())
.expect("a request");
app.clone()
.oneshot(request)
.await
.expect("the console answers")
}
/// Open the feed as `user`, publish with `emit`, and return what the stream
/// actually sent.
///
/// The subscription is taken when the handler runs, which is before this
/// returns, so items published afterwards are in the channel rather than lost.
/// Reading stops at the first quiet moment: an SSE stream never ends on its
/// own, and the thing being asserted is what did NOT arrive.
async fn feed_lines(
app: &axum::Router,
keys: &crate::auth::Webauthn,
user: &str,
role: crate::role::Role,
emit: impl FnOnce(),
) -> String {
feed_lines_at(app, keys, user, role, "/api/events", "abcdef0123", emit).await
}
/// [`feed_lines`] at any feed address (`/api/events?repo=…`), reading until
/// `until` has been heard or the stream goes quiet.
async fn feed_lines_at(
app: &axum::Router,
keys: &crate::auth::Webauthn,
user: &str,
role: crate::role::Role,
uri: &str,
until: &str,
emit: impl FnOnce(),
) -> String {
use futures_util::StreamExt as _;
use tower::ServiceExt;
let cookie = keys.make_session(user, role, true);
let request = http::Request::get(uri)
.header(
http::header::COOKIE,
format!("{}={cookie}", crate::auth::SESSION_COOKIE),
)
.body(axum::body::Body::empty())
.expect("a request");
let response = app
.clone()
.oneshot(request)
.await
.expect("the console answers");
assert_eq!(
response.status(),
http::StatusCode::OK,
"the feed refused {user}"
);
emit();
let mut body = response.into_body().into_data_stream();
let mut heard = String::new();
// Generous on purpose, and it costs nothing to be: the loop below leaves as
// soon as the last line this session is entitled to has arrived, so the
// wait is only ever paid by a stream that is genuinely missing something.
// At 750ms it was paid by a BUSY MACHINE instead — the viewer's narrowing
// costs one `Repos.List` round trip before the first event can be judged,
// and under load that alone outran the budget about one run in five, with
// the failure reading "shown nothing at all".
while let Ok(Some(Ok(chunk))) =
tokio::time::timeout(std::time::Duration::from_secs(10), body.next()).await
{
heard.push_str(&String::from_utf8_lossy(&chunk));
// Both lines have been published by now, so the moment the last one
// this session is entitled to has arrived there is nothing more to wait
// for.
if heard.contains(until) {
break;
}
}
heard
}
/// ★ **One anonymous GET cannot be turned into an unbounded number of walks of
/// gunnar's history.**
///
/// `blame` is on `is_anonymous_read`, and it folded `Refs.Blame` with one
/// `Admin.VerifyChain` per DISTINCT commit in the file. The bound was the
/// sentence "a file has few distinct commits" — an assumption about other
/// people's repositories, in a comment, on a route that needs no session. The
/// file is the caller's to choose, so the factor was theirs to choose: one
/// cheap GET against the longest-lived file in a public repository, and gunnar
/// walks its history N times, each walk with its own budget.
///
/// Red before the ceiling: this request made 500 `Admin.VerifyChain` calls, one
/// per commit in the fixture, and would have made a million for a file with a
/// million. The second half is the control that keeps the first honest — an
/// ordinary file is still fully verified and not marked truncated, so this is a
/// ceiling rather than a console that stopped proving anything.
#[tokio::test]
async fn an_anonymous_blame_cannot_be_amplified_into_unbounded_backend_calls() {
let fake = FakeGunnar::new();
let (app, _passkeys, _feed) = console(fake.clone(), true).await;
let ceiling = crate::api::blame::MAX_VERIFIED_COMMITS;
let (status, body) = anon(
&app,
&format!("/api/repos/{NS}/{PUBLIC}/blame?path={CROWDED_FILE}"),
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let walks = fake.verifications.load(Ordering::SeqCst);
assert!(
walks <= ceiling,
"one anonymous GET cost gunnar {walks} VerifyChain walks for a file with \
{CROWDED_FILE_COMMITS} distinct commits"
);
assert!(
body.contains("\"truncated\":true"),
"an answer that stopped short must say so: {}",
&body[body.len().saturating_sub(200)..]
);
// …and the count it reports is the number of walks it really made. Not
// pinned to the ceiling: the clock is the other half of the bound, and on a
// loaded machine the five-second budget can be what stops the loop. Either
// way the answer says how much of it was proven.
assert!(walks > 0, "nothing was verified at all");
assert!(
body.contains(&format!("\"verified\":{walks}")),
"the answer must say how much of it was proven: {}",
&body[body.len().saturating_sub(200)..]
);
// Every line is still attributed and served; the ceiling costs the proof,
// not the blame.
assert!(
body.contains("line 0") && body.contains("line 499"),
"{body}"
);
// The control: an ordinary file is verified all the way through.
let before = fake.verifications.load(Ordering::SeqCst);
let (status, body) = anon(
&app,
&format!("/api/repos/{NS}/{PUBLIC}/blame?path=small.rs"),
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert_eq!(fake.verifications.load(Ordering::SeqCst) - before, 3);
assert!(body.contains("\"truncated\":false"), "{body}");
assert!(body.contains("\"signature\":\"proven\""), "{body}");
}
/// ★ **A misconfigured guest key does not turn `/api/repos` into a public index
/// of every private repository on the box.**
///
/// `auth::routes::public_repos` has filtered the login page's list on
/// visibility since the guest identity existed, with a comment saying why: one
/// `==` against a deployment mistake, where somebody points
/// `GUNNAR_UI_GUEST_IDENTITY` at a key that holds a grant — the console's own,
/// copied because it was to hand. gunnar then answers correctly for the
/// principal it was asked about, which is everything.
///
/// `/api/repos` is the same list, on the same credential, to the same visitor,
/// and it carried no such filter. This drives the mistake: the guest channel is
/// signed with the CONSOLE's key, so the fake gunnar hands back the whole
/// catalogue, and the console must still print only what is public.
///
/// Red before: the visitor's `/api/repos` named `payroll`. The fingerprint
/// assertion is what keeps the test honest — it proves gunnar really did answer
/// the wide list, rather than the fixture quietly doing the filtering.
#[tokio::test]
async fn a_guest_key_that_was_given_a_grant_still_lists_only_public_repositories() {
let fake = FakeGunnar::new();
// THE MISTAKE: the guest identity is the console's own admin key.
let (app, _passkeys, _feed) = console_with(fake.clone(), Some(CONSOLE_SEED), None).await;
let (status, body) = anon(&app, "/api/repos").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(
body.contains(PUBLIC),
"the public repository is still listed"
);
assert!(
!body.contains(PRIVATE),
"a misconfigured guest key made /api/repos a public index of private \
repositories: {body}"
);
// And gunnar really was asked as the console, so the row was there to be
// printed and this console chose not to.
assert_eq!(
fake.fingerprints(),
vec![signer(CONSOLE_SEED).fingerprint().to_owned()],
"the fixture did not exercise the misconfiguration at all"
);
// The login page's own list has always had this wall; asserted here beside
// the new one so the two cannot drift apart.
let (status, body) = anon(&app, "/login/repos").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(!body.contains(PRIVATE), "{body}");
}
/// **With no guest identity there is no anonymous surface**, and in particular
/// no fallback to the console's key.
///
/// The fallback is the whole hazard: a console that answered anonymous traffic
/// with its own credential "because the guest key was missing" would fail open
/// on a misconfiguration, which is the failure mode that never gets noticed
/// because everything looks like it is working.
#[tokio::test]
async fn without_a_guest_identity_nothing_is_served_anonymously() {
let fake = FakeGunnar::new();
let (app, _passkeys, _feed) = console(fake.clone(), false).await;
let (status, body) = anon(&app, "/login/repos").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert_eq!(
body.trim(),
"[]",
"with no guest identity the login page must list nothing: {body}"
);
for route in read_routes(NS, PUBLIC) {
let (status, body) = anon(&app, &route).await;
assert_eq!(
status,
http::StatusCode::UNAUTHORIZED,
"{route} was served without a guest identity: {status} {body}"
);
}
assert!(
fake.fingerprints().is_empty(),
"a call reached gunnar for an anonymous request with no guest identity: {:?}",
fake.fingerprints()
);
}
/// **Mutations stay shut.** The read allow-list is method-scoped, so this is a
/// property of one `matches!` — which is exactly why it is worth pinning: the
/// day somebody widens that match, this is what says so.
#[tokio::test]
async fn a_visitor_cannot_write_anything() {
let (app, _passkeys, _feed) = console(FakeGunnar::new(), true).await;
let attempts = [
(http::Method::POST, "/api/repos".to_owned()),
(http::Method::PATCH, format!("/api/repos/{NS}/{PUBLIC}")),
(http::Method::DELETE, format!("/api/repos/{NS}/{PUBLIC}")),
(http::Method::POST, format!("/api/repos/{NS}/{PUBLIC}/gc")),
(
http::Method::POST,
format!("/api/repos/{NS}/{PUBLIC}/issues"),
),
(http::Method::POST, "/api/grants".to_owned()),
(http::Method::POST, "/api/principals".to_owned()),
// **The one mutating surface a VIEWER may reach**, and a visitor is
// not a viewer. `/api/account/**` is floored at `Role::Viewer` so a
// tenant can add their own key (`api::account`), and the floor is
// reached only by a request that HAS a role — a caller with no session
// has none. Listed here so that lowering the floor cannot be mistaken
// for opening the door.
(http::Method::POST, "/api/account/keys".to_owned()),
(
http::Method::DELETE,
"/api/account/keys?fingerprint=SHA256:x".to_owned(),
),
];
for (method, uri) in attempts {
let (status, body) = send(&app, method.clone(), &uri, None).await;
assert_eq!(
status,
http::StatusCode::UNAUTHORIZED,
"{method} {uri} was not refused: {status} {body}"
);
}
}
/// **The operator's surface is still the operator's.** A read route that is not
/// on the anonymous allow-list must refuse a visitor even though it is a `GET`
/// on a repository they can otherwise see.
#[tokio::test]
async fn the_operator_surface_is_not_widened_by_the_anonymous_one() {
let (app, _passkeys, _feed) = console(FakeGunnar::new(), true).await;
let gated = [
"/api/repos/deleted".to_owned(),
"/api/orphans".to_owned(),
"/api/namespaces".to_owned(),
"/api/storage".to_owned(),
// The box's own disks: device paths, disk serials, the ISO hash and
// the kernel command line. An ADMIN read behind a session, and
// therefore doubly not a visitor's.
"/api/machine".to_owned(),
"/api/stats".to_owned(),
"/api/principals".to_owned(),
"/api/keys".to_owned(),
// Reading your own keys still needs a session to be the OWNER of.
"/api/account/keys".to_owned(),
"/api/grants".to_owned(),
"/api/accounts/gates".to_owned(),
// **A namespace's own meters and its own traffic.** Both take `Read`
// on the namespace at gunnar, and this console's key holds it
// server-wide — so the gate that keeps a stranger out of them is this
// list and nothing else.
// `an_anonymous_visitor_cannot_read_a_namespaces_activity` below is the
// same refusal asserted on the BODY rather than on the status.
format!("/api/namespaces/{NS}/usage"),
format!("/api/namespaces/{NS}/activity"),
// On a repository the visitor CAN see: it is the leaf that is gated,
// not the repository.
format!("/api/repos/{NS}/{PUBLIC}/gc"),
format!("/api/repos/{NS}/{PUBLIC}/protection"),
format!("/api/repos/{NS}/{PUBLIC}/gates"),
format!("/api/repos/{NS}/{PUBLIC}/ref-names"),
format!("/api/repos/{NS}/{PUBLIC}/pulls"),
];
for route in gated {
let (status, body) = anon(&app, &route).await;
assert_eq!(
status,
http::StatusCode::UNAUTHORIZED,
"{route} is not an anonymous route but answered {status}: {body}"
);
}
}
/// The console's own document routes: a visitor may reach the repository pages
/// and is still sent to the login page for the operator's dashboard.
///
/// Asserted on the REDIRECT rather than on the body, because with `ui_dir:
/// None` there is no bundle to serve — what is being tested is the middleware's
/// decision, and a 404 from the missing fallback is that decision saying "I
/// would have served you".
#[tokio::test]
async fn the_repository_pages_are_reachable_and_the_dashboard_is_not() {
let (app, passkeys, _feed) = console(FakeGunnar::new(), true).await;
guests_on(&app, &passkeys).await;
for page in ["/repos", &format!("/{NS}/{PUBLIC}"), &format!("/repo/{NS}/{PUBLIC}")] {
let (status, _) = anon(&app, page).await;
assert_ne!(
status,
http::StatusCode::SEE_OTHER,
"{page} redirected a visitor to the login page"
);
assert_ne!(
status,
http::StatusCode::TEMPORARY_REDIRECT,
"{page} redirected a visitor to the login page"
);
assert_ne!(
status,
http::StatusCode::UNAUTHORIZED,
"{page} refused a visitor"
);
}
for page in [
"/",
"/access",
"/storage",
// The machine's disks. An admin page, and a visitor is not even a
// viewer.
"/machine",
"/usage",
"/events",
"/account",
]
// `/` is the PUBLIC HOME in a `public-site` build (TASKS-12 S-1), the
// server-rendered page `anonymous::public_site` holds; never the dashboard.
.into_iter()
.filter(|page| !(crate::PUBLIC_SITE && *page == "/"))
{
let (status, _) = anon(&app, page).await;
assert_eq!(
status,
http::StatusCode::UNAUTHORIZED,
"{page} is an operator page and must not be served to a visitor"
);
}
}
/// ★ **The machine's disks are the administrator's, and an older gunnar is not
/// a failure box.**
///
/// Two properties of one route, together, because they are the two ways the
/// Machine page can be got wrong:
///
/// * a **viewer** is refused with a 403 that names both roles. The box's
/// device paths, disk serials, ISO hash and kernel command line are not a
/// tenant's business, and the gate is `role_floor`'s one admin READ — a
/// client-side `is_admin()` in the console is a courtesy, never a boundary;
/// * an **admin** on a gunnar that does not serve `Admin.StorageInfo` gets a
/// **200** carrying gunnar's own refusal in `unanswered`, not a 501. The
/// page still has three layers to draw and a sentence to draw about the
/// server. `FakeGunnar` answers `Unimplemented` for every Admin verb, so
/// this is exactly that case.
///
/// The machine now rides on `Admin.StorageInfo`'s `reality` field rather than
/// on an `Admin.Machine` RPC that gunnar never served, which means the OTHER
/// "cannot answer" — a gunnar that serves the call and predates field 10 —
/// arrives as `Ok` with the field absent and cannot be produced by refusing a
/// verb. `api::machine`'s own tests cover that half; this one covers the
/// refusal half end to end, through the role gate.
#[tokio::test]
async fn the_machine_page_is_an_admins_and_an_older_gunnar_answers_it_in_words() {
let (app, passkeys, _feed) = console(FakeGunnar::new(), true).await;
let (status, body) = signed_in(
&app,
&passkeys,
"viewer-vera",
crate::role::Role::Viewer,
"/api/machine",
)
.await;
assert_eq!(
status,
http::StatusCode::FORBIDDEN,
"a viewer read the machine's disks: {body}"
);
assert!(
body.contains("admin"),
"the refusal does not name the role it needs: {body}"
);
let (status, body) = signed_in(
&app,
&passkeys,
"alice",
crate::role::Role::Admin,
"/api/machine",
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let answer: serde_json::Value = serde_json::from_str(&body).expect("the machine, as JSON");
assert!(
answer["unanswered"]
.as_str()
.is_some_and(|words| words.contains("Admin.StorageInfo")),
"an older gunnar's refusal did not reach the page in words: {body}"
);
// And nothing was invented to fill the hole.
assert!(answer["record"].is_null(), "{body}");
assert_eq!(
answer["filesystems"].as_array().map(Vec::len),
Some(0),
"{body}"
);
assert!(answer["placement"].is_null(), "{body}");
// A visitor, for completeness: refused before the role is ever consulted.
let (status, _) = anon(&app, "/api/machine").await;
assert_eq!(status, http::StatusCode::UNAUTHORIZED);
}
/// ★ **A visitor cannot read a namespace's ACTIVITY — asserted on the body, not
/// on the status code.**
///
/// `Usage.Activity` answers what a tenant pushed and fetched, day by day, and
/// when they were last seen. It takes `Read` **on the namespace** at gunnar —
/// and this console signs with a key that holds `Read` on every namespace on
/// the box. So gunnar would answer it. The only thing between a stranger and
/// another tenant's traffic is the console's own allow-list, which is exactly
/// the confused-deputy shape this module exists for, and exactly the shape a
/// new route is most likely to be added into by accident: `/usage` was already
/// gated, `/activity` sits beside it, and copying the route registration
/// without thinking about the gate is a one-line mistake.
///
/// Asserted three ways, because a 401 alone proves the least of them:
///
/// * **no digit of the ledger is in the body** — not the byte total, not the
/// day key, not the last-push stamp, and not the `days` array itself;
/// * **no call reached gunnar at all**, so the console did not fetch the
/// answer and then decide to withhold it. A refusal after the read has
/// already happened is a refusal that a logging change can undo;
/// * **an operator still gets the numbers**, so this is not green because the
/// route is broken for everybody. That control is the half that makes the
/// other two mean something: without it, deleting the handler would pass.
#[tokio::test]
async fn an_anonymous_visitor_cannot_read_a_namespaces_activity() {
let fake = FakeGunnar::new();
let (app, _passkeys, _feed) = console(fake.clone(), true).await;
let route = format!("/api/namespaces/{NS}/activity");
let before = fake.calls.load(Ordering::SeqCst);
let (status, body) = anon(&app, &route).await;
assert_eq!(
status,
http::StatusCode::UNAUTHORIZED,
"{route} answered {status} to a visitor: {body}"
);
// The disclosure, looked for as it would be written on the wire.
for secret in [
SECRET_BYTES_MOVED.to_string(),
SECRET_DAY_MS.to_string(),
SECRET_LAST_PUSH_MS.to_string(),
] {
assert!(
!body.contains(&secret),
"the visitor was handed {NS}'s traffic ({secret}): {body}"
);
}
assert!(
!body.contains("days") && !body.contains("retained_days"),
"the visitor was handed a day series: {body}"
);
assert_eq!(
fake.calls.load(Ordering::SeqCst),
before,
"the console asked gunnar for {NS}'s activity on behalf of a visitor and then \
withheld the answer; the read must not happen at all"
);
// The control. An operator asks the same question and gets the numbers —
// so the three assertions above are about the gate and not about a route
// that answers nobody.
let (status, body) = admin(&app, &route).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(
body.contains(&SECRET_BYTES_MOVED.to_string()),
"an operator must still be able to read the namespace's activity: {body}"
);
assert!(
body.contains(&SECRET_LAST_PUSH_MS.to_string()),
"…including when it was last pushed to: {body}"
);
// And the same for the glance on `/usage`, which carries `bytes_in_today`
// and `last_push_unix_ms` — the same facts in a smaller shape, behind the
// same gate, and worth pinning here rather than trusting the neighbouring
// route's test to cover a different module's decision.
let usage = format!("/api/namespaces/{NS}/usage");
let (status, body) = anon(&app, &usage).await;
assert_eq!(status, http::StatusCode::UNAUTHORIZED, "{usage}: {body}");
assert!(
!body.contains(&SECRET_BYTES_MOVED.to_string())
&& !body.contains(&SECRET_LAST_PUSH_MS.to_string()),
"the visitor was handed {NS}'s meters: {body}"
);
// And the admission plan, which carries the same window's byte total.
let admit = format!("/api/namespaces/{NS}/admit");
let before = fake.calls.load(Ordering::SeqCst);
let (status, body) = anon(&app, &admit).await;
assert_eq!(status, http::StatusCode::UNAUTHORIZED, "{admit}: {body}");
assert!(
!body.contains(&SECRET_BYTES_MOVED.to_string()) && !body.contains("used_bytes"),
"the visitor was handed {NS}'s admission plan: {body}"
);
assert_eq!(fake.calls.load(Ordering::SeqCst), before, "{admit} reached gunnar for a visitor");
}
/// **The allow-list, pinned by shape.** A unit test over the predicate itself,
/// because the integration tests above can only ever cover the routes somebody
/// remembered to list.
#[test]
fn the_anonymous_read_predicate_admits_only_named_leaves() {
use crate::auth::middleware::is_anonymous_read;
use http::Method;
let get = Method::GET;
assert!(is_anonymous_read(&get, "/api/repos"));
assert!(is_anonymous_read(&get, "/api/repos/ns/name"));
for leaf in [
"refs", "head", "log", "browse", "diff", "blame", "search", "object", "chain", "readme",
"raw",
] {
assert!(
is_anonymous_read(&get, &format!("/api/repos/ns/name/{leaf}")),
"{leaf} should be readable"
);
}
// The leaves that are the operator's.
for leaf in [
"gc",
"protection",
"gates",
"ref-names",
"pulls",
"undelete",
] {
assert!(
!is_anonymous_read(&get, &format!("/api/repos/ns/name/{leaf}")),
"{leaf} must NOT be anonymous"
);
}
// GUN-GUEST (owner 2026-10-30): a public repository's issues are a guest's to read, the
// list and one issue (gunnar takes Read for both), never a write under them.
assert!(is_anonymous_read(&get, "/api/repos/ns/name/issues"));
assert!(is_anonymous_read(&get, "/api/repos/ns/name/issues/3"));
assert!(!is_anonymous_read(&get, "/api/repos/ns/name/issues/3/comment"));
// `/api/repos/deleted` is the whole server's tombstones, and it has the
// shape of a namespace. A prefix match would have admitted it.
assert!(!is_anonymous_read(&get, "/api/repos/deleted"));
// **A namespace's meters and its traffic are never anonymous.** They have
// the same three-segment shape the repository leaves have — `/api/x/y/z` —
// so a predicate written by counting segments rather than by naming the
// prefix would let them through, and this console's key can read every
// namespace on the box.
assert!(!is_anonymous_read(&get, "/api/namespaces/ns/usage"));
assert!(!is_anonymous_read(&get, "/api/namespaces/ns/activity"));
assert!(!is_anonymous_read(&get, "/api/namespaces/ns/admit"));
// Nothing deeper than one leaf.
assert!(!is_anonymous_read(&get, "/api/repos/ns/name/pulls/1/merge"));
assert!(!is_anonymous_read(&get, "/api/repos/ns/name/log/extra"));
// Empty segments cannot stand in for a repository.
assert!(!is_anonymous_read(&get, "/api/repos//name"));
assert!(!is_anonymous_read(&get, "/api/repos/ns/"));
// Not a read.
for method in [Method::POST, Method::PATCH, Method::PUT, Method::DELETE] {
assert!(!is_anonymous_read(&method, "/api/repos"));
assert!(!is_anonymous_read(&method, "/api/repos/ns/name/browse"));
}
}
/// The document allow-list, same treatment: the repository pages and `/<ns>`
/// (owner, 2026-10-03), and none of the console's own surfaces.
#[test]
fn the_anonymous_page_predicate_admits_only_the_repository_and_namespace_pages() {
use crate::auth::middleware::is_anonymous_page;
use http::Method;
let get = Method::GET;
assert!(is_anonymous_page(&get, "/repos"));
assert!(is_anonymous_page(&get, "/repo/ns/name"));
assert!(is_anonymous_page(&get, "/repo/ns/name/files"));
for page in [
"/",
"/access",
"/storage",
// The machine's disks. An admin page, and a visitor is not even a
// viewer.
"/machine",
"/usage",
"/events",
"/account",
"/admin",
"/overview",
"/account/storage",
"/admin/storage",
"/login",
"/api",
"/health",
"/favicon.ico",
] {
assert!(!is_anonymous_page(&get, page), "{page} must not be public");
}
// `/<ns>`: ONE segment that is not a console route — the page that shows a
// visitor a private namespace's public repositories, and "does not exist"
// for the rest (the data is `/api/namespaces/{ns}/page`, asked as the
// guest). `/repository-settings` is a namespace name by that rule, which is
// harmless: it is the same `index.html`, and the namespace answers 404.
assert!(is_anonymous_page(&get, "/acme"));
assert!(is_anonymous_page(&get, "/repository-settings"));
// GUN-GUEST: `/{ns}/{name}` IS a repository's address now (the same `index.html`, and a
// missing repository answers as missing); a leaf the console has no page for is not one.
assert!(is_anonymous_page(&get, "/repository-settings/x"));
assert!(!is_anonymous_page(&get, "/repository-settings/x/y"));
assert!(!is_anonymous_page(&Method::POST, "/repo/ns/name"));
assert!(!is_anonymous_page(&Method::POST, "/acme"));
}
/// ★ **The CLI page is the console's own and open to a visitor** (E4, 2026-10-11): `/cli`
/// renders what the front serves under `/download/` (the signed `gunnar` CLI), which a
/// person needs before any encrypted flow, logged in or not — and it is not a namespace.
#[test]
fn the_cli_page_is_a_console_route_a_visitor_may_open() {
use crate::auth::middleware::{is_anonymous_page, is_namespace_page};
use http::Method;
assert!(crate::serve::CONSOLE_ROUTES.contains(&"/cli"));
assert!(!is_namespace_page("/cli"));
assert!(is_anonymous_page(&Method::GET, "/cli"));
assert!(!is_anonymous_page(&Method::POST, "/cli"));
assert!(!is_anonymous_page(&Method::GET, "/cli/x"));
}
/// **The README route, both ways round and in both shapes.**
///
/// The public repository's README is rendered for somebody with no session, by
/// the GUEST key, in each of the two grades; the private one's is refused at
/// the same place every other read of it is. All of it in one test because the
/// interesting claim is the pair — "it is served" alone would be green on a
/// console that served everything.
#[tokio::test]
async fn a_public_readme_is_rendered_for_a_visitor_and_a_private_one_is_refused() {
let fake = FakeGunnar::new();
let (app, _passkeys, _feed) = console(fake.clone(), true).await;
// ── the default grade: the typeset page, words and all ───────────────
let (status, body) = anon(&app, &format!("/api/repos/{NS}/{PUBLIC}/readme")).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(
body.contains("\"kind\":\"svg\""),
"the typeset page is the default grade: {}",
&body[..body.len().min(400)]
);
assert!(
body.contains(&format!("{NS}/{PUBLIC} README.md")),
"the page is named after the repository and file it came from"
);
// **The heading text IS in the SVG, and that is what let the typeset grade
// become the default.** It used to be asserted absent — `typst-svg` draws
// every glyph as a path and nothing in the markup spelled the words — and
// that absence was the whole reason this console led with the html grade.
// docbook 0.1.2's text layer puts the characters back over the glyphs, so
// the assertion is inverted rather than deleted.
assert!(
body.contains(README_HEADING),
"the heading must be SELECTABLE TEXT in the typeset page's layer: {}",
&body[..body.len().min(400)]
);
assert!(
body.contains("README.md"),
"the answer names where it was found"
);
assert!(!body.contains("<script"), "{body}");
// ── the plain grade: real elements, a thirtieth of the bytes ─────────
let (status, body) = anon(&app, &format!("/api/repos/{NS}/{PUBLIC}/readme?as=html")).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(
body.contains("\"kind\":\"html\""),
"?as=html must serve the plain grade: {}",
&body[..body.len().min(400)]
);
assert!(
body.contains(README_HEADING),
"the heading must be selectable text in the html grade too: {body}"
);
assert!(!body.contains("<script"), "{body}");
// ── and the private one, in both shapes ──────────────────────────────
for query in ["", "?as=html"] {
let (status, body) = anon(&app, &format!("/api/repos/{NS}/{PRIVATE}/readme{query}")).await;
assert_eq!(status, http::StatusCode::NOT_FOUND, "{query}: {body}");
assert!(!body.contains(README_HEADING), "{body}");
assert!(
!body.contains(std::str::from_utf8(SECRET_BYTES).expect("utf8")),
"{body}"
);
}
// Every one of those calls went out as the guest, not as the console.
let guest = signer(GUEST_SEED).fingerprint().to_owned();
let seen = fake.fingerprints();
assert!(!seen.is_empty(), "no call reached gunnar at all");
assert!(
seen.iter().all(|f| *f == guest),
"the README route used a key other than the guest's: {seen:?}"
);
}
/// **A README's relative link lands on the file, in both shapes.**
///
/// Measured on gunnar.rs 2026-09-11: the gunnar README's link to its own PDF
/// was written for the tree (`docs/…pdf`) and resolved against the console's
/// `/repo/{ns}/{name}` route, a page that is not a directory of anything. Both
/// grades must hand the browser the raw route instead, and the typeset one must
/// carry it in the link layer that sits above the text.
#[tokio::test]
async fn a_readme_link_to_a_file_in_the_tree_points_at_the_raw_route() {
let (app, _passkeys, _feed) = console(FakeGunnar::new(), true).await;
let raw = format!("/api/repos/{NS}/{PUBLIC}/raw?path=docs/guide.pdf");
for query in ["", "?as=html"] {
let (status, body) = anon(&app, &format!("/api/repos/{NS}/{PUBLIC}/readme{query}")).await;
assert_eq!(status, http::StatusCode::OK, "{query}: {body}");
assert!(
body.contains(&raw),
"{query}: the link was not rewritten: {body}"
);
assert!(
!body.contains("href=\\\"docs/guide.pdf"),
"{query}: a link still points at the tree path: {body}"
);
}
let (_, body) = anon(&app, &format!("/api/repos/{NS}/{PUBLIC}/readme")).await;
assert!(
body.contains("class=\\\"link-layer\\\""),
"the typeset page must lift the link above its text layer"
);
let (_, body) = anon(&app, &format!("/api/repos/{NS}/{PUBLIC}/readme?as=html")).await;
assert!(
body.contains("external"),
"the router must not claim a click on a README link: {body}"
);
}
/// **The raw route, both ways round.** A stranger gets a public repository's
/// file as the file — with the type decided by the console, not the file — and
/// the private repository's is refused, by gunnar, against the guest.
#[tokio::test]
async fn a_public_file_is_served_raw_to_a_visitor_and_a_private_one_is_refused() {
use tower::ServiceExt;
let fake = FakeGunnar::new();
let (app, _passkeys, _feed) = console(fake.clone(), true).await;
let get = |uri: String| {
let app = app.clone();
async move {
let request = http::Request::get(uri)
.body(axum::body::Body::empty())
.expect("a request");
let response = app.oneshot(request).await.expect("the console answers");
let (parts, body) = response.into_parts();
let body = axum::body::to_bytes(body, 1 << 20).await.expect("a body");
(parts, body)
}
};
let (parts, body) = get(format!("/api/repos/{NS}/{PUBLIC}/raw?path=README.md")).await;
assert_eq!(
parts.status,
http::StatusCode::OK,
"{}",
String::from_utf8_lossy(&body)
);
assert_eq!(&body[..], README_MARKDOWN.as_bytes());
let header = |name: http::HeaderName| {
parts
.headers
.get(&name)
.and_then(|v| v.to_str().ok())
.unwrap_or_default()
.to_owned()
};
// ★ GUN-LANDING (coordinator 2026-10-30): a Markdown or text file is read as text,
// `text/plain; charset=utf-8`, shown inline — in a sandbox, never sniffed, and under the
// route's own policy, not the console's. (It was a download, application/octet-stream.)
assert_eq!(
header(http::header::CONTENT_TYPE),
"text/plain; charset=utf-8"
);
assert!(header(http::header::CONTENT_DISPOSITION).starts_with("inline;"));
assert!(header(http::header::CONTENT_DISPOSITION).contains("README%2Emd"));
assert_eq!(
header(http::HeaderName::from_static("x-content-type-options")),
"nosniff"
);
let policy = header(http::header::CONTENT_SECURITY_POLICY);
assert!(policy.contains("sandbox"), "{policy}");
assert!(
!policy.contains("script-src"),
"the console's policy overwrote the route's: {policy}"
);
let (parts, body) = get(format!("/api/repos/{NS}/{PUBLIC}/raw?path=")).await;
assert_eq!(
parts.status,
http::StatusCode::BAD_REQUEST,
"a directory is not a file"
);
let _ = body;
let (parts, body) = get(format!("/api/repos/{NS}/{PRIVATE}/raw?path=README.md")).await;
assert_eq!(parts.status, http::StatusCode::NOT_FOUND);
assert!(!body.windows(SECRET_BYTES.len()).any(|w| w == SECRET_BYTES));
let guest = signer(GUEST_SEED).fingerprint().to_owned();
assert!(
fake.fingerprints().iter().all(|f| *f == guest),
"the raw route used a key other than the guest's"
);
}
/// ★ **An SVG picture in a PUBLIC repository is served as a picture** (BADGE-7):
/// `image/svg+xml`, inline, under a policy with no script and `sandbox`, never
/// sniffed — so gunnar.rs's README can draw the OpenSSF badge from its own origin
/// (`img-src 'self'`) and no visitor's browser asks bestpractices.dev for it. It
/// is a picture because the repository is public, not because the reader is a
/// guest: the administrator reading the same public file gets the same picture.
/// The same file in a PRIVATE repository stays a download, read by whoever may
/// read it: it would be a document on the console's origin with that reader's
/// session beside it.
#[tokio::test]
async fn an_svg_in_a_public_repository_is_a_picture_and_in_a_private_one_a_download() {
use tower::ServiceExt;
let fake = FakeGunnar::new().with_a_badge();
let (app, _passkeys, _feed) = console(fake.clone(), true).await;
let get = |uri: String, token: Option<&'static str>| {
let app = app.clone();
async move {
let mut request = http::Request::get(uri);
if let Some(token) = token {
request = request.header(http::header::AUTHORIZATION, format!("Bearer {token}"));
}
let response = app
.oneshot(request.body(axum::body::Body::empty()).expect("a request"))
.await
.expect("the console answers");
let (parts, body) = response.into_parts();
let body = axum::body::to_bytes(body, 1 << 20).await.expect("a body");
(parts, body)
}
};
let h = |parts: &http::response::Parts, name: http::HeaderName| {
parts.headers.get(&name).and_then(|v| v.to_str().ok()).unwrap_or_default().to_owned()
};
for token in [None, Some("break-glass")] {
let (parts, body) = get(format!("/api/repos/{NS}/{PUBLIC}/raw?path=badge.svg"), token).await;
assert_eq!(parts.status, http::StatusCode::OK, "{token:?}: {}", String::from_utf8_lossy(&body));
assert_eq!(&body[..], BADGE_SVG);
assert_eq!(h(&parts, http::header::CONTENT_TYPE), "image/svg+xml", "{token:?}");
assert!(h(&parts, http::header::CONTENT_DISPOSITION).starts_with("inline;"), "{token:?}");
assert_eq!(h(&parts, http::HeaderName::from_static("x-content-type-options")), "nosniff");
let policy = h(&parts, http::header::CONTENT_SECURITY_POLICY);
assert!(policy.contains("sandbox"), "{policy}");
assert!(policy.contains("default-src 'none'"), "{policy}");
assert!(!policy.contains("script-src"), "no script, and not the console's policy: {policy}");
}
// The private repository's identical file, read by the administrator: a download.
let (parts, body) = get(format!("/api/repos/{NS}/{PRIVATE}/raw?path=badge.svg"), Some("break-glass")).await;
assert_eq!(parts.status, http::StatusCode::OK, "{}", String::from_utf8_lossy(&body));
assert_eq!(h(&parts, http::header::CONTENT_TYPE), "application/octet-stream");
assert!(h(&parts, http::header::CONTENT_DISPOSITION).starts_with("attachment;"));
assert!(h(&parts, http::header::CONTENT_SECURITY_POLICY).contains("sandbox"));
// And a stranger does not get it at all.
let (parts, _) = get(format!("/api/repos/{NS}/{PRIVATE}/raw?path=badge.svg"), None).await;
assert_eq!(parts.status, http::StatusCode::NOT_FOUND);
}
/// **A file kept in Git LFS is served as the file, not as its pointer.**
///
/// The tree holds 131 bytes of pointer text for `guide.pdf`, which is the shape
/// gunnar's own `docs/gunnar-fast-setup.pdf` has. Before `Refs.ReadLfsObject`
/// the console had nothing else to serve: the pointer would have gone out as
/// `application/pdf` and the browser would have called the repository's PDF
/// broken. Red before that RPC (the body was the pointer text), green now.
///
/// The private half is the claim that matters: the same pointer, the same
/// sha256, a repository the visitor may not read — and following a pointer must
/// not be a way around that.
#[tokio::test]
async fn a_file_in_git_lfs_is_served_as_the_file_and_not_as_its_pointer() {
use tower::ServiceExt;
let fake = FakeGunnar::new();
let (app, _passkeys, _feed) = console(fake.clone(), true).await;
let get = |uri: String| {
let app = app.clone();
async move {
let request = http::Request::get(uri)
.body(axum::body::Body::empty())
.expect("a request");
let response = app.oneshot(request).await.expect("the console answers");
let (parts, body) = response.into_parts();
let body = axum::body::to_bytes(body, 1 << 20).await.expect("a body");
(parts, body)
}
};
let (parts, body) = get(format!("/api/repos/{NS}/{PUBLIC}/raw?path=guide.pdf")).await;
assert_eq!(
parts.status,
http::StatusCode::OK,
"{}",
String::from_utf8_lossy(&body)
);
assert_eq!(&body[..], LFS_BODY, "the LFS object, whole");
assert!(
!body.starts_with(b"version https://git-lfs"),
"the pointer was served as if it were the file"
);
assert_eq!(
parts
.headers
.get(http::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok()),
Some("application/pdf")
);
// The private repository's identical pointer names an object this visitor
// may not have, and the refusal comes from gunnar, against the guest.
let (parts, body) = get(format!("/api/repos/{NS}/{PRIVATE}/raw?path=guide.pdf")).await;
assert_eq!(parts.status, http::StatusCode::NOT_FOUND);
assert!(
!body.windows(SECRET_BYTES.len()).any(|w| w == SECRET_BYTES),
"the private repository's LFS object reached a stranger"
);
let guest = signer(GUEST_SEED).fingerprint().to_owned();
assert!(
fake.fingerprints().iter().all(|f| *f == guest),
"the LFS read used a key other than the guest's"
);
}
/// **Against an older gunnar, the answer says what is wrong, not which method
/// is missing.**
///
/// The console and the appliance are deployed on their own timetables — that
/// skew is what `tests/contract.rs` is for — so a console that knows
/// `Refs.ReadLfsObject` will meet a gunnar that does not. tonic answers an
/// unknown method `Unimplemented`, which would reach a visitor as a bare 501
/// naming a gRPC method. The file is in LFS, this server cannot serve it, and
/// `git lfs pull` can: that is what the reader needs.
#[tokio::test]
async fn an_older_gunnar_answers_the_lfs_link_with_a_reason_not_a_method_name() {
use tower::ServiceExt;
let (app, _passkeys, _feed) = console(FakeGunnar::new().without_the_lfs_rpc(), true).await;
let request = http::Request::get(format!("/api/repos/{NS}/{PUBLIC}/raw?path=guide.pdf"))
.body(axum::body::Body::empty())
.expect("a request");
let response = app.oneshot(request).await.expect("the console answers");
let (parts, body) = response.into_parts();
let body = axum::body::to_bytes(body, 1 << 20).await.expect("a body");
let body = String::from_utf8_lossy(&body).into_owned();
assert_eq!(parts.status, http::StatusCode::BAD_REQUEST, "{body}");
assert!(body.contains("Git LFS"), "{body}");
assert!(body.contains("git lfs pull"), "{body}");
assert!(body.contains(LFS_OID), "the object is named: {body}");
assert!(
!body.starts_with("version https://git-lfs"),
"the pointer must never be the body"
);
}
/// A body that is not a run of one byte, so a lost or repeated range shows.
fn six_megabytes() -> Vec<u8> {
(0..6_000_000u32)
.map(|i| (i % 251) as u8 ^ (i >> 16) as u8)
.collect()
}
/// `GET …/raw?path=guide.pdf` on the public repository, against `fake`.
async fn raw_guide(fake: FakeGunnar) -> (http::response::Parts, Vec<u8>) {
use tower::ServiceExt;
let (app, _passkeys, _feed) = console(fake, true).await;
let request = http::Request::get(format!("/api/repos/{NS}/{PUBLIC}/raw?path=guide.pdf"))
.body(axum::body::Body::empty())
.expect("a request");
let response = app.oneshot(request).await.expect("the console answers");
let (parts, body) = response.into_parts();
let body = axum::body::to_bytes(body, 64 << 20).await.expect("a body");
(parts, body.to_vec())
}
fn header(parts: &http::response::Parts, name: http::HeaderName) -> Option<&str> {
parts.headers.get(name).and_then(|v| v.to_str().ok())
}
/// ★ **An LFS file larger than one gunnar answer is streamed whole,
/// byte-exact** (the owner's 6 MB README picture, 2026-10-15): range after
/// range of at most 4 MiB, under the file's own type and a `Content-Length` of
/// the whole.
#[tokio::test]
async fn an_lfs_file_past_one_gunnar_answer_is_streamed_whole() {
let body = six_megabytes();
let fake = FakeGunnar::new().with_the_guide_in_lfs(body.clone());
let (parts, got) = raw_guide(fake).await;
let head = String::from_utf8_lossy(&got[..got.len().min(300)]).into_owned();
assert_eq!(parts.status, http::StatusCode::OK, "{head}");
assert_eq!(header(&parts, http::header::CONTENT_TYPE), Some("application/pdf"));
assert_eq!(header(&parts, http::header::CONTENT_LENGTH), Some("6000000"));
assert_eq!(got.len(), body.len());
assert!(got == body, "the streamed file is the stored file, byte for byte");
}
/// ★ **Against a gunnar from before ranged reads, the same file is refused in
/// words, not JSON** — what the browser got as `{"code":"resourceexhausted",…}`
/// on 2026-10-15.
#[tokio::test]
async fn against_an_older_gunnar_a_large_lfs_file_is_refused_in_words() {
let fake = FakeGunnar::new()
.without_lfs_ranges()
.with_the_guide_in_lfs(six_megabytes());
let (parts, body) = raw_guide(fake).await;
let body = String::from_utf8_lossy(&body).into_owned();
assert_eq!(parts.status, http::StatusCode::PAYLOAD_TOO_LARGE, "{body}");
assert_eq!(
header(&parts, http::header::CONTENT_TYPE),
Some("text/plain; charset=utf-8")
);
assert_eq!(
body,
"guide.pdf is 5.7 MB, kept in Git LFS, and this console hands a browser files of up to \
4 MB. Clone the repository and run `git lfs pull` to get it.\n"
);
}
/// ★ **The file view of an LFS file describes the FILE, not its pointer**
/// (owner 2026-10-15, gunnar.rs mikronova/modgunn `docs/idiot_guide.pdf` read
/// "131 B · Plain Text · version https://git-lfs…"): the size is the file's,
/// it says it is in LFS and how to show it, and no line of the pointer is
/// served as the file's content.
#[tokio::test]
async fn the_file_view_of_an_lfs_file_is_the_file_and_not_its_pointer() {
use tower::ServiceExt;
let (app, _passkeys, _feed) = console(FakeGunnar::new(), true).await;
let request = http::Request::get(format!("/api/repos/{NS}/{PUBLIC}/browse?path=guide.pdf"))
.body(axum::body::Body::empty())
.expect("a request");
let response = app.oneshot(request).await.expect("the console answers");
let (parts, body) = response.into_parts();
let body = axum::body::to_bytes(body, 1 << 20).await.expect("a body");
let body = String::from_utf8_lossy(&body).into_owned();
assert_eq!(parts.status, http::StatusCode::OK, "{body}");
let found: serde_json::Value = serde_json::from_str(&body).expect("json");
assert_eq!(found["size"], serde_json::json!(LFS_BODY.len()), "the file's size, not the pointer's: {body}");
assert_eq!(found["lfs"]["shown"], "pdf", "{body}");
assert_eq!(found["lfs"]["sha256"], LFS_OID, "{body}");
assert_eq!(found["lines"], serde_json::json!([]), "the pointer served as lines: {body}");
assert!(found["lfs"]["pointer"].as_str().unwrap_or("").starts_with("version https://git-lfs"), "{body}");
}
/// **The second reader does not pay for the typesetting.**
///
/// The counter is gunnar's, not the cache's: a warm answer makes no
/// `Refs.ReadObject` call at all, because the whole tree walk is behind the
/// cache. That is the property worth pinning — a cache that saved the compile
/// and still walked the tree would spend a credential per page view.
#[tokio::test]
async fn a_second_reader_of_the_same_readme_costs_gunnar_nothing() {
let fake = FakeGunnar::new();
let (app, _passkeys, _feed) = console(fake.clone(), true).await;
let route = format!("/api/repos/{NS}/{PUBLIC}/readme");
let (status, _) = anon(&app, &route).await;
assert_eq!(status, http::StatusCode::OK);
let after_first = fake.fingerprints().len();
let (status, body) = anon(&app, &route).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let after_second = fake.fingerprints().len();
assert!(
body.contains("\"kind\":\"svg\""),
"the warm answer is the same answer"
);
// Get, resolve and read the tree still happen — the oid is the cache key
// and it has to be learned. Reading the BLOB and typesetting it do not.
assert!(
after_second - after_first < after_first,
"the second read cost {} calls against the first read's {after_first}; \
the cache is not being hit",
after_second - after_first
);
}
/// **The last grade, through the router.**
///
/// A README that typesets to more than a browser should be handed comes back as
/// the HTML grade with the reason in `warning`, and NOT as a 500 — the console
/// is not entitled to fall over on somebody else's file. The DEFAULT request is
/// what reaches it, which is what makes the limit matter: `?as=html` has no size
/// limit to cross, because its cost is linear in the text rather than in the
/// glyphs, and a visitor asking for nothing in particular now asks for the
/// typeset page.
///
/// Size is also the only way to reach it at all. docbook writes every run of a
/// README's text as a typst string literal, so no README can produce markup the
/// typesetter refuses.
#[tokio::test]
async fn a_readme_too_large_to_typeset_falls_back_to_the_html_grade() {
let (app, _passkeys, _feed) = console(FakeGunnar::new(), true).await;
let (status, body) = anon(&app, &format!("/api/repos/{NS}/{OVERSIZE}/readme")).await;
assert_eq!(status, http::StatusCode::OK, "a big README is not an error");
assert!(
body.contains("\"kind\":\"html\""),
"the fallback grade must be html: {}",
&body[..body.len().min(400)]
);
assert!(
body.contains("over the"),
"the warning must say why it is not typeset: {}",
&body[..body.len().min(600)]
);
assert!(
body.contains("An Encyclopaedia"),
"the reader still gets the README"
);
assert!(!body.contains("<script"), "the fallback is sanitised");
}
/// **The twin's heartbeat is an operator's read, and restoring from the twin
/// an administrator's act** — through the role gate, end to end.
#[tokio::test]
async fn the_twin_panel_is_an_operators_and_the_restore_an_admins() {
let (app, passkeys, _feed) = console(FakeGunnar::new(), true).await;
let (status, body) = signed_in(&app, &passkeys, "viewer-vera", crate::role::Role::Viewer, "/api/twin").await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "a viewer read the twin's heartbeat: {body}");
The file goes on; the console shows all of it.