crates/gunnar-ui-server/src/thin_layer.rs
//! **The console is a thin layer** (owner, 2026-10-04): every read a person
//! makes is asked of gunnar AS THAT PERSON, and gunnar decides.
//!
//! Tests only, and its own module for the reason [`crate::account_model`] is:
//! what is asserted is a property of the whole path — session → bound
//! principal → the person's own key → gunnar's grant table — and not of one
//! handler.
//!
//! # The failure this is here to catch
//!
//! Until this module the viewer-floor GETs below were answered with the
//! console's OWN key, which holds a server-wide `Admin`. gunnar cannot refuse a
//! caller it cannot see, so a paying customer, an importer or an employee read
//! every principal, key, grant, lockout note, deleted repository, orphan store,
//! storage row and box counter — and another account's private pull requests,
//! issues, reviews and branch rules — by spelling a path. A test that asserted
//! only "the owner sees their pull request" is green on that hole. So every
//! row here asks as a SECOND tenant and asserts two things:
//!
//! 1. the answer is a refusal and carries nothing of the other account's, and
//! 2. **what gunnar heard**: the call reached the fake signed by the second
//! tenant's own key, never by the console's. A console that refused after
//! asking with its own key would pass (1) and fail (2).
//!
//! # Why the fake authorises by fingerprint
//!
//! Each tenant's key is enrolled through `Access.AddKey` (the console's
//! [`crate::tenant::TenantKeys`]); the fake records fingerprint → principal and
//! answers every later call by the principal that signed it, with gunnar's
//! scopes: the identity table, lockouts, the deleted and orphan listings, a
//! whole-box storage report, `Stats`, `Health` and `Capacity` need a
//! server-wide grant; a repository's pull requests, issues, reviews, branch
//! rules and maintenance need Read on that repository.
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use tonic::{Request, Response, Status};
use tower::ServiceExt as _;
use crate::api::mirror_gate::Imports;
use crate::pb;
use crate::role::Role;
const CONSOLE_SEED: [u8; 32] = [61u8; 32];
const GUEST_SEED: [u8; 32] = [62u8; 32];
/// Strings that may only ever reach a body read by the box's administrator or
/// by acme. Distinctive, so a `contains` over a whole body means something.
const SECRET_PR: &str = "SECRET-acme-pull-merger-plans";
const SECRET_ISSUE: &str = "SECRET-acme-issue-layoffs";
const SECRET_RULE: &str = "refs/heads/SECRET-acme-release";
const SECRET_GATE: &str = "SECRET-acme-gate";
const SECRET_PERSON: &str = "SECRET-person";
const SECRET_NOTE: &str = "SECRET-lockout-note";
const SECRET_DELETED: &str = "SECRET-deleted-repo";
const SECRET_ORPHAN: &str = "SECRET-orphan-store";
/// The box's size: no tenant's number.
const SECRET_BOX_TOTAL: u64 = 987_654_321_987;
/// What gunnar says is left of beta's room: not cap − used, on purpose.
const GUNNARS_REMAINING: u64 = 42;
fn signer_of(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")
}
fn fingerprint_of<T>(request: &Request<T>) -> String {
request
.metadata()
.get(crate::signer::CREDENTIAL_METADATA_KEY)
.and_then(|value| value.to_str().ok())
.and_then(|line| line.split_whitespace().nth(1))
.unwrap_or("(unsigned)")
.to_owned()
}
// ───────────────────────────────────────────────────────────────────────────
// The fake
// ───────────────────────────────────────────────────────────────────────────
/// What a call is about, in gunnar's terms.
enum Scope {
Server,
Repo(String, String),
}
#[derive(Clone, Default)]
struct Fake {
/// Every call, as `<Method> by <who>` — `console`, a principal id, or
/// `stranger`.
heard: Arc<Mutex<Vec<String>>>,
/// tenant-key fingerprint → principal.
keys: Arc<Mutex<BTreeMap<String, String>>>,
/// (principal, namespace, repo, level).
#[allow(clippy::type_complexity)]
grants: Arc<Mutex<Vec<(String, String, String, i32)>>>,
/// Every `Repos.List`'s `(query, mirrored_only)`.
searched: Arc<Mutex<Vec<(String, bool)>>>,
}
impl Fake {
fn grant(&self, principal: &str, ns: &str, level: pb::GrantLevel) {
self.grants
.lock()
.expect("grants")
.push((principal.into(), ns.into(), String::new(), level as i32));
}
fn who<T>(&self, request: &Request<T>) -> String {
let fingerprint = fingerprint_of(request);
if fingerprint == signer_of(CONSOLE_SEED).fingerprint() {
return "console".to_owned();
}
self.keys
.lock()
.expect("keys")
.get(&fingerprint)
.cloned()
.unwrap_or_else(|| "stranger".to_owned())
}
/// Note the call and decide it the way gunnar does: the console's key
/// holds a server-wide Admin; a tenant holds what the grant table says.
fn require<T>(&self, request: &Request<T>, method: &str, scope: Scope) -> Result<(), Status> {
let who = self.who(request);
self.heard
.lock()
.expect("heard")
.push(format!("{method} by {who}"));
if who == "console" {
return Ok(());
}
let allowed = self.grants.lock().expect("grants").iter().any(|(p, ns, repo, level)| {
*p == who
&& *level >= pb::GrantLevel::Read as i32
&& match &scope {
Scope::Server => ns.is_empty(),
Scope::Repo(n, r) => ns.is_empty() || (ns == n && (repo.is_empty() || repo == r)),
}
});
if allowed {
Ok(())
} else {
Err(Status::permission_denied(format!(
"{who} may not {method}"
)))
}
}
fn take_heard(&self) -> Vec<String> {
std::mem::take(&mut *self.heard.lock().expect("heard"))
}
}
fn repo_scope(path: Option<pb::RepoPath>) -> Scope {
let path = path.unwrap_or_default();
Scope::Repo(path.namespace, path.repo)
}
fn repo_scope_of(slash: &str) -> Scope {
let (ns, repo) = slash.split_once('/').unwrap_or((slash, ""));
Scope::Repo(ns.to_owned(), repo.to_owned())
}
#[tonic::async_trait]
impl pb::access_server::Access for Fake {
async fn who_am_i(
&self,
request: Request<pb::WhoAmIRequest>,
) -> Result<Response<pb::WhoAmIResponse>, Status> {
let who = self.who(&request);
self.heard.lock().expect("heard").push(format!("Access.WhoAmI by {who}"));
let server_level = self
.grants
.lock()
.expect("grants")
.iter()
.filter(|(p, ns, _, _)| *p == who && ns.is_empty())
.map(|(_, _, _, level)| *level)
.max()
.unwrap_or(pb::GrantLevel::Unspecified as i32);
Ok(Response::new(pb::WhoAmIResponse {
principal: Some(pb::Principal {
id: who.clone(),
name: who,
kind: pb::PrincipalKind::User as i32,
created_at_unix_ms: 1,
}),
fingerprint: fingerprint_of(&request),
server_level,
..Default::default()
}))
}
async fn add_key(
&self,
request: Request<pb::AddKeyRequest>,
) -> Result<Response<pb::AddKeyResponse>, Status> {
if self.who(&request) != "console" {
return Err(Status::permission_denied("only the console enrols here"));
}
let asked = request.into_inner();
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("keys")
.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: None,
}),
}))
}
async fn list_principals(
&self,
request: Request<pb::ListPrincipalsRequest>,
) -> Result<Response<pb::ListPrincipalsResponse>, Status> {
self.require(&request, "Access.ListPrincipals", Scope::Server)?;
Ok(Response::new(pb::ListPrincipalsResponse {
principals: ["acme", "beta", SECRET_PERSON]
.into_iter()
.map(|id| pb::Principal {
id: id.to_owned(),
name: id.to_owned(),
kind: pb::PrincipalKind::User as i32,
created_at_unix_ms: 1,
})
.collect(),
next_page_token: String::new(),
..Default::default()
}))
}
async fn list_keys(
&self,
request: Request<pb::ListKeysRequest>,
) -> Result<Response<pb::ListKeysResponse>, Status> {
self.require(&request, "Access.ListKeys", Scope::Server)?;
Ok(Response::new(pb::ListKeysResponse {
keys: vec![pb::PublicKey {
fingerprint: "SHA256:secret".to_owned(),
algo: "ssh-ed25519".to_owned(),
principal_id: SECRET_PERSON.to_owned(),
..Default::default()
}],
}))
}
async fn list_grants(
&self,
request: Request<pb::ListGrantsRequest>,
) -> Result<Response<pb::ListGrantsResponse>, Status> {
self.require(&request, "Access.ListGrants", Scope::Server)?;
Ok(Response::new(pb::ListGrantsResponse {
grants: vec![pb::Grant {
principal_id: SECRET_PERSON.to_owned(),
namespace: "acme".to_owned(),
repo: String::new(),
level: pb::GrantLevel::Admin as i32,
granted_at_unix_ms: 1,
}],
}))
}
}
#[tonic::async_trait]
impl pb::accounts_server::Accounts for Fake {
async fn list_account_gates(
&self,
request: Request<pb::ListAccountGatesRequest>,
) -> Result<Response<pb::ListAccountGatesResponse>, Status> {
self.require(&request, "Accounts.ListAccountGates", Scope::Server)?;
Ok(Response::new(pb::ListAccountGatesResponse {
gates: vec![pb::AccountGate {
account: SECRET_PERSON.to_owned(),
reason: pb::GateReason::Administrative as i32,
note: SECRET_NOTE.to_owned(),
..Default::default()
}],
}))
}
async fn purge_status(
&self,
request: Request<pb::PurgeAccountStatusRequest>,
) -> Result<Response<pb::PurgeAccountStatusResponse>, Status> {
self.require(&request, "Accounts.PurgeStatus", Scope::Server)?;
Ok(Response::new(pb::PurgeAccountStatusResponse {
job: None,
gate: Some(pb::AccountGate {
account: SECRET_PERSON.to_owned(),
reason: pb::GateReason::Administrative as i32,
note: SECRET_NOTE.to_owned(),
..Default::default()
}),
}))
}
}
#[tonic::async_trait]
impl pb::admin_server::Admin for Fake {
async fn health(
&self,
request: Request<pb::HealthRequest>,
) -> Result<Response<pb::HealthResponse>, Status> {
self.require(&request, "Admin.Health", Scope::Server)?;
Ok(Response::new(pb::HealthResponse {
serving: true,
agent: format!("gunnar/{SECRET_PERSON}"),
..Default::default()
}))
}
async fn stats(
&self,
request: Request<pb::StatsRequest>,
) -> Result<Response<pb::StatsResponse>, Status> {
self.require(&request, "Admin.Stats", Scope::Server)?;
Ok(Response::new(pb::StatsResponse {
principals: SECRET_BOX_TOTAL,
..Default::default()
}))
}
async fn storage_info(
&self,
request: Request<pb::StorageInfoRequest>,
) -> Result<Response<pb::StorageInfoResponse>, Status> {
let scope = match request.get_ref().repo.clone() {
Some(path) => Scope::Repo(path.namespace, path.repo),
None => Scope::Server,
};
self.require(&request, "Admin.StorageInfo", scope)?;
Ok(Response::new(pb::StorageInfoResponse {
total_hot_bytes: SECRET_BOX_TOTAL,
repos: vec![pb::RepoStorage {
repo: Some(pb::RepoPath {
namespace: "acme".to_owned(),
repo: SECRET_DELETED.to_owned(),
}),
hot_bytes: 7,
..Default::default()
}],
..Default::default()
}))
}
async fn capacity(
&self,
request: Request<pb::CapacityRequest>,
) -> Result<Response<pb::CapacityResponse>, Status> {
self.require(&request, "Admin.Capacity", Scope::Server)?;
Ok(Response::new(pb::CapacityResponse {
total_bytes: SECRET_BOX_TOTAL,
free_bytes: SECRET_BOX_TOTAL / 2,
servable_bytes: SECRET_BOX_TOTAL / 2,
..Default::default()
}))
}
}
#[tonic::async_trait]
impl pb::repos_server::Repos for Fake {
// 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 asked = request.get_ref().clone();
self.searched
.lock()
.expect("searched")
.push((asked.query.clone(), asked.mirrored_only));
let who = self.who(&request);
// gunnar's query and narrowing, applied by gunnar.
let repos = (0..300)
.map(|n| format!("repo-{n:03}"))
.chain(["the-needle".to_owned()])
.filter(|name| asked.query.is_empty() || name.contains(&asked.query))
.map(|name| pb::Repo {
path: Some(pb::RepoPath {
namespace: who.clone(),
repo: name,
}),
visibility: pb::Visibility::Private as i32,
..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 scope = repo_scope(request.get_ref().path.clone());
self.require(&request, "Repos.Get", scope)?;
Ok(Response::new(pb::GetRepoResponse {
repo: Some(pb::Repo {
path: request.into_inner().path,
hash_kind: pb::HashKind::Sha256 as i32,
..Default::default()
}),
}))
}
async fn list_namespaces(
&self,
request: Request<pb::ListNamespacesRequest>,
) -> Result<Response<pb::ListNamespacesResponse>, Status> {
let _ = self.who(&request);
Ok(Response::new(pb::ListNamespacesResponse {
namespaces: vec![
pb::Namespace {
name: "beta".to_owned(),
owner_kind: pb::PrincipalKind::User as i32,
..Default::default()
},
pb::Namespace {
name: "gunnar-twin".to_owned(),
owner_kind: pb::PrincipalKind::Machine as i32,
public: true,
..Default::default()
},
],
next_page_token: String::new(),
}))
}
async fn list_deleted(
&self,
request: Request<pb::ListDeletedRequest>,
) -> Result<Response<pb::ListDeletedResponse>, Status> {
self.require(&request, "Repos.ListDeleted", Scope::Server)?;
Ok(Response::new(pb::ListDeletedResponse {
repos: vec![pb::DeletedRepo {
repo: Some(pb::RepoPath {
namespace: "acme".to_owned(),
repo: SECRET_DELETED.to_owned(),
}),
principal_id: SECRET_PERSON.to_owned(),
bytes: 11,
..Default::default()
}],
total_bytes: 11,
default_retention_days: 30,
}))
}
async fn list_orphans(
&self,
request: Request<pb::ListOrphansRequest>,
) -> Result<Response<pb::ListOrphansResponse>, Status> {
self.require(&request, "Repos.ListOrphans", Scope::Server)?;
Ok(Response::new(pb::ListOrphansResponse {
orphans: vec![pb::OrphanStore {
repo: Some(pb::RepoPath {
namespace: "acme".to_owned(),
repo: SECRET_ORPHAN.to_owned(),
}),
bytes: 13,
..Default::default()
}],
total_bytes: 13,
}))
}
async fn gc_status(
&self,
request: Request<pb::GcStatusRequest>,
) -> Result<Response<pb::GcStatusResponse>, Status> {
let scope = repo_scope(request.get_ref().path.clone());
self.require(&request, "Repos.GcStatus", scope)?;
Ok(Response::new(pb::GcStatusResponse { job: None }))
}
}
#[tonic::async_trait]
impl pb::pull_requests_server::PullRequests for Fake {
async fn list(
&self,
request: Request<pb::ListPullRequestsRequest>,
) -> Result<Response<pb::ListPullRequestsResponse>, Status> {
let scope = repo_scope(request.get_ref().repo.clone());
self.require(&request, "PullRequests.List", scope)?;
Ok(Response::new(pb::ListPullRequestsResponse {
pulls: vec![secret_pull()],
}))
}
async fn get(
&self,
request: Request<pb::GetPullRequestRequest>,
) -> Result<Response<pb::GetPullRequestResponse>, Status> {
let scope = repo_scope(request.get_ref().repo.clone());
self.require(&request, "PullRequests.Get", scope)?;
Ok(Response::new(pb::GetPullRequestResponse {
pull: Some(secret_pull()),
}))
}
}
fn secret_pull() -> pb::PullRequest {
pb::PullRequest {
repo: Some(pb::RepoPath {
namespace: "acme".to_owned(),
repo: "app".to_owned(),
}),
number: 1,
title: SECRET_PR.to_owned(),
source_ref: "refs/heads/topic".to_owned(),
target_ref: "refs/heads/main".to_owned(),
state: pb::PullRequestState::Open as i32,
author: "acme".to_owned(),
..Default::default()
}
}
#[tonic::async_trait]
impl pb::refs_server::Refs for Fake {
async fn diff(
&self,
request: Request<pb::DiffRequest>,
) -> Result<Response<pb::DiffResponse>, Status> {
let scope = repo_scope(request.get_ref().repo.clone());
self.require(&request, "Refs.Diff", scope)?;
// gunnar's structure and counts: 70 files, more than the console draws
// hunks for, every one counted.
let changes = (0..70)
.map(|n| pb::FileChange {
kind: pb::ChangeKind::Added as i32,
path: format!("f{n:02}.txt"),
new_oid: format!("{n:064x}"),
insertions: 2,
..Default::default()
})
.collect();
Ok(Response::new(pb::DiffResponse { changes }))
}
async fn read_object(
&self,
request: Request<pb::ReadObjectRequest>,
) -> Result<Response<pb::ReadObjectResponse>, Status> {
let scope = repo_scope(request.get_ref().repo.clone());
self.require(&request, "Refs.ReadObject", scope)?;
Ok(Response::new(pb::ReadObjectResponse {
kind: pb::ObjectKind::Blob as i32,
data: b"one\ntwo\n".to_vec(),
..Default::default()
}))
}
async fn list(
&self,
request: Request<pb::ListRefsRequest>,
) -> Result<Response<pb::ListRefsResponse>, Status> {
let scope = repo_scope(request.get_ref().repo.clone());
self.require(&request, "Refs.List", scope)?;
Ok(Response::new(pb::ListRefsResponse::default()))
}
}
#[tonic::async_trait]
impl pb::forge_server::Forge for Fake {
async fn append(
&self,
request: Request<pb::AppendEventRequest>,
) -> Result<Response<pb::AppendEventResponse>, Status> {
let repo = request
.get_ref()
.event
.as_ref()
.and_then(|e| e.event.as_ref())
.map(|e| e.repo.clone())
.unwrap_or_default();
let actor = request
.get_ref()
.event
.as_ref()
.and_then(|e| e.event.as_ref())
.map(|e| e.actor.clone())
.unwrap_or_default();
self.require(&request, "Forge.Append", repo_scope_of(&repo))?;
self.heard
.lock()
.expect("heard")
.push(format!("event actor {actor}"));
Ok(Response::new(pb::AppendEventResponse::default()))
}
async fn get_review(
&self,
request: Request<pb::GetReviewRequest>,
) -> Result<Response<pb::GetReviewResponse>, Status> {
let scope = repo_scope_of(&request.get_ref().repo);
self.require(&request, "Forge.GetReview", scope)?;
Ok(Response::new(pb::GetReviewResponse {
state: Some(pb::ReviewState {
repo: "acme/app".to_owned(),
pull: 1,
..Default::default()
}),
events: Vec::new(),
}))
}
async fn list_issues(
&self,
request: Request<pb::ListIssuesRequest>,
) -> Result<Response<pb::ListIssuesResponse>, Status> {
let scope = repo_scope_of(&request.get_ref().repo);
self.require(&request, "Forge.ListIssues", scope)?;
Ok(Response::new(pb::ListIssuesResponse {
issues: vec![secret_issue()],
}))
}
async fn get_issue(
&self,
request: Request<pb::GetIssueRequest>,
) -> Result<Response<pb::GetIssueResponse>, Status> {
let scope = repo_scope_of(&request.get_ref().repo);
self.require(&request, "Forge.GetIssue", scope)?;
Ok(Response::new(pb::GetIssueResponse {
state: Some(secret_issue()),
events: Vec::new(),
}))
}
}
fn secret_issue() -> pb::IssueState {
pb::IssueState {
repo: "acme/app".to_owned(),
number: 1,
title: SECRET_ISSUE.to_owned(),
author: "acme".to_owned(),
open: true,
..Default::default()
}
}
#[tonic::async_trait]
impl pb::policy_server::Policy for Fake {
async fn get_protection(
&self,
request: Request<pb::GetProtectionRequest>,
) -> Result<Response<pb::GetProtectionResponse>, Status> {
let scope = repo_scope(request.get_ref().repo.clone());
self.require(&request, "Policy.GetProtection", scope)?;
Ok(Response::new(pb::GetProtectionResponse {
protections: vec![pb::BranchProtection {
ref_pattern: SECRET_RULE.to_owned(),
..Default::default()
}],
}))
}
async fn get_ref_name_policy(
&self,
request: Request<pb::GetRefNamePolicyRequest>,
) -> Result<Response<pb::GetRefNamePolicyResponse>, Status> {
let scope = repo_scope(request.get_ref().repo.clone());
self.require(&request, "Policy.GetRefNamePolicy", scope)?;
Ok(Response::new(pb::GetRefNamePolicyResponse {
policy: Some(pb::RefNamePolicy {
allow_patterns: vec![SECRET_RULE.to_owned()],
deny_patterns: Vec::new(),
}),
}))
}
async fn list_gates(
&self,
request: Request<pb::ListGatesRequest>,
) -> Result<Response<pb::ListGatesResponse>, Status> {
let scope = repo_scope(request.get_ref().repo.clone());
self.require(&request, "Policy.ListGates", scope)?;
Ok(Response::new(pb::ListGatesResponse {
gates: vec![pb::GateInfo {
name: SECRET_GATE.to_owned(),
description: String::new(),
enabled: true,
}],
}))
}
}
#[tonic::async_trait]
impl pb::usage_server::Usage for Fake {
async fn read(
&self,
request: Request<pb::ReadUsageRequest>,
) -> Result<Response<pb::ReadUsageResponse>, Status> {
let namespace = request.get_ref().namespace.clone();
self.require(&request, "Usage.Read", Scope::Repo(namespace.clone(), String::new()))?;
Ok(Response::new(pb::ReadUsageResponse {
usage: Some(pb::NamespaceUsage {
namespace: namespace.clone(),
pack_bytes: 5,
..Default::default()
}),
allowance: Some(pb::Allowance {
pack_bytes: Some(100),
..Default::default()
}),
// gunnar's own `remaining` — deliberately NOT 100 − 5, so a console
// that computed its own would be caught.
remaining: Some(pb::Remaining {
pack_bytes: Some(GUNNARS_REMAINING),
..Default::default()
}),
repos: vec![pb::RepoStored {
repo: Some(pb::RepoPath {
namespace,
repo: "only-in-usage-read".to_owned(),
}),
stored_bytes: 5,
..Default::default()
}],
}))
}
}
// ───────────────────────────────────────────────────────────────────────────
// The console
// ───────────────────────────────────────────────────────────────────────────
struct Rig {
router: axum::Router,
webauthn: Arc<crate::auth::Webauthn>,
fake: Fake,
state: crate::app::AppState,
}
impl Rig {
/// A passkey session for `user` at `role`, bound to the principal of the
/// same name.
fn session(&self, user: &str, role: Role) -> String {
self.webauthn
.plant_bound_credential_for_tests(user, role, user);
self.webauthn.make_session(user, role, true)
}
async fn post(&self, uri: &str, cookie: &str, body: serde_json::Value) -> (http::StatusCode, String) {
let request = http::Request::builder()
.method("POST")
.uri(uri)
.header(http::header::CONTENT_TYPE, "application/json")
.header(http::header::ORIGIN, "https://console.example")
.header(
http::header::COOKIE,
format!("{}={cookie}", crate::auth::SESSION_COOKIE),
)
.body(axum::body::Body::from(body.to_string()))
.expect("a request");
let response = self
.router
.clone()
.oneshot(request)
.await
.expect("the console answers");
let status = response.status();
let bytes = axum::body::to_bytes(response.into_body(), 4 << 20)
.await
.expect("a body");
(status, String::from_utf8_lossy(&bytes).into_owned())
}
async fn get(&self, uri: &str, cookie: &str) -> (http::StatusCode, String) {
let request = http::Request::builder()
.method("GET")
.uri(uri)
.header(http::header::ORIGIN, "https://console.example")
.header(
http::header::COOKIE,
format!("{}={cookie}", crate::auth::SESSION_COOKIE),
)
.body(axum::body::Body::empty())
.expect("a request");
let response = self
.router
.clone()
.oneshot(request)
.await
.expect("the console answers");
let status = response.status();
let bytes = axum::body::to_bytes(response.into_body(), 4 << 20)
.await
.expect("a body");
(status, String::from_utf8_lossy(&bytes).into_owned())
}
}
async fn console() -> Rig {
let fake = Fake::default();
// acme owns its namespace; beta owns its own and nothing of acme's.
fake.grant("acme", "acme", pb::GrantLevel::Admin);
fake.grant("beta", "beta", pb::GrantLevel::Admin);
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(pb::access_server::AccessServer::new(serving.clone()))
.add_service(pb::accounts_server::AccountsServer::new(serving.clone()))
.add_service(pb::admin_server::AdminServer::new(serving.clone()))
.add_service(pb::repos_server::ReposServer::new(serving.clone()))
.add_service(pb::pull_requests_server::PullRequestsServer::new(serving.clone()))
.add_service(pb::refs_server::RefsServer::new(serving.clone()))
.add_service(pb::forge_server::ForgeServer::new(serving.clone()))
.add_service(pb::policy_server::PolicyServer::new(serving.clone()))
.add_service(pb::usage_server::UsageServer::new(serving))
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
.await
});
let dir = tempfile::tempdir().expect("a temp dir").keep();
let store = crate::state::StateStore::open(dir.join("state.redb")).expect("a store");
let webauthn = Arc::new(
crate::auth::Webauthn::new(
crate::auth::RelyingParty {
rp_id: "console.example".to_owned(),
origin: "https://console.example".to_owned(),
rp_name: "gunnar".to_owned(),
},
store.clone(),
)
.expect("a subsystem"),
);
let endpoint = format!("http://{addr}");
let state: crate::app::AppState = Arc::new(crate::app::App {
webauthn: Some(webauthn.clone()),
admin_token: Some("break-glass".to_owned()),
#[cfg(feature = "metered")]
monetize: None,
events: crate::sse::EventHub::detached(),
gunnar: crate::grpc::Gunnar::connect_lazy(&endpoint, Arc::new(signer_of(CONSOLE_SEED)))
.expect("a handle"),
twin: None,
guest: Some(
crate::grpc::Gunnar::connect_lazy(&endpoint, Arc::new(signer_of(GUEST_SEED)))
.expect("a guest handle"),
),
reviewers: Arc::new(crate::reviewer::ReviewerKeys::new(
endpoint.clone(),
store.clone(),
)),
tenants: Arc::new(crate::tenant::TenantKeys::new(endpoint, store)),
forges: Default::default(),
imports: Arc::new(Imports::default()),
forge_apps: Arc::default(),
ui_dir: None,
caches: crate::api::Caches::default(),
objects: Default::default(),
readmes: Default::default(),
});
let router = crate::serve::router(state.clone());
Rig {
router,
webauthn,
fake,
state,
}
}
/// The secrets a body for `beta` must not carry.
fn leaks(body: &str) -> Vec<&'static str> {
[
SECRET_PR,
SECRET_ISSUE,
SECRET_RULE,
SECRET_GATE,
SECRET_PERSON,
SECRET_NOTE,
SECRET_DELETED,
SECRET_ORPHAN,
]
.into_iter()
.filter(|s| body.contains(s))
.chain(body.contains(&SECRET_BOX_TOTAL.to_string()).then_some("the box total"))
.collect()
}
/// Every call gunnar heard for one request was signed by `who`, and at least
/// one was made — a refusal the console raised on its own is not "gunnar
/// decided".
fn all_by(heard: &[String], who: &str, uri: &str) {
assert!(!heard.is_empty(), "{uri}: gunnar was never asked");
for line in heard {
assert!(
line.ends_with(&format!(" by {who}")),
"{uri}: gunnar heard {line:?}, not a call signed by {who} — every call was {heard:?}"
);
}
}
// ───────────────────────────────────────────────────────────────────────────
// The rows
// ───────────────────────────────────────────────────────────────────────────
/// The reads of ONE repository: its pull requests, the review and inline
/// comments, its issues, its branch rules, ref-name policy, gates and
/// maintenance.
const REPO_READS: &[&str] = &[
"/api/repos/acme/app/pulls",
"/api/repos/acme/app/pulls/1",
"/api/repos/acme/app/pulls/1/review",
"/api/repos/acme/app/pulls/1/comments",
"/api/repos/acme/app/issues",
"/api/repos/acme/app/issues/1",
"/api/repos/acme/app/protection",
"/api/repos/acme/app/ref-names",
"/api/repos/acme/app/gates",
"/api/repos/acme/app/gc",
];
/// ★ **A second tenant reads nothing of another account's repository, and it
/// is GUNNAR that says no — on the second tenant's own key.** Red before this
/// module: each of these was a `State`-only handler signed with the console's
/// server-wide admin key, so beta read acme's private pull requests, issues
/// and branch rules by spelling the path.
#[tokio::test]
async fn a_second_tenant_is_refused_another_accounts_repository_by_gunnar_on_their_own_key() {
let rig = console().await;
let beta = rig.session("beta", Role::Viewer);
for uri in REPO_READS {
rig.fake.take_heard();
let (status, body) = rig.get(uri, &beta).await;
assert!(
matches!(status, http::StatusCode::FORBIDDEN | http::StatusCode::NOT_FOUND),
"{uri}: beta was answered {status}: {body}"
);
assert!(leaks(&body).is_empty(), "{uri}: {:?} reached beta: {body}", leaks(&body));
all_by(&rig.fake.take_heard(), "beta", uri);
}
}
/// The same reads as the OWNER: answered, on acme's own key — so the rows
/// above are gunnar's grant table and not a wall the console put up.
#[tokio::test]
async fn the_owner_reads_their_own_repository_on_their_own_key() {
let rig = console().await;
let acme = rig.session("acme", Role::Viewer);
for uri in REPO_READS {
rig.fake.take_heard();
let (status, body) = rig.get(uri, &acme).await;
assert_eq!(status, http::StatusCode::OK, "{uri}: {body}");
all_by(&rig.fake.take_heard(), "acme", uri);
}
let (_, body) = rig.get("/api/repos/acme/app/pulls", &acme).await;
assert!(body.contains(SECRET_PR), "{body}");
let (_, body) = rig.get("/api/repos/acme/app/issues", &acme).await;
assert!(body.contains(SECRET_ISSUE), "{body}");
}
/// The box-wide reads: the identity table, lockouts, an account's purge, the
/// deleted and orphan listings, the whole-box storage report, the counters and
/// gunnar's health.
const BOX_READS: &[&str] = &[
"/api/principals",
"/api/keys",
"/api/grants",
"/api/accounts/gates",
"/api/accounts/SECRET-person/purge",
"/api/repos/deleted",
"/api/orphans",
"/api/storage",
"/api/stats",
"/api/health",
];
/// ★ **A tenant is refused the box's identity table and its administrator's
/// reads — by gunnar, on the tenant's key.** Red before this module: every one
/// was answered with the console's key to any viewer, so a paying stranger read
/// every principal, key, grant, lockout note, deleted repository and orphan
/// store on the box. The administrator still reads them, and a tenant asking
/// AFTER the administrator is not handed the administrator's cached answer.
#[tokio::test]
async fn a_tenant_is_refused_the_boxs_identity_table_and_admin_reads_by_gunnar() {
let rig = console().await;
let beta = rig.session("beta", Role::Viewer);
// The administrator: a server-wide Admin grant at gunnar, reading as
// themselves (console roles are grants, thin layer 2026-10-04).
rig.fake.grant("boss", "", pb::GrantLevel::Admin);
let boss = rig.session("boss", Role::Admin);
for round in ["before the administrator", "after the administrator"] {
for uri in BOX_READS {
rig.fake.take_heard();
let (status, body) = rig.get(uri, &beta).await;
assert!(
matches!(status, http::StatusCode::FORBIDDEN | http::StatusCode::NOT_FOUND),
"{uri} ({round}): beta was answered {status}: {body}"
);
assert!(leaks(&body).is_empty(), "{uri} ({round}): {:?} reached beta: {body}", leaks(&body));
all_by(&rig.fake.take_heard(), "beta", uri);
}
if round == "before the administrator" {
for uri in BOX_READS {
let (status, body) = rig.get(uri, &boss).await;
assert_eq!(status, http::StatusCode::OK, "{uri}: the administrator: {body}");
}
}
}
}
/// ★ **A tenant's usage carries no box.** The namespace's own meters are
/// theirs; the appliance's capacity is not, and gunnar refuses it on the
/// tenant's key. Red before this module: `Admin.Capacity` was read with the
/// console's key and every customer saw the machine's total, free and
/// servable bytes beside their own.
#[tokio::test]
async fn a_tenants_usage_carries_their_meters_and_not_the_box() {
let rig = console().await;
let beta = rig.session("beta", Role::Viewer);
rig.fake.take_heard();
let (status, body) = rig.get("/api/namespaces/beta/usage", &beta).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let json: serde_json::Value = serde_json::from_str(&body).expect("json");
assert_eq!(json["pack_bytes"], 5, "{body}");
assert!(
json.get("box").is_some_and(serde_json::Value::is_null),
"the box reached a tenant: {body}"
);
assert!(
!json["box_unreadable"].as_str().unwrap_or_default().is_empty(),
"the page is told why there is no box: {body}"
);
assert!(leaks(&body).is_empty(), "{:?}: {body}", leaks(&body));
all_by(&rig.fake.take_heard(), "beta", "/api/namespaces/beta/usage");
// …and another account's meters are refused, by gunnar.
let (status, body) = rig.get("/api/namespaces/acme/usage", &beta).await;
assert!(
matches!(status, http::StatusCode::FORBIDDEN | http::StatusCode::NOT_FOUND),
"{body}"
);
}
// ── gunnar's answers, taken as given (thin layer C-group, 2026-10-04) ──────
/// ★ **"My room" and the usage page are gunnar's answer, asked as the
/// person**: the per-repository table is `Usage.Read`'s own breakdown and
/// "free"/`remaining` is gunnar's `remaining` — not cap − used computed here,
/// and never a whole-box `Admin.StorageInfo` read with the console's key.
#[tokio::test]
async fn my_room_and_my_remaining_are_gunnars_answer_asked_as_me() {
let rig = console().await;
let beta = rig.session("beta", Role::Viewer);
rig.fake.take_heard();
let (status, body) = rig.get("/api/account/room", &beta).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let room: serde_json::Value = serde_json::from_str(&body).expect("json");
assert_eq!(room["free_bytes"], GUNNARS_REMAINING, "free is not gunnar's remaining: {body}");
assert_eq!(room["repos"][0]["repo"], "only-in-usage-read", "{body}");
let heard = rig.fake.take_heard();
assert!(
!heard.iter().any(|h| h.starts_with("Admin.StorageInfo")),
"My room read the box's storage report: {heard:?}"
);
all_by(&heard, "beta", "/api/account/room");
let (status, body) = rig.get("/api/namespaces/beta/usage", &beta).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let usage: serde_json::Value = serde_json::from_str(&body).expect("json");
assert_eq!(usage["remaining"]["pack_bytes"], GUNNARS_REMAINING, "{body}");
}
/// ★ **A search finds the 301st repository: gunnar applies it before
/// paging**, and "Imported only" is gunnar's `mirrored_only`. Red before: the
/// console asked for the whole catalogue and the browser filtered the first
/// 250 rows it was handed.
#[tokio::test]
async fn a_search_is_gunnars_and_finds_what_is_past_the_first_page() {
let rig = console().await;
let beta = rig.session("beta", Role::Viewer);
let (status, body) = rig.get("/api/repos?q=needle&imported=true", &beta).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(body.contains("the-needle"), "{body}");
assert!(
rig.fake
.searched
.lock()
.expect("searched")
.contains(&("needle".to_owned(), true)),
"the query was not handed to gunnar: {:?}",
rig.fake.searched.lock().expect("searched")
);
}
/// ★ **Whose account a namespace is comes from gunnar** (`owner_kind`), so
/// the page can tell the box's own namespaces from people's without reading
/// the identity table — which a person may not.
#[tokio::test]
async fn a_namespace_says_whose_kind_of_account_it_is() {
let rig = console().await;
let beta = rig.session("beta", Role::Viewer);
let (status, body) = rig.get("/api/namespaces", &beta).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let page: serde_json::Value = serde_json::from_str(&body).expect("json");
let kind = |name: &str| {
page["items"]
.as_array()
.and_then(|items| items.iter().find(|n| n["name"] == name))
.map(|n| n["owner_kind"].clone())
};
assert_eq!(kind("beta"), Some(serde_json::json!("user")), "{body}");
assert_eq!(kind("gunnar-twin"), Some(serde_json::json!("machine")), "{body}");
}
/// ★ **A comparison is gunnar's `Refs.Diff`, asked as the reader**: every
/// changed file and its counts, past the 60 the console draws hunks for. Red
/// before: the console walked both trees itself, one `ReadObject` per tree,
/// believing gunnar could not diff.
#[tokio::test]
async fn a_comparison_is_gunnars_tree_diff_asked_as_the_reader() {
let rig = console().await;
let acme = rig.session("acme", Role::Viewer);
// The reader acme's own channel, as the route would use it.
let (status, _) = rig.get("/api/repos/acme/app/pulls", &acme).await;
assert_eq!(status, http::StatusCode::OK);
let reader = crate::api::tenant_reader(&rig.state, "acme").await.expect("acme reads as acme");
let repo = crate::git::Repository::open(&reader.gunnar, "acme", "app", Default::default())
.await
.expect("opens");
rig.fake.take_heard();
let comparison = crate::git::diff::compare(&repo, "tree-before", "tree-after", "")
.await
.expect("gunnar answers the comparison");
assert_eq!(comparison.changes.len(), 70);
assert_eq!(comparison.counted_files, 70, "every file is counted, by gunnar");
assert_eq!(comparison.insertions, 140);
assert!(comparison.hunks_truncated);
let heard = rig.fake.take_heard();
assert!(heard.contains(&"Refs.Diff by acme".to_owned()), "{heard:?}");
}
// ── roles are gunnar's grants (thin layer L, 2026-10-04) ───────────────────
/// ★ **An administrator reads and acts as THEMSELVES**: their console role is
/// their server-wide grant at gunnar, so the console signs with their own key
/// and gunnar's audit names them. Red before: an operator's or an
/// administrator's session was answered with the console's own key.
#[tokio::test]
async fn an_administrator_reads_as_themselves_and_gunnar_names_them() {
let rig = console().await;
rig.fake.grant("boss", "", pb::GrantLevel::Admin);
let boss = rig.session("boss", Role::Admin);
rig.fake.take_heard();
let (status, body) = rig.get("/api/principals", &boss).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
all_by(&rig.fake.take_heard(), "boss", "/api/principals");
}
/// ★ **The console role is gunnar's answer, not the passkey row's**: a
/// session whose row says admin, for a person gunnar holds no server-wide
/// grant for, is a viewer — refused an administrator's route, and told so by
/// `/auth/status` — and a grant at gunnar is what makes somebody an operator.
/// Red before: the role was the console's own ladder, read off its own row.
#[tokio::test]
async fn the_console_role_is_the_persons_server_wide_grant_at_gunnar() {
let rig = console().await;
rig.webauthn
.plant_bound_credential_for_tests("mallory", Role::Admin, "mallory");
let mallory = rig.webauthn.make_session_asking_gunnar("mallory", Role::Admin);
let (status, body) = rig.get("/auth/status", &mallory).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let said: serde_json::Value = serde_json::from_str(&body).expect("json");
assert_eq!(said["role"], "viewer", "gunnar holds no grant for mallory: {body}");
let (status, body) = rig.get("/api/disks", &mallory).await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "an administrator's read: {body}");
// A server-wide Write at gunnar is what makes somebody an operator.
rig.fake.grant("olive", "", pb::GrantLevel::Write);
rig.webauthn
.plant_bound_credential_for_tests("olive", Role::Operator, "olive");
let olive = rig.webauthn.make_session_asking_gunnar("olive", Role::Operator);
let (_, body) = rig.get("/auth/status", &olive).await;
let said: serde_json::Value = serde_json::from_str(&body).expect("json");
assert_eq!(said["role"], "operator", "{body}");
}
/// ★ **An issue is filed as the person**: the forge event names their
/// principal and carries their own signature, and gunnar's Write on the
/// repository decides. Red before: every issue, comment and close the console
/// filed was the console's own, attributed to its principal.
#[tokio::test]
async fn an_issue_is_filed_as_the_person_and_gunnar_decides() {
let rig = console().await;
rig.fake.grant("acme", "", pb::GrantLevel::Write);
let acme = rig.session("acme", Role::Operator);
rig.fake.take_heard();
let (status, body) = rig
.post(
"/api/repos/acme/app/issues",
&acme,
serde_json::json!({ "title": "a bug", "body": "it broke" }),
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let heard = rig.fake.take_heard();
assert!(heard.contains(&"Forge.Append by acme".to_owned()), "{heard:?}");
assert!(heard.contains(&"event actor acme".to_owned()), "{heard:?}");
}