crates/gunnar-ui-server/src/account_model.rs
//! **The owner's account model (2026-10-03), the console's half**, driven end
//! to end against a gunnar that keeps a grant table and a namespace list.
//!
//! Nothing is bought and nothing is a flag an operator sets:
//!
//! 1. every account's namespace and events are private until its admin says
//! otherwise (`PATCH /api/account/namespaces/{ns}/settings`), and a session
//! that is not the account's admin is refused before anything is signed;
//! 2. a repository's visibility is the account admin's (gunnar's default for
//! every account);
//! 3. the account admin enrolls NEW employees (`POST /api/account/employees`):
//! gunnar's `Accounts.EnrollEmployee` and an invite pinned to the new name;
//! 4. `/api/namespaces/{ns}/page` — what a visitor sees at `/<ns>` — lists only
//! the public repositories of a private namespace, and a private namespace
//! with none answers exactly as one that does not exist.
//!
//! Tests only, and its own module for the reason [`crate::tenant_import`] is:
//! what is asserted is a property of the whole path — session → bound
//! principal → gunnar's grant table → `Repos.SetVisibility` — not of one
//! handler.
//!
//! # What gunnar HEARD, and from whom, is the assertion
//!
//! Since 2026-10-04 (thin layer) the console signs `Repos.SetVisibility`,
//! `Repos.SetNamespaceSettings`, `Accounts.EnrollEmployee` and
//! `Accounts.ListEmployees` AS THE PERSON, and the fake below decides the way
//! gunnar does: the console's key holds a server-wide `Admin`; a person is the
//! account admin of their own namespace and of any namespace they hold `Admin`
//! over, and is refused in gunnar's words otherwise. So every row asserts two
//! logs: `asked` (every such call, and WHOSE key signed it) and `heard` (what
//! gunnar actually changed) — a refusal is a call signed by the person that
//! changed nothing.
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] = [53u8; 32];
const GUEST_SEED: [u8; 32] = [54u8; 32];
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 console_signer() -> crate::signer::Signer {
signer_of(CONSOLE_SEED)
}
/// Did the CONSOLE'S key sign this call? The fake answers that key with the
/// whole server, as gunnar answers a server-wide admin; anything else (the
/// guest, a tenant) is an outsider and is narrowed, as gunnar narrows it.
fn by_console<T>(request: &Request<T>) -> bool {
let console = console_signer().fingerprint().to_owned();
request
.metadata()
.get(crate::signer::CREDENTIAL_METADATA_KEY)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.split_whitespace().nth(1).map(str::to_owned))
.is_some_and(|fp| fp == console)
}
// ───────────────────────────────────────────────────────────────────────────
// The fake
// ───────────────────────────────────────────────────────────────────────────
#[derive(Clone, Default)]
struct Fake {
/// Every change gunnar MADE, as `<ns>/<repo> -> <visibility>` and friends.
heard: Arc<Mutex<Vec<String>>>,
/// Every account-admin call, as `<Method> by <who>` — `console`, the
/// principal whose key signed it, or `stranger`.
asked: Arc<Mutex<Vec<String>>>,
/// Tenant-key fingerprint → principal, as `Access.AddKey` enrolled them.
keys: Arc<Mutex<BTreeMap<String, String>>>,
/// Every `Access.Check`, as `Access.Check <ns> by <who>`.
checks: Arc<Mutex<Vec<String>>>,
grants: Arc<Mutex<Vec<pb::Grant>>>,
/// id → kind.
principals: Arc<Mutex<BTreeMap<String, i32>>>,
/// The namespaces gunnar lists → (public, events public).
namespaces: Arc<Mutex<BTreeMap<String, (bool, bool)>>>,
/// Employee → account.
employees: Arc<Mutex<BTreeMap<String, String>>>,
/// Members the account's admin gave Admin over the account (round 5.3.1): gunnar's
/// WhoAmI answers them `admin-appointed`.
appointed: Arc<Mutex<std::collections::BTreeSet<String>>>,
/// `<ns>/<repo>` → visibility.
repos: Arc<Mutex<BTreeMap<String, i32>>>,
/// `<ns>/<repo>` → the "encrypted" flag, as `Repos.SetEncrypted` set it.
encrypted: Arc<Mutex<BTreeMap<String, bool>>>,
/// Invites gunnar minted: (principal, server level, who made it).
/// A separate invite call gunnar would have heard: the deleted
/// `Access.CreateInvite` (IDENTITY-HOST) — there is none any more, and the
/// rows assert it stays empty.
invites: Arc<Mutex<Vec<(String, i32, String)>>>,
}
impl Fake {
fn heard(&self) -> Vec<String> {
self.heard.lock().expect("heard").clone()
}
fn asked(&self) -> Vec<String> {
self.asked.lock().expect("asked").clone()
}
/// Whose key signed this request.
fn who<T>(&self, request: &Request<T>) -> String {
if by_console(request) {
return "console".to_owned();
}
let fingerprint = request
.metadata()
.get(crate::signer::CREDENTIAL_METADATA_KEY)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.split_whitespace().nth(1).map(str::to_owned))
.unwrap_or_default();
self.keys
.lock()
.expect("keys")
.get(&fingerprint)
.cloned()
.unwrap_or_else(|| "stranger".to_owned())
}
/// gunnar's account-admin rule (`require(namespace, Admin)`): the console's
/// server-wide Admin, the account's own principal, or a namespace Admin
/// grant. Refused in gunnar's words, after noting who asked.
/// Admin over `account`, by gunnar's rule.
fn is_admin(&self, who: &str, account: &str) -> bool {
who == "console"
|| who == account
|| self.grants.lock().expect("grants").iter().any(|g| {
g.principal_id == who
// A namespace Admin grant, or a server-wide one (the box's
// administrator is every account's admin).
&& (g.namespace == account || g.namespace.is_empty())
&& g.repo.is_empty()
&& g.level == pb::GrantLevel::Admin as i32
})
}
fn account_admin<T>(&self, request: &Request<T>, method: &str, account: &str, what: &str) -> Result<(), Status> {
let who = self.who(request);
self.asked
.lock()
.expect("asked")
.push(format!("{method} by {who}"));
let admin = self.is_admin(&who, account);
if admin {
Ok(())
} else {
Err(Status::permission_denied(format!(
"principal {who} has no admin grant on namespace:{account}: {what} {account} \
are the account admin's decision (admin of account {account})"
)))
}
}
fn user(&self, id: &str) {
self.principals
.lock()
.expect("principals")
.insert(id.to_owned(), pb::PrincipalKind::User as i32);
}
fn grant(&self, principal: &str, ns: &str, repo: &str, level: pb::GrantLevel) {
self.grants.lock().expect("grants").push(pb::Grant {
principal_id: principal.into(),
namespace: ns.into(),
repo: repo.into(),
level: level as i32,
granted_at_unix_ms: 1,
});
}
fn namespace(&self, ns: &str, public: bool) {
self.namespaces
.lock()
.expect("namespaces")
.insert(ns.to_owned(), (public, false));
}
fn repo_with(&self, ns: &str, repo: &str, visibility: pb::Visibility) {
self.repo(ns, repo);
self.repos
.lock()
.expect("repos")
.insert(format!("{ns}/{repo}"), visibility as i32);
}
fn repo(&self, ns: &str, repo: &str) {
self.namespaces
.lock()
.expect("namespaces")
.entry(ns.to_owned())
.or_insert((false, false));
self.repos
.lock()
.expect("repos")
.insert(format!("{ns}/{repo}"), pb::Visibility::Private as i32);
}
fn repo_out(path: pb::RepoPath, visibility: i32) -> pb::Repo {
pb::Repo {
path: Some(path),
visibility,
..Default::default()
}
}
}
// Only the calls this path makes; the generated default stubs answer every
// other one `UNIMPLEMENTED`.
#[tonic::async_trait]
impl pb::access_server::Access for Fake {
async fn add_key(
&self,
request: Request<pb::AddKeyRequest>,
) -> Result<Response<pb::AddKeyResponse>, Status> {
if !by_console(&request) {
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,
..Default::default()
}),
}))
}
async fn check(
&self,
request: Request<pb::CheckRequest>,
) -> Result<Response<pb::CheckResponse>, Status> {
let who = self.who(&request);
let asked = request.into_inner();
self.checks
.lock()
.expect("checks")
.push(format!("Access.Check {} by {who}", asked.namespace));
// Only the account-admin question is modelled; it is the only one
// this path asks.
// A repository's Admin (the Danger zone's question, 2026-10-28): the account
// admin's, or a repository Admin grant.
let repo_admin = !asked.repo.is_empty()
&& self.grants.lock().expect("grants").iter().any(|g| {
g.principal_id == who
&& g.namespace == asked.namespace
&& g.repo == asked.repo
&& g.level == pb::GrantLevel::Admin as i32
});
let allowed = asked.level == pb::GrantLevel::Admin as i32
&& (self.is_admin(&who, &asked.namespace) || repo_admin);
Ok(Response::new(pb::CheckResponse {
allowed,
reason: if allowed {
String::new()
} else {
format!("principal {who} has no admin grant on namespace:{}", asked.namespace)
},
}))
}
async fn list_principals(
&self,
_: Request<pb::ListPrincipalsRequest>,
) -> Result<Response<pb::ListPrincipalsResponse>, Status> {
let principals = self
.principals
.lock()
.expect("principals")
.iter()
.map(|(id, kind)| pb::Principal {
id: id.clone(),
name: id.clone(),
kind: *kind,
created_at_unix_ms: 1,
})
.collect();
Ok(Response::new(pb::ListPrincipalsResponse {
principals,
next_page_token: String::new(),
..Default::default()
}))
}
/// gunnar's rule since 2026-10-07 for a request naming a namespace: that
/// namespace's admin's, answered for that namespace only.
async fn list_grants(
&self,
request: Request<pb::ListGrantsRequest>,
) -> Result<Response<pb::ListGrantsResponse>, Status> {
let who = self.who(&request);
let asked = request.into_inner();
let grants = self.grants.lock().expect("grants").clone();
// The un-narrowed listing answers as it always did in this fake (the
// rows above lean on it); the namespace question is modelled exactly.
if who == "console" || asked.namespace.is_empty() {
return Ok(Response::new(pb::ListGrantsResponse { grants }));
}
if !self.is_admin(&who, &asked.namespace) {
return Err(Status::permission_denied(format!(
"principal {who} has no admin grant on namespace:{}",
asked.namespace
)));
}
Ok(Response::new(pb::ListGrantsResponse {
grants: grants
.into_iter()
.filter(|g| g.namespace == asked.namespace && (asked.repo.is_empty() || g.repo == asked.repo))
.collect(),
}))
}
/// **Who am I** (2026-10-07: and which account do I act in). The fake
/// keeps no server grants for persons, so it answers `Admin` and the
/// console's role stays the cookie's (the console takes the lower of the
/// two) — every row above reads as it did when this was unimplemented.
/// The account: a member's employer, else a namespace of the person's own
/// name — `admin` with members, `individual` without.
async fn who_am_i(
&self,
request: Request<pb::WhoAmIRequest>,
) -> Result<Response<pb::WhoAmIResponse>, Status> {
let who = self.who(&request);
let employer = self.employees.lock().expect("employees").get(&who).cloned();
let appointed = self.appointed.lock().expect("appointed").contains(&who);
let (account, role) = match employer {
Some(account) if appointed => (account, "admin-appointed"),
Some(account) => (account, "employee"),
None if self.namespaces.lock().expect("namespaces").contains_key(&who) => {
let members = self.employees.lock().expect("employees").values().any(|a| *a == who);
(who.clone(), if members { "admin" } else { "individual" })
}
None => (String::new(), ""),
};
Ok(Response::new(pb::WhoAmIResponse {
principal: Some(pb::Principal { id: who.clone(), name: who.clone(), kind: pb::PrincipalKind::User as i32, created_at_unix_ms: 1 }),
server_level: pb::GrantLevel::Admin as i32,
account_admin: account.clone(),
account,
account_role: role.to_owned(),
..Default::default()
}))
}
/// gunnar's rule: Admin over the repository (the account's admin, or the
/// console) grants on it.
async fn grant(
&self,
request: Request<pb::GrantRequest>,
) -> Result<Response<pb::GrantResponse>, Status> {
let who = self.who(&request);
let asked = request.into_inner();
self.asked.lock().expect("asked").push(format!("Access.Grant by {who}"));
if !self.is_admin(&who, &asked.namespace) {
return Err(Status::permission_denied(format!(
"principal {who} has no admin grant on repo:{}/{}",
asked.namespace, asked.repo
)));
}
let level = pb::GrantLevel::try_from(asked.level).unwrap_or(pb::GrantLevel::Unspecified);
self.grants
.lock()
.expect("grants")
.retain(|g| !(g.principal_id == asked.principal_id && g.namespace == asked.namespace && g.repo == asked.repo));
self.grant(&asked.principal_id, &asked.namespace, &asked.repo, level);
self.heard.lock().expect("heard").push(format!(
"grant {} {} on {}/{}",
asked.principal_id,
level.as_str_name(),
asked.namespace,
asked.repo
));
Ok(Response::new(pb::GrantResponse { grant: None }))
}
async fn revoke(
&self,
request: Request<pb::RevokeRequest>,
) -> Result<Response<pb::RevokeResponse>, Status> {
let who = self.who(&request);
let asked = request.into_inner();
self.asked.lock().expect("asked").push(format!("Access.Revoke by {who}"));
if !self.is_admin(&who, &asked.namespace) {
return Err(Status::permission_denied(format!(
"principal {who} has no admin grant on repo:{}/{}",
asked.namespace, asked.repo
)));
}
self.grants
.lock()
.expect("grants")
.retain(|g| !(g.principal_id == asked.principal_id && g.namespace == asked.namespace && g.repo == asked.repo));
self.heard
.lock()
.expect("heard")
.push(format!("revoke {} on {}/{}", asked.principal_id, asked.namespace, asked.repo));
Ok(Response::new(pb::RevokeResponse {}))
}
async fn list_group_members(
&self,
_: Request<pb::ListGroupMembersRequest>,
) -> Result<Response<pb::ListGroupMembersResponse>, Status> {
Ok(Response::new(pb::ListGroupMembersResponse {
members: Vec::new(),
}))
}
}
#[tonic::async_trait]
impl pb::repos_server::Repos for Fake {
/// gunnar's rule for a new repository (`require(namespace, Admin)`): the
/// account admin, in their own account or one they hold Admin over.
async fn create(
&self,
request: Request<pb::CreateRepoRequest>,
) -> Result<Response<pb::CreateRepoResponse>, Status> {
let path = request.get_ref().path.clone().unwrap_or_default();
self.account_admin(&request, "Repos.Create", &path.namespace, "new repositories in account")?;
let visibility = match request.get_ref().visibility {
v if v == pb::Visibility::Public as i32 => v,
_ => pb::Visibility::Private as i32,
};
let key = format!("{}/{}", path.namespace, path.repo);
if self.repos.lock().expect("repos").contains_key(&key) {
return Err(Status::already_exists(key));
}
self.heard.lock().expect("heard").push(format!("created {key} {}", crate::api::visibility_name(visibility)));
self.repo_with(&path.namespace, &path.repo, if visibility == pb::Visibility::Public as i32 { pb::Visibility::Public } else { pb::Visibility::Private });
Ok(Response::new(pb::CreateRepoResponse { repo: Some(Fake::repo_out(path, visibility)) }))
}
// Encrypted repositories (gunnar 2026-10-05): the flag is the ACCOUNT
// admin's, the same rule as visibility (gunnar's `account_admin_only_for`).
async fn set_encrypted(
&self,
request: tonic::Request<pb::SetEncryptedRequest>,
) -> Result<tonic::Response<pb::SetEncryptedResponse>, tonic::Status> {
let account = request
.get_ref()
.path
.as_ref()
.map(|p| p.namespace.clone())
.unwrap_or_default();
self.account_admin(&request, "Repos.SetEncrypted", &account, "encrypted repositories in account")?;
let asked = request.into_inner();
let path = asked.path.unwrap_or_default();
let key = format!("{}/{}", path.namespace, path.repo);
self.heard.lock().expect("heard").push(format!(
"{key} -> {}",
if asked.encrypted { "encrypted" } else { "not encrypted" }
));
self.encrypted.lock().expect("encrypted").insert(key.clone(), asked.encrypted);
let visibility = self.repos.lock().expect("repos").get(&key).copied().unwrap_or_default();
let mut repo = Fake::repo_out(path, visibility);
repo.encrypted = asked.encrypted;
Ok(Response::new(pb::SetEncryptedResponse { repo: Some(repo) }))
}
async fn recipient_keys(
&self,
_: tonic::Request<pb::RecipientKeysRequest>,
) -> Result<tonic::Response<pb::RecipientKeysResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("Repos.RecipientKeys"))
}
/// gunnar's rule for `Repos.Delete`: Admin over the repository or its
/// namespace (the account admin, or a repository admin grant).
async fn delete(
&self,
request: Request<pb::DeleteRepoRequest>,
) -> Result<Response<pb::DeleteRepoResponse>, Status> {
let who = self.who(&request);
let path = request.get_ref().path.clone().unwrap_or_default();
self.asked.lock().expect("asked").push(format!("Repos.Delete by {who}"));
let repo_admin = self.grants.lock().expect("grants").iter().any(|g| {
g.principal_id == who
&& g.namespace == path.namespace
&& g.repo == path.repo
&& g.level == pb::GrantLevel::Admin as i32
});
if !(self.is_admin(&who, &path.namespace) || repo_admin) {
return Err(Status::permission_denied(format!(
"principal {who} has no admin grant on repo:{}/{}",
path.namespace, path.repo
)));
}
let key = format!("{}/{}", path.namespace, path.repo);
self.heard.lock().expect("heard").push(format!("{key} deleted"));
self.repos.lock().expect("repos").remove(&key);
Ok(Response::new(pb::DeleteRepoResponse::default()))
}
/// gunnar's rule for `Repos.Undelete`: the delete's, Admin over the repository
/// or its namespace.
async fn undelete(
&self,
request: Request<pb::UndeleteRepoRequest>,
) -> Result<Response<pb::UndeleteRepoResponse>, Status> {
let who = self.who(&request);
let path = request.get_ref().path.clone().unwrap_or_default();
self.asked.lock().expect("asked").push(format!("Repos.Undelete by {who}"));
if !self.is_admin(&who, &path.namespace) {
return Err(Status::permission_denied(format!(
"principal {who} has no admin grant on repo:{}/{}",
path.namespace, path.repo
)));
}
let key = format!("{}/{}", path.namespace, path.repo);
self.heard.lock().expect("heard").push(format!("{key} undeleted"));
self.repos.lock().expect("repos").insert(key, pb::Visibility::Private as i32);
Ok(Response::new(pb::UndeleteRepoResponse {
repo: Some(Fake::repo_out(path, pb::Visibility::Private as i32)),
}))
}
/// gunnar's rule for one namespace's `Repos.ListDeleted`: Read on it. The
/// fake models the account admin's, and answers gunnar's default window.
async fn list_deleted(
&self,
request: Request<pb::ListDeletedRequest>,
) -> Result<Response<pb::ListDeletedResponse>, Status> {
let who = self.who(&request);
let namespace = request.get_ref().namespace.clone();
if namespace.is_empty() || !self.is_admin(&who, &namespace) {
return Err(Status::permission_denied(format!("principal {who} may not list {namespace}")));
}
Ok(Response::new(pb::ListDeletedResponse {
default_retention_days: 30,
..Default::default()
}))
}
async fn get(
&self,
request: Request<pb::GetRepoRequest>,
) -> Result<Response<pb::GetRepoResponse>, Status> {
let path = request.into_inner().path.unwrap_or_default();
let key = format!("{}/{}", path.namespace, path.repo);
let visibility = *self
.repos
.lock()
.expect("repos")
.get(&key)
.ok_or_else(|| Status::not_found(key.clone()))?;
Ok(Response::new(pb::GetRepoResponse {
repo: Some(Fake::repo_out(path, visibility)),
}))
}
async fn list_namespaces(
&self,
request: Request<pb::ListNamespacesRequest>,
) -> Result<Response<pb::ListNamespacesResponse>, Status> {
let who = self.who(&request);
let employees = self.employees.lock().expect("employees").clone();
let namespaces = self
.namespaces
.lock()
.expect("namespaces")
.iter()
// gunnar narrows: what the caller may read (admin over it, here),
// and the public ones.
.filter(|(name, (public, _))| *public || self.is_admin(&who, name))
.map(|(name, (public, events_public))| pb::Namespace {
name: name.clone(),
public: *public,
events_public: *events_public,
employees: employees.values().filter(|a| *a == name).count() as u32,
..Default::default()
})
.collect();
Ok(Response::new(pb::ListNamespacesResponse {
namespaces,
next_page_token: String::new(),
}))
}
async fn list(
&self,
request: Request<pb::ListReposRequest>,
) -> Result<Response<pb::ListReposResponse>, Status> {
let everything = by_console(&request);
let namespace = request.into_inner().namespace;
let repos = self
.repos
.lock()
.expect("repos")
.iter()
.filter(|(key, _)| namespace.is_empty() || key.split('/').next() == Some(namespace.as_str()))
.filter(|(_, visibility)| everything || **visibility == pb::Visibility::Public as i32)
.map(|(key, visibility)| {
let (ns, name) = key.split_once('/').expect("ns/name");
Fake::repo_out(pb::RepoPath { namespace: ns.into(), repo: name.into() }, *visibility)
})
.collect();
Ok(Response::new(pb::ListReposResponse {
repos,
next_page_token: String::new(),
}))
}
async fn set_namespace_settings(
&self,
request: Request<pb::SetNamespaceSettingsRequest>,
) -> Result<Response<pb::SetNamespaceSettingsResponse>, Status> {
let account = request.get_ref().name.clone();
self.account_admin(&request, "Repos.SetNamespaceSettings", &account, "the settings of account")?;
let asked = request.into_inner();
let mut namespaces = self.namespaces.lock().expect("namespaces");
let row = namespaces
.get_mut(&asked.name)
.ok_or_else(|| Status::not_found(asked.name.clone()))?;
if let Some(public) = asked.public {
row.0 = public;
}
if let Some(events_public) = asked.events_public {
row.1 = events_public;
}
self.heard.lock().expect("heard").push(format!(
"settings {} public={} events={}",
asked.name, row.0, row.1
));
Ok(Response::new(pb::SetNamespaceSettingsResponse {
namespace: Some(pb::Namespace {
name: asked.name,
public: row.0,
events_public: row.1,
..Default::default()
}),
}))
}
async fn set_default_branch(
&self,
request: Request<pb::SetDefaultBranchRequest>,
) -> Result<Response<pb::SetDefaultBranchResponse>, Status> {
let path = request.into_inner().path.unwrap_or_default();
Ok(Response::new(pb::SetDefaultBranchResponse {
repo: Some(Fake::repo_out(path, pb::Visibility::Private as i32)),
}))
}
async fn set_visibility(
&self,
request: Request<pb::SetVisibilityRequest>,
) -> Result<Response<pb::SetVisibilityResponse>, Status> {
let account = request
.get_ref()
.path
.as_ref()
.map(|p| p.namespace.clone())
.unwrap_or_default();
self.account_admin(&request, "Repos.SetVisibility", &account, "the repositories of account")?;
let asked = request.into_inner();
let path = asked.path.unwrap_or_default();
let key = format!("{}/{}", path.namespace, path.repo);
self.heard.lock().expect("heard").push(format!(
"{key} -> {}",
crate::api::visibility_name(asked.visibility)
));
self.repos
.lock()
.expect("repos")
.insert(key, asked.visibility);
Ok(Response::new(pb::SetVisibilityResponse {
repo: Some(Fake::repo_out(path, asked.visibility)),
}))
}
}
/// gunnar's `Usage.Read`: on the namespace's own Read — its owner, a member
/// of the account (enrolment's Read), or the console.
#[tonic::async_trait]
impl pb::usage_server::Usage for Fake {
async fn read(
&self,
request: Request<pb::ReadUsageRequest>,
) -> Result<Response<pb::ReadUsageResponse>, Status> {
let who = self.who(&request);
let ns = request.into_inner().namespace;
self.asked.lock().expect("asked").push(format!("Usage.Read {ns} by {who}"));
let member = self.employees.lock().expect("employees").get(&who) == Some(&ns);
if !(self.is_admin(&who, &ns) || member) {
return Err(Status::permission_denied(format!("principal {who} has no read grant on namespace:{ns}")));
}
if !self.namespaces.lock().expect("namespaces").contains_key(&ns) {
return Err(Status::not_found(format!("namespace {ns} is not in the catalog")));
}
Ok(Response::new(pb::ReadUsageResponse {
usage: Some(pb::NamespaceUsage { pack_bytes: 3 << 20, ..Default::default() }),
..Default::default()
}))
}
}
#[tonic::async_trait]
impl pb::accounts_server::Accounts for Fake {
async fn enroll_employee(
&self,
request: Request<pb::EnrollEmployeeRequest>,
) -> Result<Response<pb::EnrollEmployeeResponse>, Status> {
let account = request.get_ref().account.clone();
self.account_admin(&request, "Accounts.EnrollEmployee", &account, "the employees of account")?;
let asked = request.into_inner();
if asked.count > 1 {
// Seats: no names, nobody created — gunnar's rule, in miniature.
self.heard.lock().expect("heard").push(format!(
"enroll {} seats {} room {}",
asked.account, asked.count, asked.room_bytes
));
return Ok(Response::new(pb::EnrollEmployeeResponse {
room_bytes: asked.room_bytes,
seats: asked.count,
seats_open: asked.count,
need_bytes: asked.room_bytes * u64::from(asked.count),
..Default::default()
}));
}
if self.principals.lock().expect("principals").contains_key(&asked.id) {
return Err(Status::already_exists(format!(
"principal {} already exists, and an existing principal can never become an \
employee",
asked.id
)));
}
self.user(&asked.id);
self.employees
.lock()
.expect("employees")
.insert(asked.id.clone(), asked.account.clone());
self.heard.lock().expect("heard").push(match (asked.from_seat, asked.invite) {
(true, true) => format!("enroll {}/{} from a seat, invited", asked.account, asked.id),
(true, false) => format!("enroll {}/{} from a seat", asked.account, asked.id),
(false, true) => format!("enroll {}/{}, invited", asked.account, asked.id),
(false, false) => format!("enroll {}/{}", asked.account, asked.id),
});
let (invite_id, invite_secret) = if asked.invite {
(format!("invite-of-{}", asked.id), format!("gunnars-secret-for-{}", asked.id))
} else {
(String::new(), String::new())
};
Ok(Response::new(pb::EnrollEmployeeResponse {
invite_id,
invite_secret,
invite_expires_at_unix_ms: if asked.invite { 9_000_000_000_000 } else { 0 },
principal: Some(pb::Principal {
id: asked.id.clone(),
name: asked.name,
kind: pb::PrincipalKind::User as i32,
created_at_unix_ms: 1,
}),
room_bytes: asked.room_bytes,
..Default::default()
}))
}
async fn list_employees(
&self,
request: Request<pb::ListEmployeesRequest>,
) -> Result<Response<pb::ListEmployeesResponse>, Status> {
let account = request.get_ref().account.clone();
self.account_admin(&request, "Accounts.ListEmployees", &account, "the employees of account")?;
let employees = self
.employees
.lock()
.expect("employees")
.iter()
.filter(|(_, a)| **a == account)
.map(|(id, _)| pb::Principal {
id: id.clone(),
name: id.clone(),
kind: pb::PrincipalKind::User as i32,
created_at_unix_ms: 1,
})
.collect();
Ok(Response::new(pb::ListEmployeesResponse { employees, ..Default::default() }))
}
}
// ───────────────────────────────────────────────────────────────────────────
// The console
// ───────────────────────────────────────────────────────────────────────────
struct Rig {
router: axum::Router,
webauthn: Arc<crate::auth::Webauthn>,
fake: Fake,
}
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.fake.user(user);
self.webauthn
.plant_bound_credential_for_tests(user, role, user);
self.webauthn.make_session(user, role, true)
}
async fn send(
&self,
method: &str,
uri: &str,
cookie: &str,
body: Option<serde_json::Value>,
) -> (http::StatusCode, serde_json::Value) {
let request = http::Request::builder()
.method(method)
.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.map(|b| b.to_string()).unwrap_or_default(),
))
.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");
let text = String::from_utf8_lossy(&bytes).into_owned();
let json =
serde_json::from_str(&text).unwrap_or_else(|_| serde_json::Value::String(text.clone()));
(status, json)
}
async fn set(
&self,
cookie: &str,
path: &str,
to: &str,
) -> (http::StatusCode, serde_json::Value) {
self.send(
"PATCH",
&format!("/api/repos/{path}"),
cookie,
Some(serde_json::json!({ "visibility": to })),
)
.await
}
/// A request with NO session at all — a visitor.
async fn visit(&self, uri: &str) -> (http::StatusCode, serde_json::Value) {
let request = http::Request::builder()
.method("GET")
.uri(uri)
.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");
let text = String::from_utf8_lossy(&bytes).into_owned();
(
status,
serde_json::from_str(&text).unwrap_or(serde_json::Value::String(text)),
)
}
async fn authority(&self, cookie: &str, path: &str) -> serde_json::Value {
let (status, body) = self
.send(
"GET",
&format!("/api/repos/{path}/visibility"),
cookie,
None,
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
body
}
}
async fn console(fake: Fake) -> Rig {
console_selling(fake, None).await
}
/// [`console`], its monetize on loopback at `monetize_addr` (`None`: a console that sells
/// nothing, every row above).
#[cfg_attr(not(feature = "metered"), allow(unused_variables))]
async fn console_selling(fake: Fake, monetize_addr: Option<String>) -> Rig {
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::usage_server::UsageServer::new(serving.clone()))
.add_service(pb::repos_server::ReposServer::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 router = crate::serve::router(Arc::new(crate::app::App {
webauthn: Some(webauthn.clone()),
admin_token: Some("break-glass".to_owned()),
#[cfg(feature = "metered")]
monetize: monetize_addr.map(|addr| {
crate::monetize::Monetize::connect_lazy(&addr, crate::auth::signup::tests::BEARER, "gunnar").expect("a lazy monetize channel")
}),
events: crate::sse::EventHub::detached(),
gunnar: crate::grpc::Gunnar::connect_lazy(&endpoint, Arc::new(console_signer()))
.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(),
}));
Rig {
router,
webauthn,
fake,
}
}
// ───────────────────────────────────────────────────────────────────────────
// The rows
// ───────────────────────────────────────────────────────────────────────────
/// ★ **A repository admin who is not the account admin is refused, by name,
/// and gunnar hears nothing.** Red before this module: `PATCH` passed the
/// visibility straight to `Repos.SetVisibility` on the console's key, so any
/// operator session published any repository.
#[tokio::test]
async fn an_operator_who_is_not_the_account_admin_cannot_change_visibility() {
let fake = Fake::default();
fake.repo("acme", "app");
fake.grant("kim", "acme", "app", pb::GrantLevel::Admin);
let b = console(fake).await;
let kim = b.session("kim", Role::Operator);
let (status, body) = b.set(&kim, "acme/app", "public").await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
let said = body["message"].as_str().unwrap_or_default();
assert!(
said.contains("account admin") && said.contains("acme"),
"the refusal says whose decision it is: {body}"
);
assert!(
b.fake.heard().is_empty(),
"the console refused AFTER publishing: gunnar heard {:?}",
b.fake.heard()
);
// …and the page is told the same thing, so it draws no control.
let authority = b.authority(&kim, "acme/app").await;
assert_eq!(authority["account_admin"], false, "{authority}");
assert_eq!(authority["may_change"], false, "{authority}");
assert!(
authority["why"]
.as_str()
.unwrap_or_default()
.contains("account admin"),
"{authority}"
);
// ★ gunnar's answer, asked as kim (thin layer, owner 2026-10-04) — not the
// console's copy of the rule over a grant table it read with its own key.
assert_eq!(
b.fake.checks.lock().expect("checks").clone(),
vec!["Access.Check acme by kim".to_owned()],
);
}
/// The account admin may: their own account, or a namespace admin grant.
#[tokio::test]
async fn the_account_admin_changes_visibility_from_the_console() {
let fake = Fake::default();
fake.repo("kim", "notes");
fake.repo("acme", "app");
fake.grant("kim", "acme", "", pb::GrantLevel::Admin);
let b = console(fake).await;
let kim = b.session("kim", Role::Operator);
for path in ["kim/notes", "acme/app"] {
let authority = b.authority(&kim, path).await;
assert_eq!(authority["may_change"], true, "{path}: {authority}");
let (status, body) = b.set(&kim, path, "public").await;
assert_eq!(status, http::StatusCode::OK, "{path}: {body}");
assert_eq!(body["visibility"], "public", "{body}");
}
assert_eq!(
b.fake.heard(),
vec![
"kim/notes -> public".to_owned(),
"acme/app -> public".to_owned()
]
);
// A namespace WRITE grant is not account admin.
b.fake.repo("team", "tool");
b.fake.grant("kim", "team", "", pb::GrantLevel::Write);
let (status, body) = b.set(&kim, "team/tool", "public").await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
}
/// The box's administrator is every account's admin (the console's key holds a
/// server-wide `Admin`), and the rule is the same both ways: the account admin
/// publishes and retracts; an operator who is not the account admin may do
/// neither — making private is as much the account's decision as making public.
#[tokio::test]
async fn the_boxs_administrator_turns_any_account_both_ways_and_nobody_else_does() {
let fake = Fake::default();
fake.repo("acme", "app");
fake.grant("kim", "acme", "app", pb::GrantLevel::Admin);
// The box's administrator: a server-wide Admin grant at gunnar (console
// roles are grants, thin layer 2026-10-04) — and every act signed as boss.
fake.grant("boss", "", "", pb::GrantLevel::Admin);
let b = console(fake).await;
let boss = b.session("boss", Role::Admin);
let kim = b.session("kim", Role::Operator);
let authority = b.authority(&boss, "acme/app").await;
assert_eq!(authority["account_admin"], true, "{authority}");
assert_eq!(authority["may_change"], true, "{authority}");
for to in ["public", "private"] {
let (status, body) = b.set(&boss, "acme/app", to).await;
assert_eq!(status, http::StatusCode::OK, "{to}: {body}");
}
let (status, body) = b.set(&kim, "acme/app", "private").await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
assert_eq!(
b.fake.heard(),
vec!["acme/app -> public".to_owned(), "acme/app -> private".to_owned()],
"only the box administrator's two turns reached gunnar"
);
}
/// A viewer who IS the account admin is offered the control (2026-10-06: a
/// paying customer is a viewer, and gunnar decides as the person). And a
/// settings PATCH that does not touch visibility is not this rule's.
#[tokio::test]
async fn a_viewer_account_admin_is_offered_the_control_and_other_settings_are_untouched() {
let fake = Fake::default();
fake.repo("vera", "ledger");
fake.repo("acme", "app");
let b = console(fake).await;
let vera = b.session("vera", Role::Viewer);
let authority = b.authority(&vera, "vera/ledger").await;
assert_eq!(authority["account_admin"], true, "{authority}");
assert_eq!(authority["may_change"], true, "{authority}");
assert!(authority["why"].is_null(), "{authority}");
// An operator who is not acme's admin may still set acme/app's default
// branch — that is not a visibility change and was never this rule's.
let kim = b.session("kim", Role::Operator);
let (status, body) = b
.send(
"PATCH",
"/api/repos/acme/app",
&kim,
Some(serde_json::json!({ "default_branch": "refs/heads/trunk" })),
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(b.fake.heard().is_empty());
}
/// ★ **An encrypted repository is the account admin's decision at the
/// console too** (2026-10-06): an installed estate binds the control plane to
/// its WireGuard overlay, so the console is where an account admin turns the
/// flag — `PATCH {"encrypted": true}` signs `Repos.SetEncrypted` AS THE PERSON
/// and gunnar's rule decides; the repository read back says it. Anyone else
/// is refused in gunnar's words and gunnar changes nothing.
#[tokio::test]
async fn the_account_admin_turns_a_repository_encrypted_at_the_console_and_nobody_else_does() {
let fake = Fake::default();
fake.repo("kim", "vault");
fake.repo("acme", "app");
fake.grant("lee", "kim", "vault", pb::GrantLevel::Admin);
let b = console(fake).await;
let kim = b.session("kim", Role::Operator);
let lee = b.session("lee", Role::Operator);
let (status, body) = b
.send("PATCH", "/api/repos/kim/vault", &kim, Some(serde_json::json!({ "encrypted": true })))
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert_eq!(body["encrypted"], true, "the repository read back is encrypted: {body}");
assert_eq!(b.fake.heard(), vec!["kim/vault -> encrypted".to_owned()]);
assert!(
b.fake.asked().contains(&"Repos.SetEncrypted by kim".to_owned()),
"signed as the person, not the console: {:?}",
b.fake.asked()
);
// A repository admin who is not the account admin: refused, nothing heard.
let (status, body) = b
.send("PATCH", "/api/repos/kim/vault", &lee, Some(serde_json::json!({ "encrypted": false })))
.await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
assert!(
body["message"].as_str().unwrap_or_default().contains("account admin"),
"{body}"
);
assert_eq!(b.fake.heard(), vec!["kim/vault -> encrypted".to_owned()], "lee changed nothing");
// Off again, by the account admin.
let (status, body) = b
.send("PATCH", "/api/repos/kim/vault", &kim, Some(serde_json::json!({ "encrypted": false })))
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert_eq!(body["encrypted"], false, "{body}");
}
/// ★ **A paying customer is a viewer, and the admin of their own account**
/// (the owner's accounts spec: only the account admin makes a repository
/// public; clans: only the clan's admin publishes or deletes). Since the thin
/// layer every one of these acts is signed AS THE PERSON and gunnar decides,
/// so the console's floor must not stand in front of gunnar's rule: the
/// customer publishes, encrypts and deletes their own repository from the
/// console, and a viewer who writes to it but is not its account admin is
/// refused by gunnar, in gunnar's words, with nothing changed.
#[tokio::test]
async fn a_customer_publishes_encrypts_and_deletes_their_own_repository_and_a_writer_is_refused() {
let fake = Fake::default();
fake.repo("vera", "ledger");
fake.grant("walt", "vera", "ledger", pb::GrantLevel::Write);
let b = console(fake).await;
let vera = b.session("vera", Role::Viewer);
let walt = b.session("walt", Role::Viewer);
let authority = b.authority(&vera, "vera/ledger").await;
assert_eq!(authority["may_change"], true, "the account admin is offered the control: {authority}");
let (status, body) = b.set(&vera, "vera/ledger", "public").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let (status, body) = b
.send("PATCH", "/api/repos/vera/ledger", &vera, Some(serde_json::json!({ "encrypted": true })))
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
// walt writes to it and is not its account admin: gunnar refuses.
let authority = b.authority(&walt, "vera/ledger").await;
assert_eq!(authority["may_change"], false, "{authority}");
let (status, body) = b.set(&walt, "vera/ledger", "private").await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
assert!(body["message"].as_str().unwrap_or_default().contains("account admin"), "{body}");
let (status, body) = b.send("DELETE", "/api/repos/vera/ledger", &walt, None).await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
assert_eq!(
b.fake.heard(),
vec!["vera/ledger -> public".to_owned(), "vera/ledger -> encrypted".to_owned()],
"walt changed nothing"
);
assert!(b.fake.asked().contains(&"Repos.Delete by walt".to_owned()), "gunnar was asked, as walt: {:?}", b.fake.asked());
// The account admin deletes it (a soft delete, undoable).
let (status, body) = b.send("DELETE", "/api/repos/vera/ledger", &vera, None).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert_eq!(b.fake.heard().last().map(String::as_str), Some("vera/ledger deleted"));
}
/// ★ **The Danger zone's answer** (owner spec 2026-10-28): `GET
/// /api/repos/{ns}/{name}/deletable` says whether THIS person may delete the
/// repository — gunnar's `Access.Check` at Admin on it, asked as them — and,
/// when they may, for how many days it can be restored: gunnar's window
/// (`ListDeletedResponse.default_retention_days`), never a number of the
/// console's. Whoever may not is given a sentence with gunnar's reason in it.
/// The list's Undo is the account admin's too, on a viewer session, and gunnar
/// decides it as the person.
#[tokio::test]
async fn the_danger_zone_is_gunnars_answer_with_gunnars_window_and_the_undo_is_the_account_admins() {
let fake = Fake::default();
fake.repo("vera", "ledger");
fake.grant("walt", "vera", "ledger", pb::GrantLevel::Write);
let b = console(fake).await;
let vera = b.session("vera", Role::Viewer);
let walt = b.session("walt", Role::Viewer);
let (status, said) = b.send("GET", "/api/repos/vera/ledger/deletable", &vera, None).await;
assert_eq!(status, http::StatusCode::OK, "{said}");
assert_eq!(said["may_delete"], true, "the account admin may delete: {said}");
assert_eq!(said["retention_days"], 30, "the window is gunnar's: {said}");
assert!(said["why"].is_null(), "{said}");
let (status, said) = b.send("GET", "/api/repos/vera/ledger/deletable", &walt, None).await;
assert_eq!(status, http::StatusCode::OK, "{said}");
assert_eq!(said["may_delete"], false, "a writer may not: {said}");
let why = said["why"].as_str().unwrap_or_default();
assert!(why.contains("Only an admin of vera/ledger or of its account may delete it."), "{why}");
assert!(why.contains("walt has no admin grant"), "gunnar's own reason is in the sentence: {why}");
assert_eq!(said["retention_days"], 0, "{said}");
// A visitor is refused before it: not on the anonymous allow-list.
let (status, _) = b.visit("/api/repos/vera/ledger/deletable").await;
assert_eq!(status, http::StatusCode::UNAUTHORIZED);
// Deleted, then undone from the list: walt is refused by gunnar, vera puts it back.
let (status, body) = b.send("DELETE", "/api/repos/vera/ledger", &vera, None).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let (status, body) = b.send("POST", "/api/repos/vera/ledger/undelete", &walt, None).await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
assert!(b.fake.asked().contains(&"Repos.Undelete by walt".to_owned()), "gunnar was asked, as walt: {:?}", b.fake.asked());
let (status, body) = b.send("POST", "/api/repos/vera/ledger/undelete", &vera, None).await;
assert_eq!(status, http::StatusCode::OK, "the account admin's undo on a viewer session: {body}");
assert_eq!(b.fake.heard().last().map(String::as_str), Some("vera/ledger undeleted"));
}
// ── rule 4: `/<ns>` — a private namespace shows a visitor only its public repos
fn repo_names(body: &serde_json::Value) -> Vec<String> {
body["repos"]
.as_array()
.map(|repos| {
repos
.iter()
.filter_map(|r| r["name"].as_str().map(str::to_owned))
.collect()
})
.unwrap_or_default()
}
/// ★ **A visitor at `/<ns>` of a private namespace sees ONLY its public
/// repositories** — not the private ones, not even a count — and a private
/// namespace with no public repository answers exactly as one that does not
/// exist. A PUBLIC namespace is shown even while it holds nothing public.
#[tokio::test]
async fn a_visitor_sees_a_private_namespace_as_its_public_repositories_or_not_at_all() {
let fake = Fake::default();
fake.repo_with("acme", "open", pb::Visibility::Public);
fake.repo_with("acme", "app", pb::Visibility::Private);
fake.repo_with("quiet", "hidden", pb::Visibility::Private);
fake.namespace("shown", true);
let b = console(fake).await;
let (status, body) = b.visit("/api/namespaces/acme/page").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert_eq!(repo_names(&body), vec!["open".to_owned()], "{body}");
let text = body.to_string();
assert!(!text.contains("\"app\""), "a private repository reached a visitor: {text}");
assert!(!text.contains("private"), "the page says something about what is private: {text}");
let (quiet, quiet_body) = b.visit("/api/namespaces/quiet/page").await;
let (ghost, ghost_body) = b.visit("/api/namespaces/ghost/page").await;
assert_eq!(quiet, http::StatusCode::NOT_FOUND, "{quiet_body}");
assert_eq!(ghost, http::StatusCode::NOT_FOUND, "{ghost_body}");
assert_eq!(quiet_body, ghost_body, "a private namespace is told apart from a missing one");
assert_eq!(quiet_body, crate::api::absent::body());
let (status, body) = b.visit("/api/namespaces/shown/page").await;
assert_eq!(status, http::StatusCode::OK, "a public namespace is shown: {body}");
assert!(repo_names(&body).is_empty(), "{body}");
}
// ── rule 1: the account's settings are its admin's, private by default ─────
/// ★ **The namespace's settings are the account admin's**: an operator who
/// holds admin on one repository is refused before anything is signed; the
/// account admin may. A viewer's namespace list is asked as the viewer, so a
/// private namespace is not in it until its admin makes it public.
#[tokio::test]
async fn the_namespace_settings_are_the_account_admins_and_a_viewer_lists_only_public_ones() {
let fake = Fake::default();
fake.repo("acme", "app");
fake.grant("kim", "acme", "app", pb::GrantLevel::Admin);
let b = console(fake).await;
let kim = b.session("kim", Role::Operator);
let acme = b.session("acme", Role::Viewer);
let vera = b.session("vera", Role::Viewer);
let listed = |body: &serde_json::Value| -> Vec<String> {
body["items"]
.as_array()
.map(|a| a.iter().filter_map(|n| n["name"].as_str().map(str::to_owned)).collect())
.unwrap_or_default()
};
let (status, body) = b.send("GET", "/api/namespaces", &vera, None).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(!listed(&body).contains(&"acme".to_owned()), "a private namespace listed to a viewer: {body}");
let settings = "/api/account/namespaces/acme/settings";
let (status, body) = b
.send("PATCH", settings, &kim, Some(serde_json::json!({ "public": true })))
.await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
assert!(
body["message"].as_str().unwrap_or_default().contains("account admin"),
"{body}"
);
assert!(b.fake.heard().is_empty(), "gunnar heard {:?}", b.fake.heard());
let (status, body) = b
.send("PATCH", settings, &acme, Some(serde_json::json!({ "public": true })))
.await;
assert_eq!(status, http::StatusCode::OK, "the account admin sets it: {body}");
assert_eq!(body["public"], true, "{body}");
assert_eq!(b.fake.heard(), vec!["settings acme public=true events=false".to_owned()]);
let (status, body) = b.send("GET", "/api/namespaces", &vera, None).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(listed(&body).contains(&"acme".to_owned()), "a public namespace is listed: {body}");
}
// ── rule 3: the account admin enrolls NEW employees ─────────────────────────
/// ★ **The account admin enrolls a NEW employee** — gunnar creates the
/// principal bound to the account, and gunnar mints the invite for it, asked
/// AS the account admin (thin layer L), so the employee's passkey lands on it.
/// Anybody else is refused before gunnar hears a thing; an existing principal
/// is refused by gunnar and no invite is minted for it.
#[tokio::test]
async fn the_account_admin_enrolls_a_new_employee_with_an_invite_pinned_to_it() {
let fake = Fake::default();
fake.repo("acme", "app");
fake.user("carol");
let b = console(fake).await;
let kim = b.session("kim", Role::Operator);
let acme = b.session("acme", Role::Viewer);
let enroll = |name: &str| Some(serde_json::json!({ "account": "acme", "name": name }));
let (status, body) = b.send("POST", "/api/account/employees", &kim, enroll("bob")).await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
assert!(b.fake.heard().is_empty(), "gunnar heard {:?}", b.fake.heard());
let (status, body) = b.send("POST", "/api/account/employees", &acme, enroll("bob")).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert_eq!(body["principal"]["id"], "bob", "{body}");
assert!(
body["url"].as_str().unwrap_or_default().contains("invite="),
"the employee's invite link comes back: {body}"
);
// ONE gunnar call: bob and his invite (EnrollEmployee{invite}, Add users).
assert_eq!(b.fake.heard(), vec!["enroll acme/bob, invited".to_owned()]);
assert!(body["url"].as_str().unwrap_or_default().contains("gunnars-secret-for-bob"), "{body}");
assert!(b.fake.invites.lock().expect("invites").is_empty(), "a second, separate invite call");
let (status, body) = b.send("POST", "/api/account/employees", &acme, enroll("carol")).await;
assert_ne!(status, http::StatusCode::OK, "an existing principal became an employee: {body}");
assert!(
b.fake.invites.lock().expect("invites").is_empty(),
"an invite was minted for a refused enrolment"
);
let (status, body) = b
.send("GET", "/api/account/employees?account=acme", &acme, None)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert_eq!(body["employees"][0]["id"], "bob", "{body}");
}
/// ★ **A reserved name is refused BEFORE gunnar enrolls it** (owner,
/// 2026-10-04). Red before this row: `Accounts.EnrollEmployee` was signed
/// first and the console's reserved-name rule ran only when the invite was
/// minted, so an account admin left a `console-<x>` principal at gunnar —
/// the name the reviewer provisioning used to adopt and grant server-wide
/// Write. gunnar must hear nothing, and no invite is minted.
#[tokio::test]
async fn a_reserved_name_is_refused_before_gunnar_enrolls_it() {
let fake = Fake::default();
fake.repo("acme", "app");
let b = console(fake).await;
let acme = b.session("acme", Role::Viewer);
for name in ["console-x", "gunnar-ui"] {
let (status, body) = b
.send(
"POST",
"/api/account/employees",
&acme,
Some(serde_json::json!({ "account": "acme", "name": name })),
)
.await;
assert_ne!(status, http::StatusCode::OK, "{name}: {body}");
assert!(
b.fake.heard().is_empty(),
"{name}: gunnar enrolled a reserved name: {:?}",
b.fake.heard()
);
}
assert!(
b.fake.invites.lock().expect("invites").is_empty(),
"an invite was minted for a reserved name"
);
}
/// ★ **The account admin's decisions reach gunnar signed AS THE PERSON**
/// (thin layer, owner 2026-10-04) — never with the console's key — and a
/// person who is not the account admin is refused BY GUNNAR, in gunnar's
/// words. Red before this row: the console decided with its own copy of the
/// rule (`visibility::require_may_set`, `account_admin::require_account_admin`)
/// and then signed with its server-wide key, so gunnar heard every one of these
/// from `console` and could refuse none of them.
#[tokio::test]
async fn the_account_admins_decisions_are_signed_by_the_person_and_gunnar_decides() {
let fake = Fake::default();
fake.repo("acme", "app");
fake.repo("kim", "notes");
fake.grant("kim", "acme", "app", pb::GrantLevel::Admin);
let b = console(fake).await;
let acme = b.session("acme", Role::Viewer);
let kim = b.session("kim", Role::Operator);
// The account admin (a viewer: a paying customer) — settings and employees.
let (status, body) = b
.send(
"PATCH",
"/api/account/namespaces/acme/settings",
&acme,
Some(serde_json::json!({ "events_public": true })),
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let (status, body) = b
.send(
"POST",
"/api/account/employees",
&acme,
Some(serde_json::json!({ "account": "acme", "name": "bob" })),
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let (status, body) = b
.send("GET", "/api/account/employees?account=acme", &acme, None)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
// An operator: their own account's repository, and someone else's.
let (status, body) = b.set(&kim, "kim/notes", "public").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let (status, body) = b.set(&kim, "acme/app", "public").await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
assert!(
body["message"].as_str().unwrap_or_default().contains("account admin"),
"gunnar's refusal reaches the page: {body}"
);
let (status, body) = b
.send(
"PATCH",
"/api/account/namespaces/acme/settings",
&kim,
Some(serde_json::json!({ "public": true })),
)
.await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
assert_eq!(
b.fake.asked(),
vec![
"Repos.SetNamespaceSettings by acme".to_owned(),
"Accounts.EnrollEmployee by acme".to_owned(),
"Accounts.ListEmployees by acme".to_owned(),
"Repos.SetVisibility by kim".to_owned(),
"Repos.SetVisibility by kim".to_owned(),
"Repos.SetNamespaceSettings by kim".to_owned(),
],
"every account-admin call is signed by the person, and the refusals are gunnar's"
);
assert!(
!b.fake.heard().iter().any(|h| h.starts_with("acme/app") || h.contains("public=true")),
"a refused call changed something: {:?}",
b.fake.heard()
);
}
// ── "Add users" (owner, 2026-10-04): one press, one server call ────────────
/// ★ **More than one user is that many SEATS, with no names and no invite** —
/// sent as typed (the room in GiB becomes bytes, nothing else is decided
/// here), signed as the person, and gunnar's answer is what comes back.
#[tokio::test]
async fn the_account_admin_adds_ten_seats_with_no_names_and_no_invite() {
let fake = Fake::default();
fake.repo("acme", "app");
let b = console(fake).await;
let acme = b.session("acme", Role::Viewer);
let kim = b.session("kim", Role::Operator);
let ten = Some(serde_json::json!({ "account": "acme", "count": 10, "room_gib": 20 }));
let (status, body) = b.send("POST", "/api/account/employees", &kim, ten.clone()).await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
assert!(b.fake.heard().is_empty(), "gunnar granted {:?}", b.fake.heard());
let (status, body) = b.send("POST", "/api/account/employees", &acme, ten).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert_eq!((body["seats"].as_u64(), body["seats_open"].as_u64()), (Some(10), Some(10)), "{body}");
assert!(body["principal"].is_null(), "seats name nobody: {body}");
assert_eq!(body["url"], "", "an invite for nobody: {body}");
assert_eq!(b.fake.heard(), vec![format!("enroll acme seats 10 room {}", 20u64 << 30)]);
assert!(b.fake.invites.lock().expect("invites").is_empty(), "an invite was minted for a seat");
}
/// ★ **A bought seat is filled from the console** (2026-10-04, the circle
/// analysis: nothing filled a seat): the account admin names the NEW user,
/// gunnar takes a seat for them and makes their invite in the SAME call, and
/// the console hands the link back. A seat claim names a room nobody types.
#[tokio::test]
async fn the_account_admin_fills_a_bought_seat_with_a_new_user_and_their_invite() {
let fake = Fake::default();
fake.repo("acme", "app");
let b = console(fake).await;
let acme = b.session("acme", Role::Viewer);
let fill = Some(serde_json::json!({ "account": "acme", "name": "sam", "from_seat": true }));
let (status, body) = b.send("POST", "/api/account/employees", &acme, fill).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert_eq!(body["principal"]["id"], "sam", "{body}");
assert!(body["url"].as_str().unwrap_or_default().contains("invite="), "no invite link: {body}");
assert_eq!(b.fake.heard(), vec!["enroll acme/sam from a seat, invited".to_owned()], "not ONE gunnar call");
assert!(b.fake.invites.lock().expect("invites").is_empty(), "a second, separate invite call");
}
// ───────────────────────────────────────────────────────────────────────────
// New repository: the account admin's (owner, 2026-10-06)
// ───────────────────────────────────────────────────────────────────────────
/// A `POST /api/repos` as `cookie`.
async fn new_repo(b: &Rig, cookie: &str, body: serde_json::Value) -> (http::StatusCode, serde_json::Value) {
b.send("POST", "/api/repos", cookie, Some(body)).await
}
/// ★ **An account admin on a VIEWER session creates a repository in their own
/// account and in one they administer — gunnar decides, on the person's key.**
/// Red before: the console's role floor refused every viewer's `POST
/// /api/repos` with 403 before gunnar was asked, so a customer could create a
/// repository only by pushing one into existence.
#[tokio::test]
async fn an_account_admin_on_a_viewer_session_creates_a_repository_and_gunnar_decides() {
let fake = Fake::default();
fake.namespace("kim", false);
fake.namespace("acme", false);
fake.grant("kim", "acme", "", pb::GrantLevel::Admin);
let b = console(fake).await;
let kim = b.session("kim", Role::Viewer);
for (ns, visibility) in [("kim", "private"), ("kim", "public"), ("acme", "private")] {
let name = format!("made-{visibility}");
let (status, body) = new_repo(&b, &kim, serde_json::json!({ "namespace": ns, "name": name, "visibility": visibility })).await;
assert_eq!(status, http::StatusCode::OK, "{ns} {visibility}: {body}");
}
assert_eq!(
b.fake.heard(),
vec!["created kim/made-private private", "created kim/made-public public", "created acme/made-private private"],
"gunnar made the three"
);
assert!(b.fake.asked().iter().all(|a| a == "Repos.Create by kim"), "every create signed as kim: {:?}", b.fake.asked());
}
/// ★ **A writer, a reader, and another account's namespace are refused — by
/// gunnar, on the person's key, and nothing is made.**
#[tokio::test]
async fn a_writer_a_reader_and_another_account_get_gunnars_refusal_and_nothing_is_made() {
let fake = Fake::default();
fake.namespace("acme", false);
fake.namespace("beta", false);
fake.grant("wendy", "acme", "", pb::GrantLevel::Write);
fake.grant("vic", "acme", "", pb::GrantLevel::Read);
let b = console(fake).await;
for (who, ns) in [("wendy", "acme"), ("vic", "acme"), ("beta", "acme"), ("wendy", "beta")] {
let cookie = b.session(who, Role::Viewer);
let (status, body) = new_repo(&b, &cookie, serde_json::json!({ "namespace": ns, "name": "intruder" })).await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{who} in {ns}: {body}");
assert!(body.to_string().contains("admin"), "gunnar's words reach the person: {body}");
}
assert!(b.fake.heard().is_empty(), "a refused create made {:?}", b.fake.heard());
assert_eq!(
b.fake.asked(),
vec!["Repos.Create by wendy", "Repos.Create by vic", "Repos.Create by beta", "Repos.Create by wendy"],
"every refusal is gunnar's, asked as the person — not a wall the console put up"
);
}
/// ★ **The encrypted choice** marks the new repository encrypted — the
/// existing `Repos.SetEncrypted`, signed as the same person, right after the
/// create.
#[tokio::test]
async fn the_encrypted_choice_marks_the_new_repository_encrypted_as_the_person() {
let fake = Fake::default();
fake.namespace("kim", false);
let b = console(fake).await;
let kim = b.session("kim", Role::Viewer);
let (status, body) = new_repo(&b, &kim, serde_json::json!({ "namespace": "kim", "name": "vault", "encrypted": true })).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert_eq!(body["encrypted"], true, "{body}");
assert_eq!(b.fake.heard(), vec!["created kim/vault private", "kim/vault -> encrypted"]);
assert_eq!(b.fake.asked(), vec!["Repos.Create by kim", "Repos.SetEncrypted by kim"]);
}
/// ★ **Where the button may create**, in gunnar's words: the namespaces this
/// session's person is the account admin of (their own, and one they hold
/// Admin over — asked of gunnar, `Access.Check`), each one publishable; and an
/// operator's session keeps the free namespace field it has today.
#[tokio::test]
async fn the_new_repository_button_offers_the_accounts_gunnar_says_the_person_administers() {
let fake = Fake::default();
fake.namespace("kim", false);
fake.namespace("acme", false);
fake.namespace("beta", false);
fake.grant("kim", "acme", "", pb::GrantLevel::Admin);
fake.grant("kim", "beta", "", pb::GrantLevel::Write);
let b = console(fake).await;
let kim = b.session("kim", Role::Viewer);
let (status, body) = b.send("GET", "/api/repos/creatable", &kim, None).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert_eq!(body["any_namespace"], false, "{body}");
let offered: Vec<&str> = body["namespaces"].as_array().expect("namespaces").iter().filter_map(|n| n["name"].as_str()).collect();
assert_eq!(offered, vec!["acme", "kim"], "kim's own and acme, not beta (write only): {body}");
assert!(body["namespaces"].as_array().expect("namespaces").iter().all(|n| n["public"] == true), "{body}");
let wendy = b.session("wendy", Role::Viewer);
let (_, body) = b.send("GET", "/api/repos/creatable", &wendy, None).await;
assert_eq!(body["namespaces"].as_array().map(|a| a.iter().filter_map(|n| n["name"].as_str()).collect::<Vec<_>>()), Some(vec!["wendy"]), "{body}");
let boss = b.session("boss", Role::Operator);
let (_, body) = b.send("GET", "/api/repos/creatable", &boss, None).await;
assert_eq!(body["any_namespace"], true, "an operator keeps the free namespace field: {body}");
}
// ── members (owner 2026-10-07: a customer is an ACCOUNT that pays; MEMBERS
// use its space, never buy, and get Write from its admin) ─────────────────
/// acme's account (its admin `acme`, the repository `acme/site`, the member
/// `ben` it enrolled) and `kim`, an individual with an account of her own.
async fn members_world() -> Rig {
let fake = Fake::default();
fake.repo("acme", "site");
fake.namespace("kim", false);
fake.user("ben");
fake.employees.lock().expect("employees").insert("ben".into(), "acme".into());
fake.grant("ben", "acme", "", pb::GrantLevel::Read);
console(fake).await
}
/// ★ **A member is told whose space it is, and is shown no shop** —
/// `/auth/status` carries gunnar's word (`Access.WhoAmI`): the account, what
/// the person is to it, and the one line a member reads where the shop would
/// be. The account's admin and an individual may buy; a member may not.
#[tokio::test]
async fn a_member_is_told_whose_space_it_is_and_is_shown_no_shop() {
let b = members_world().await;
let ben = b.session("ben", Role::Viewer);
let acme = b.session("acme", Role::Viewer);
let kim = b.session("kim", Role::Viewer);
let (status, me) = b.send("GET", "/auth/status", &ben, None).await;
assert_eq!(status, http::StatusCode::OK, "{me}");
assert_eq!((me["account"].as_str(), me["account_role"].as_str(), me["account_admin"].as_str()), (Some("acme"), Some("employee"), Some("acme")), "{me}");
assert_eq!(me["account_line"], "Your space comes from acme. Ask acme for more room.", "{me}");
#[cfg(feature = "metered")]
{
assert_eq!(me["may_buy"], false, "a member never reaches the shop: {me}");
assert!(me["buy_url"].is_null() && me["private_url"].is_null(), "no door for a member: {me}");
}
for (who, cookie, role) in [("acme", &acme, "admin"), ("kim", &kim, "individual")] {
let (_, me) = b.send("GET", "/auth/status", cookie, None).await;
assert_eq!((me["account"].as_str(), me["account_role"].as_str()), (Some(who), Some(role)), "{me}");
assert!(me["account_line"].is_null(), "{who} is no member: {me}");
#[cfg(feature = "metered")]
assert_eq!(me["may_buy"], true, "{who} buys for its own account: {me}");
}
}
/// ★ **An appointed account admin administers the account's repositories and never buys**
/// (owner 2026-10-08, round 5.3.1). gunnar answers WhoAmI `admin-appointed` for a member the
/// account's admin gave Admin over the account: he publishes and encrypts the account's
/// repositories (gunnar's grant decides, asked as him), reads the account's room read only,
/// and is shown no shop — no buy, private or sign-up door, the members line naming the
/// account, and every purchase door and the sign-up checkout refuse him.
#[tokio::test]
async fn an_appointed_admin_administers_the_accounts_repositories_and_never_buys() {
let fake = Fake::default();
fake.repo("acme", "site");
fake.user("dan");
fake.employees.lock().expect("employees").insert("dan".into(), "acme".into());
fake.appointed.lock().expect("appointed").insert("dan".into());
fake.grant("dan", "acme", "", pb::GrantLevel::Admin);
#[cfg(feature = "metered")]
let (monetize, calls) = {
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 shop = crate::auth::signup::tests::FakeMonetize { checkout: Some("https://pay.example/c/cs_1".into()), ..Default::default() };
let calls = shop.calls.clone();
tokio::spawn(async move {
tonic::transport::Server::builder()
.add_service(crate::mpb::signup_server::SignupServer::new(shop))
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
.await
});
(Some(format!("http://{addr}")), calls)
};
#[cfg(not(feature = "metered"))]
let monetize = None;
let b = console_selling(fake, monetize).await;
let dan = b.session("dan", Role::Viewer);
let (status, me) = b.send("GET", "/auth/status", &dan, None).await;
assert_eq!(status, http::StatusCode::OK, "{me}");
assert_eq!((me["account"].as_str(), me["account_role"].as_str()), (Some("acme"), Some("admin-appointed")), "{me}");
assert_eq!(me["account_line"], "Your space comes from acme. Ask acme for more room.", "the members line names the account: {me}");
#[cfg(feature = "metered")]
{
assert_eq!(me["may_buy"], false, "an appointed admin never reaches the shop: {me}");
for door in ["buy_url", "private_url", "signup_url"] {
assert!(me[door].is_null(), "{door} for an appointed admin: {me}");
}
}
// The account's repository decisions are his: gunnar's grant, asked as him.
let authority = b.authority(&dan, "acme/site").await;
assert_eq!(authority["may_change"], true, "{authority}");
let (status, body) = b.set(&dan, "acme/site", "public").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let (status, body) = b.send("PATCH", "/api/repos/acme/site", &dan, Some(serde_json::json!({ "encrypted": true }))).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
// The account's room, read only: never a plan of his own.
let (status, room) = b.send("GET", "/api/account/room", &dan, None).await;
assert_eq!(status, http::StatusCode::OK, "{room}");
assert_eq!((room["namespace"].as_str(), room["read_only"].as_bool()), (Some("acme"), Some(true)), "{room}");
#[cfg(feature = "metered")]
{
let (status, body) = b.send("POST", "/api/account/subscription/buy-bundle", &dan, Some(serde_json::json!({ "bundle": "gunnar/10gb-1m" }))).await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
let order = serde_json::json!({ "username": "danco", "email": "dan@example.com", "plan": "gunnar/10gb-1m" });
let (status, body) = b.send("POST", "/auth/signup/start", &dan, Some(order)).await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
assert!(crate::auth::signup::tests::asks(&calls).is_empty(), "monetize was asked for an appointed admin");
}
}
/// ★ **A member is refused the sign-up checkout by the SERVER, not only by the page**
/// (wiring tracer B 2026-10-08). `/signup` draws no shop for a member (the page reads
/// `account_role`), but `POST /auth/signup/start` read no session: a signed-in member got a
/// checkout for a NEW account straight from the console. The member is told whom to ask, and
/// monetize hears nothing; the account's admin still reaches it.
#[cfg(feature = "metered")]
#[tokio::test]
async fn a_member_is_refused_the_signup_checkout_by_the_server_and_monetize_hears_nothing() {
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 shop = crate::auth::signup::tests::FakeMonetize { checkout: Some("https://pay.example/c/cs_1".into()), ..Default::default() };
let calls = shop.calls.clone();
tokio::spawn(async move {
tonic::transport::Server::builder()
.add_service(crate::mpb::signup_server::SignupServer::new(shop))
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
.await
});
let fake = Fake::default();
fake.repo("acme", "site");
fake.user("ben");
fake.employees.lock().expect("employees").insert("ben".into(), "acme".into());
let b = console_selling(fake, Some(format!("http://{addr}"))).await;
let ben = b.session("ben", Role::Viewer);
let order = serde_json::json!({ "username": "benco", "email": "ben@example.com", "plan": "gunnar/10gb-1m" });
let (status, body) = b.send("POST", "/auth/signup/start", &ben, Some(order.clone())).await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "a member reached the checkout: {body}");
assert!(body.to_string().contains("Ask acme"), "the member is told whom to ask: {body}");
assert!(crate::auth::signup::tests::asks(&calls).is_empty(), "monetize was asked for a member: {:?}", calls.lock().expect("the call log"));
let acme = b.session("acme", Role::Viewer);
let (status, body) = b.send("POST", "/auth/signup/start", &acme, Some(order)).await;
assert_eq!(status, http::StatusCode::OK, "the account's admin is no member: {body}");
}
/// ★ **Every purchase door refuses a member on the server** (wiring tracer B 2026-10-08:
/// the UI test probed buy-bundle only). Renew, buy, buy-bundle, cancel, the bundles read and
/// Add users (`POST /api/account/employees`, a seat purchase) — a member posting straight at
/// them, past every hidden button, is refused, and monetize hears nothing.
#[cfg(feature = "metered")]
#[tokio::test]
async fn every_purchase_door_refuses_a_member_posting_past_the_page() {
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 shop = crate::auth::signup::tests::FakeMonetize::default();
let calls = shop.calls.clone();
tokio::spawn(async move {
tonic::transport::Server::builder()
.add_service(crate::mpb::signup_server::SignupServer::new(shop))
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
.await
});
let fake = Fake::default();
fake.repo("acme", "site");
fake.user("ben");
fake.employees.lock().expect("employees").insert("ben".into(), "acme".into());
let b = console_selling(fake, Some(format!("http://{addr}"))).await;
let ben = b.session("ben", Role::Viewer);
for (method, path, body) in [
("POST", "/api/account/subscription/renew", serde_json::json!({})),
("POST", "/api/account/subscription/buy-bundle", serde_json::json!({ "bundle": "gunnar/10gb-1m" })),
("POST", "/api/account/subscription/cancel", serde_json::json!({ "reference": "gunnar/acme/2026-10-08" })),
("GET", "/api/account/subscription/bundles", serde_json::Value::Null),
("POST", "/api/account/employees", serde_json::json!({ "count": 5 })),
] {
let (status, answer) = b.send(method, path, &ben, (!body.is_null()).then_some(body)).await;
if path.starts_with("/api/account/subscription") {
// The console's own wall (OwnSubscription), before any ticket or quote.
assert_eq!(status, http::StatusCode::FORBIDDEN, "{method} {path} answered a member: {answer}");
assert!(answer.to_string().contains("Ask acme"), "{method} {path}: the member is told whom to ask: {answer}");
} else {
// Add users: gunnar's (the account admin's door), never a success.
assert!(status.is_client_error() || status.is_server_error(), "{method} {path} answered a member {status}: {answer}");
assert_ne!(status, http::StatusCode::NOT_FOUND, "{method} {path} is mounted on this selling console: {answer}");
}
}
assert!(crate::auth::signup::tests::asks(&calls).is_empty(), "monetize was asked for a member: {:?}", calls.lock().expect("the call log"));
}
/// ★ **A member's storage is the ACCOUNT's room, read only** — the same
/// `Usage.Read`, asked for the account as the member; never a namespace of
/// his own (the walkthrough's "namespace ben is not in the catalog").
#[tokio::test]
async fn a_members_storage_is_the_accounts_room_read_only() {
let b = members_world().await;
let ben = b.session("ben", Role::Viewer);
let (status, room) = b.send("GET", "/api/account/room", &ben, None).await;
assert_eq!(status, http::StatusCode::OK, "{room}");
assert_eq!((room["namespace"].as_str(), room["read_only"].as_bool()), (Some("acme"), Some(true)), "{room}");
assert!(b.fake.asked().contains(&"Usage.Read acme by ben".to_owned()), "asked as ben, for acme: {:?}", b.fake.asked());
let acme = b.session("acme", Role::Viewer);
let (status, room) = b.send("GET", "/api/account/room", &acme, None).await;
assert_eq!(status, http::StatusCode::OK, "{room}");
assert_eq!(room["namespace"], "acme");
assert!(room.get("read_only").is_none(), "the admin's own room is no member's view: {room}");
}
/// ★ **The account's admin gives a member Write on one repository, sees it,
/// and takes it away; the member cannot** — every call signed as the person,
/// gunnar deciding (the fake keeps gunnar's rule: Admin over the namespace).
#[tokio::test]
async fn the_account_admin_gives_a_member_write_and_takes_it_away_and_a_member_cannot() {
let b = members_world().await;
let acme = b.session("acme", Role::Viewer);
let ben = b.session("ben", Role::Viewer);
let members = |cookie: String| {
let b = &b;
async move { b.send("GET", "/api/repos/acme/site/members", &cookie, None).await }
};
let (status, table) = members(acme.clone()).await;
assert_eq!(status, http::StatusCode::OK, "{table}");
assert_eq!(table["members"][0]["id"], "ben", "{table}");
assert_eq!(table["members"][0]["account_level"], "read", "enrolment's Read on the account: {table}");
assert!(table["members"][0]["repo_level"].is_null(), "{table}");
// The member may neither read the table nor give himself Write.
let (status, _) = members(ben.clone()).await;
assert_eq!(status, http::StatusCode::FORBIDDEN);
let (status, body) = b.send("PUT", "/api/repos/acme/site/members/ben", &ben, Some(serde_json::json!({ "level": "write" }))).await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "gunnar refuses the member: {body}");
assert!(b.fake.heard().is_empty(), "gunnar changed nothing for a member: {:?}", b.fake.heard());
assert!(b.fake.asked().contains(&"Access.Grant by ben".to_owned()), "signed as ben, refused by gunnar: {:?}", b.fake.asked());
let (status, body) = b.send("PUT", "/api/repos/acme/site/members/ben", &acme, Some(serde_json::json!({ "level": "write" }))).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert_eq!(b.fake.heard(), vec!["grant ben GRANT_LEVEL_WRITE on acme/site".to_owned()]);
assert!(b.fake.asked().contains(&"Access.Grant by acme".to_owned()), "signed as the account's admin: {:?}", b.fake.asked());
let (_, table) = members(acme.clone()).await;
assert_eq!(table["members"][0]["repo_level"], "write", "{table}");
let (status, body) = b.send("DELETE", "/api/repos/acme/site/members/ben", &acme, None).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert_eq!(b.fake.heard().last().map(String::as_str), Some("revoke ben on acme/site"));
let (_, table) = members(acme).await;
assert!(table["members"][0]["repo_level"].is_null(), "back to the account's Read: {table}");
let (status, body) = b.send("PUT", "/api/repos/acme/site/members/ben", &b.session("kim", Role::Viewer), Some(serde_json::json!({ "level": "admin" }))).await;
assert_eq!(status, http::StatusCode::BAD_REQUEST, "a member is given Read or Write, nothing else: {body}");
}
/// ★ **The account admin's role follows their first member at once** (the
/// UI test of 2026-10-07 read acme as "individual" right after Add users: the
/// console believed gunnar's earlier answer for its TTL). Enrolling a member
/// forgets what this console believed about the admin, so the next
/// `/auth/status` asks gunnar again.
#[tokio::test]
async fn the_account_admins_role_follows_their_first_member_at_once() {
let fake = Fake::default();
fake.repo("acme", "site");
let b = console(fake).await;
let acme = b.session("acme", Role::Viewer);
let (_, me) = b.send("GET", "/auth/status", &acme, None).await;
assert_eq!(me["account_role"], "individual", "no member yet: {me}");
let (status, body) = b
.send("POST", "/api/account/employees", &acme, Some(serde_json::json!({ "account": "acme", "name": "ben" })))
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let (_, me) = b.send("GET", "/auth/status", &acme, None).await;
assert_eq!(me["account_role"], "admin", "acme has a member now: {me}");
}