crates/gunnar-grpc/tests/billing.rs
//! **`Billing`: gunnar enforces the tenant scope itself, and these are the
//! tests that say so.**
//!
//! # The one line this whole file exists for
//!
//! A customer must be able to renew their own subscription from the console.
//! The console (`gunnar-ui`'s BFF) holds a SERVER-WIDE admin grant on this
//! appliance, because it renders operator screens. So if the console alone
//! decided "this browser session may only touch namespace X", any routing bug
//! in the console — one namespace read off the wrong request, one cached
//! session — would be full administrative reach over every customer on the box.
//!
//! The console therefore does not decide. It presents the HUMAN's own
//! `gunnar-sshsig-v2` credential, signed with the ed25519 key `gunnar-ui`
//! enrolled on that person's own gunnar principal, and gunnar performs the
//! grant check itself with the model it already has:
//!
//! ```text
//! require(&caller, Access::namespace(&ns), GrantLevel::Admin, "Billing.MintActorTicket")
//! -> Identity::may -> AuthTable::may -> owns_account (a principal's id IS
//! the namespace it owns) or a Scope::Namespace grant
//! ```
//!
//! Every test below drives the real gate: the caller is produced by
//! `AuthGate::authenticate` over a real signature from a really-enrolled key,
//! so nothing here invents an identity the server would not have accepted.
//!
//! # What the double is allowed to stand in for, and what it is not
//!
//! `MemoryControlPlane` stands in for the STORE. It does not stand in for the
//! decision: its `may` is `AuthTable::may`'s shape, `owns_account` included,
//! and `tests/bootstrap.rs` asserts both directions of that so the double
//! cannot quietly allow more than the real table would. Nor does it stand in
//! for the signature: it holds a real ed25519 key and
//! [`the_ticket_a_tenant_gets_verifies_under_the_key_the_appliance_advertises`]
//! checks what it produced with `monetize_embed::verify_ticket`, which is the
//! verifier the OTHER repository runs. That test is the cross-repo contract,
//! executed rather than described.
use std::sync::Arc;
use gunnar_grpc::{
sign_credential_for, AuthGate, AuthMode, BillingService, Caller, ControlPlane, EventBus,
MemoryControlPlane, ServerConfig, ServiceContext, CREDENTIAL_METADATA_KEY,
};
use gunnar_proto::billing_server::Billing as _;
use gunnar_proto::{
ApplianceIdentityRequest, EntitlementFact, EntitlementState, GrantLevel,
MintActorTicketRequest, PrincipalKind,
};
use monetize_embed::ticket::{
ActorTicket, MAX_LIFE_MS, PURPOSE_ORDER, PURPOSE_READ, PURPOSE_RENEW,
};
use tonic::{Code, Request};
/// **The map type `verify_ticket` compares**: `ActorTicket::caps` is a
/// `BTreeMap` because the canonical JSON that gets signed sorts keys anyway, so
/// the ordered map makes the Rust value and the signed bytes agree by
/// construction. Aliased here so a test cannot quietly introduce a second one.
type Caps = std::collections::BTreeMap<String, u64>;
const ALICE: &str = "alice";
const BETA: &str = "beta";
fn now_ms() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("after 1970")
.as_millis() as i64
}
fn mint_key() -> ssh_key::PrivateKey {
ssh_key::PrivateKey::random(
&mut rand::rand_core::UnwrapErr(rand::rngs::SysRng),
ssh_key::Algorithm::Ed25519,
)
.expect("mint an ed25519 key")
}
struct Harness {
plane: Arc<MemoryControlPlane>,
ctx: ServiceContext,
}
impl Harness {
/// Two namespaces and no grants. Every test says out loud who holds what,
/// so a test that forgets gets a refusal rather than an accident.
fn new() -> Harness {
let plane = Arc::new(MemoryControlPlane::new());
plane
.create_namespace(ALICE, "alice's own")
.expect("namespace alice");
plane
.create_namespace(BETA, "somebody else entirely")
.expect("namespace beta");
let control: Arc<dyn ControlPlane> = plane.clone();
let ctx = ServiceContext::new(
control,
Arc::new(EventBus::new(16)),
ServerConfig::default(),
);
assert_eq!(
ctx.config.auth,
AuthMode::Required,
"the fixture must run the SHIPPED default; a suite that quietly ran the \
allow-all escape hatch would prove nothing about a grant check"
);
Harness { plane, ctx }
}
/// Enrol a principal whose id IS `name`, with a freshly minted key — the
/// shape `gunnar-ui`'s `TenantKeys` produces: one ed25519 key per console
/// user, on that person's own gunnar principal.
fn enrol(&self, name: &str) -> ssh_key::PrivateKey {
let key = mint_key();
let principal = self
.plane
.add_principal(name, name, PrincipalKind::User)
.expect("principal added");
assert_eq!(
principal.id, name,
"the whole scope check rests on a principal's id being the namespace it owns"
);
let openssh = key.public_key().to_openssh().expect("render public key");
self.plane
.add_key(&principal.id, &openssh, name, None, "", None)
.expect("key registered");
key
}
/// The caller the REAL gate produces for this key, over a real signature.
fn caller_for(&self, key: &ssh_key::PrivateKey, nonce: &str) -> Caller {
let credential = sign_credential_for(key, now_ms(), nonce, "Admin.Health").expect("sign");
let mut request = Request::new(());
request.metadata_mut().insert(
CREDENTIAL_METADATA_KEY,
credential.to_header_value().parse().expect("ascii"),
);
AuthGate::new(Arc::clone(&self.ctx.control), self.ctx.config.auth)
.authenticate_bound(request.metadata(), now_ms(), Some("Admin.Health"))
.expect("a freshly signed credential from an enrolled key must verify")
}
fn authed<T>(&self, caller: &Caller, message: T) -> Request<T> {
let mut request = Request::new(message);
request.extensions_mut().insert(caller.clone());
request
}
fn billing(&self) -> BillingService {
BillingService::new(self.ctx.clone())
}
}
fn renew(namespace: &str) -> MintActorTicketRequest {
MintActorTicketRequest {
namespace: namespace.to_owned(),
purpose: PURPOSE_RENEW.to_owned(),
caps: no_caps(),
}
}
/// An order for one namespace, naming [`ten_gib`] — the caps a real console
/// would put on the button the customer pressed.
fn order(namespace: &str) -> MintActorTicketRequest {
MintActorTicketRequest {
namespace: namespace.to_owned(),
purpose: PURPOSE_ORDER.to_owned(),
caps: ten_gib(),
}
}
/// **The caps a renewal names: none.**
///
/// A function and not a constant because `verify_ticket` takes a reference to
/// the map the ORDER names, and for a renewal that map is empty on both sides —
/// the emptiness is the assertion, not an argument nobody looked at.
fn no_caps() -> Caps {
Caps::new()
}
/// The caps an order names: one meter, at a number no other fixture in this
/// file uses, so a cap that arrived from the wrong place is visible rather
/// than coincidentally equal to the right one.
fn ten_gib() -> Caps {
Caps::from([("pack_bytes".to_owned(), 10_737_418_240u64)])
}
/// The appliance's public key, as `ApplianceIdentity` advertises it, fetched
/// through an operator caller — which is the only caller that may.
///
/// As `monetize_embed::VerifyingKey`, the type `verify_ticket` takes: since the
/// merged line's RustCrypto bump this crate links ed25519-dalek 3 while the
/// published monetize-embed it resolves is on 2, and the target stopped
/// compiling on that mismatch. The verifier's own key type is the right one
/// to build here anyway — it is the function monetize calls.
async fn advertised_key(harness: &Harness) -> monetize_embed::VerifyingKey {
let operator = harness.enrol("operator");
harness.plane.grant_server("operator", GrantLevel::Admin);
let caller = harness.caller_for(&operator, "nonce-operator-identity-1");
let identity = harness
.billing()
.appliance_identity(harness.authed(&caller, ApplianceIdentityRequest {}))
.await
.expect("a server admin may read the appliance's public key")
.into_inner();
assert_eq!(identity.issuer, "gunnar");
assert_eq!(identity.public_key_hex.len(), 64);
let mut raw = [0u8; 32];
for (slot, pair) in raw
.iter_mut()
.zip(identity.public_key_hex.as_bytes().chunks(2))
{
*slot = u8::from_str_radix(std::str::from_utf8(pair).expect("ascii"), 16)
.expect("hex digit pair");
}
monetize_embed::VerifyingKey::from_bytes(&raw).expect("the advertised key parses")
}
// ───────────────────────────────────────────────────────────────────────────
// ★ The one that matters
// ───────────────────────────────────────────────────────────────────────────
/// ★ **A caller may not mint a ticket for a namespace they do not hold.**
///
/// This is the single most important assertion in the change. `alice` is a
/// real, enrolled, fully authenticated caller — nothing about her credential is
/// wrong — and she asks for a ticket naming `beta`. The refusal does not come
/// from the console, which is exactly the point: it comes from
/// `ServiceContext::require` asking `Identity::may` about
/// `Access::Namespace("beta")`, the same call `Repos.Delete` makes.
///
/// **And nothing is minted.** The second half is not decoration. A handler that
/// signed first and checked afterwards would satisfy a test that only looked at
/// the returned `Status`, and would have put a valid, signed, spendable ticket
/// for another tenant through whatever sits between here and the caller — a
/// log line, a trace, a buffered response. The appliance's key is asked to sign
/// exactly zero times.
/// A read for one namespace: no caps, because a read buys nothing.
fn read(namespace: &str) -> MintActorTicketRequest {
MintActorTicketRequest {
namespace: namespace.to_owned(),
purpose: PURPOSE_READ.to_owned(),
caps: no_caps(),
}
}
/// ★ **A read ticket is minted, names no caps, and is NOT a renewal.**
///
/// The cross-repo contract for the third verb. It has a word of its own
/// because a ticket is single-use: gating a history read with a RENEWAL ticket
/// burned the one the customer would have renewed with, and made this
/// appliance vouch for a verb the caller was not performing.
#[tokio::test]
async fn a_read_ticket_is_minted_and_is_not_a_renewal() {
let harness = Harness::new();
let key = advertised_key(&harness).await;
let alice = harness.enrol(ALICE);
let caller = harness.caller_for(&alice, "nonce-alice-reads");
let minted = harness
.billing()
.mint_actor_ticket(harness.authed(&caller, read(ALICE)))
.await
.expect("a namespace owner may mint a read ticket for their own namespace")
.into_inner();
let ticket = monetize_embed::verify_ticket(
&minted.ticket,
&key,
"gunnar",
ALICE,
PURPOSE_READ,
&no_caps(),
now_ms(),
)
.expect("the ticket this appliance minted must verify as a read");
assert!(ticket.caps.is_empty(), "a read ticket covers no amount");
assert!(
monetize_embed::verify_ticket(
&minted.ticket,
&key,
"gunnar",
ALICE,
PURPOSE_RENEW,
&no_caps(),
now_ms(),
)
.is_err(),
"a read ticket bought a period — the two verbs are not interchangeable, which is the \
whole reason the third one exists"
);
}
/// A read names no caps, and a request that sends some is refused by name
/// rather than quietly ignored.
#[tokio::test]
async fn a_read_that_names_an_amount_is_refused() {
let harness = Harness::new();
let alice = harness.enrol(ALICE);
let caller = harness.caller_for(&alice, "nonce-alice-reads-with-caps");
let asked = MintActorTicketRequest {
namespace: ALICE.to_owned(),
purpose: PURPOSE_READ.to_owned(),
caps: ten_gib(),
};
let refused = harness
.billing()
.mint_actor_ticket(harness.authed(&caller, asked))
.await
.expect_err("a read that names an amount is a client that has confused two paths");
assert_eq!(refused.code(), Code::InvalidArgument);
}
#[tokio::test]
async fn a_caller_may_not_mint_a_ticket_for_a_namespace_they_do_not_hold() {
let harness = Harness::new();
let alice = harness.enrol(ALICE);
let caller = harness.caller_for(&alice, "nonce-alice-asks-for-beta");
let before = harness.plane.actor_tickets_signed();
let refused = harness
.billing()
.mint_actor_ticket(harness.authed(&caller, renew(BETA)))
.await
.expect_err("alice must not mint a ticket naming beta");
assert_eq!(refused.code(), Code::PermissionDenied);
assert_eq!(
harness.plane.actor_tickets_signed(),
before,
"the appliance SIGNED something on a refused request. A ticket that exists is a \
ticket that can leak; the grant check must run before the key is touched"
);
// The same caller, her own namespace: the refusal above is about the
// namespace and not about alice, her key, or the gate being shut.
let caller = harness.caller_for(&alice, "nonce-alice-asks-for-alice");
harness
.billing()
.mint_actor_ticket(harness.authed(&caller, renew(ALICE)))
.await
.expect("alice holds alice");
}
// ───────────────────────────────────────────────────────────────────────────
// The happy path, and the cross-repo contract
// ───────────────────────────────────────────────────────────────────────────
/// **A tenant's own ticket parses, names them, and verifies under the key the
/// appliance advertises.**
///
/// This is the real contract test between the two repositories. The ticket is
/// minted here and checked with `monetize_embed::verify_ticket` — the function
/// monetize itself calls, taking the product, the tenant and the purpose from
/// what the ORDER says, exactly as `Purchase.Start` will. If gunnar's canonical
/// form and monetize's ever diverge, this goes red in gunnar's own test run
/// rather than in production on somebody's renewal page.
#[tokio::test]
async fn the_ticket_a_tenant_gets_verifies_under_the_key_the_appliance_advertises() {
let harness = Harness::new();
let key = advertised_key(&harness).await;
let alice = harness.enrol(ALICE);
let caller = harness.caller_for(&alice, "nonce-alice-happy-path-01");
let minted = harness
.billing()
.mint_actor_ticket(harness.authed(&caller, renew(ALICE)))
.await
.expect("alice may mint for alice")
.into_inner();
let ticket = monetize_embed::verify_ticket(
&minted.ticket,
&key,
"gunnar",
ALICE,
PURPOSE_RENEW,
&no_caps(),
now_ms(),
)
.expect("the appliance's own ticket must verify under the key it advertises");
assert_eq!(ticket.tenant, ALICE);
assert_eq!(ticket.purpose, PURPOSE_RENEW);
assert_eq!(ticket.issuer, "gunnar");
assert_eq!(ticket.product, "gunnar");
assert_eq!(
minted.expires_unix_ms, ticket.expires_unix_ms as u64,
"the expiry in the envelope must be the expiry in the signed claims, or a console \
reusing a ticket it thinks is fresh gets a refusal it cannot explain"
);
// ★ The other half of the enforcement, now from the VERIFIER's side: a
// genuine ticket, signed by this very appliance, spent on another tenant.
// Nothing about it is forged; what refuses it is that `verify_ticket`
// compares the signed tenant against the order's.
assert!(
monetize_embed::verify_ticket(
&minted.ticket,
&key,
"gunnar",
BETA,
PURPOSE_RENEW,
&no_caps(),
now_ms(),
)
.is_err(),
"alice's genuine ticket bought beta a period"
);
}
/// **Two tickets are two tickets**: the nonce is fresh each time, so a verifier
/// keeping a spent-nonce ring (`monetize_embed::ticket::SeenNonces`) admits the
/// second one. A constant nonce would make a customer's second renewal attempt
/// in ten minutes fail with "already spent", which is the most confusing
/// possible failure on a page whose whole job is to take money.
#[tokio::test]
async fn each_ticket_carries_its_own_nonce() {
let harness = Harness::new();
let alice = harness.enrol(ALICE);
let mut nonces = std::collections::BTreeSet::new();
for round in 0..4 {
let caller = harness.caller_for(&alice, &format!("nonce-alice-round-{round:04}"));
let minted = harness
.billing()
.mint_actor_ticket(harness.authed(&caller, renew(ALICE)))
.await
.expect("alice may mint for alice")
.into_inner();
let ticket: ActorTicket = serde_json::from_slice(&minted.ticket).expect("parses");
assert!(
!ticket.nonce.trim().is_empty() && ticket.nonce.len() <= 256,
"the verifier refuses an empty or oversized nonce outright: {:?}",
ticket.nonce
);
assert!(
nonces.insert(ticket.nonce.clone()),
"the appliance minted the same nonce twice ({}); a verifier with a spent-nonce \
ring would refuse the second renewal a customer attempted",
ticket.nonce
);
}
}
/// **A minted ticket lives well under the cap the VERIFIER enforces.**
///
/// `MAX_LIFE_MS` is ten minutes and it is monetize-embed's rule, not the
/// issuer's: `verify_ticket` refuses a longer one even when the signature is
/// perfect. A ticket is fetched inside the request that spends it, so its life
/// has to cover one round trip and a clock disagreement and nothing more —
/// minting anywhere near the cap would leave no room for either.
#[tokio::test]
async fn a_minted_ticket_lives_well_under_the_verifiers_cap() {
let harness = Harness::new();
let alice = harness.enrol(ALICE);
let caller = harness.caller_for(&alice, "nonce-alice-lifetime-001");
let minted = harness
.billing()
.mint_actor_ticket(harness.authed(&caller, renew(ALICE)))
.await
.expect("alice may mint for alice")
.into_inner();
let ticket: ActorTicket = serde_json::from_slice(&minted.ticket).expect("parses");
let life = ticket.expires_unix_ms - ticket.issued_unix_ms;
assert!(
life > 0,
"a ticket that expires before it is issued is refused"
);
assert!(
life * 4 <= MAX_LIFE_MS,
"a ticket lives {life} ms against a cap of {MAX_LIFE_MS} ms. Minting near the cap \
leaves no room for the clock skew the verifier also allows"
);
}
// ───────────────────────────────────────────────────────────────────────────
// ★ The load-bearing one: a lapsed tenant can still pay
// ───────────────────────────────────────────────────────────────────────────
/// ★ **A Suspended tenant can still mint a ticket, which is the only reason
/// self-service renewal works at all.**
///
/// A renewal is what a customer does when the box is full, suspended, or down.
/// A design in which the tenant must be paid-up before they can pay cannot
/// serve the case it exists for.
///
/// Today that holds because gunnar's entitlement gate is the SSH/git door and
/// ONLY the SSH/git door: `gunnar_ssh::PlanGate` is consulted by
/// `gunnar_ssh::Dispatch::authorise_service`, and the metered implementation
/// (`gunnar_server::entitlement::EntitlementSlot`) is wired in exactly twice,
/// both times through `gunnar_ssh::Control::with_plan_gate`. The gRPC control
/// plane has no equivalent — `gunnar-grpc`'s only interceptor is `AuthGate` —
/// so a Suspended namespace authenticates and is served here exactly as a Paid
/// one is.
///
/// **That is an accident of where the gate was put, and this test is what turns
/// it into a decision.** Nothing else in the tree connects "the gRPC plane has
/// no entitlement gate" to "a customer can renew". Somebody adding a plan check
/// to the gRPC plane — a reasonable-sounding thing to want — would take
/// self-service renewal away silently, and the first report would be a customer
/// who cannot pay and a box that will not let them.
///
/// The fact is seeded and then READ BACK through the very seam method the
/// box's own readers use (`ControlPlane::entitlement_get`, which is what
/// `Usage.Read` asks for a namespace's allowance), so the test cannot pass
/// because the suspension never landed.
#[tokio::test]
async fn a_lapsed_tenant_can_still_mint_a_ticket() {
let harness = Harness::new();
let key = advertised_key(&harness).await;
let alice = harness.enrol(ALICE);
harness.plane.set_entitlement_fact(EntitlementFact {
namespace: ALICE.to_owned(),
plan: "small".to_owned(),
state: EntitlementState::Suspended as i32,
paid_until_unix_ms: Some(1),
caps: [("pack_bytes".to_owned(), 7_000_u64)].into_iter().collect(),
source: "operator".to_owned(),
signature: Vec::new(),
// The replay stamp. This fixture is about the ENTITLEMENT LADDER — can
// a lapsed tenant still mint a ticket — and not about replay, so the
// fact is seeded unstamped, which is what a fact from before the stamp
// existed looks like. `set_entitlement_fact` is the test double's own
// seam and verifies nothing, so an unstamped fact is a legitimate one
// to hand it.
issued_unix_ms: None,
issued_signature: Vec::new(),
});
// The suspension is real on this box, read through the same seam call
// `Usage.Read` makes. Without this the test could pass on a fact that was
// never stored, and would then be proving nothing at all.
let held = harness
.plane
.entitlement_get(ALICE)
.expect("the box can read its own facts")
.expect("alice has one");
assert_eq!(held.state, EntitlementState::Suspended as i32);
assert_eq!(held.caps.get("pack_bytes"), Some(&7_000));
// And she can still buy her way back.
let caller = harness.caller_for(&alice, "nonce-alice-lapsed-renews-1");
let minted = harness
.billing()
.mint_actor_ticket(harness.authed(&caller, renew(ALICE)))
.await
.expect(
"a Suspended tenant was refused a ticket. Self-service renewal is now dead: the \
only way back onto a plan is through an operator",
)
.into_inner();
monetize_embed::verify_ticket(
&minted.ticket,
&key,
"gunnar",
ALICE,
PURPOSE_RENEW,
&no_caps(),
now_ms(),
)
.expect("a lapsed tenant's ticket is an ordinary ticket");
}
// ───────────────────────────────────────────────────────────────────────────
// The rest of the refusals
// ───────────────────────────────────────────────────────────────────────────
/// **A key nobody enrolled gets no ticket**, and is told nothing that
/// distinguishes it from a bad signature.
#[tokio::test]
async fn an_unenrolled_caller_is_refused() {
let harness = Harness::new();
let stranger = mint_key();
// The gate itself will not produce a caller for this key, which is where
// the refusal really happens.
let credential =
sign_credential_for(&stranger, now_ms(), "nonce-stranger-00001", "Admin.Health")
.expect("sign");
let mut metadata = tonic::metadata::MetadataMap::new();
metadata.insert(
CREDENTIAL_METADATA_KEY,
credential.to_header_value().parse().expect("ascii"),
);
assert!(
AuthGate::new(Arc::clone(&harness.ctx.control), harness.ctx.config.auth)
.authenticate_bound(&metadata, now_ms(), Some("Admin.Health"))
.is_err(),
"an unenrolled key authenticated"
);
// And with no verified caller attached at all — a service mounted without
// the gate in front of it — the handler itself refuses rather than
// assuming an admin.
let before = harness.plane.actor_tickets_signed();
let refused = harness
.billing()
.mint_actor_ticket(Request::new(renew(ALICE)))
.await
.expect_err("no credential, no ticket");
assert_eq!(refused.code(), Code::Unauthenticated);
assert_eq!(harness.plane.actor_tickets_signed(), before);
let refused = harness
.billing()
.appliance_identity(Request::new(ApplianceIdentityRequest {}))
.await
.expect_err("no credential, no key");
assert_eq!(refused.code(), Code::Unauthenticated);
}
/// **`ApplianceIdentity` is operator configuration, and a tenant admin is not
/// an operator.**
///
/// The key it serves is public and keeping it back protects nothing — the
/// comment in `billing.proto` says exactly that, so the gate is not mistaken
/// for protecting key material. It is server `Admin` because the one person who
/// needs the string is the one wiring this appliance to a monetize.
#[tokio::test]
async fn appliance_identity_takes_a_server_admin_and_owning_a_namespace_is_not_one() {
let harness = Harness::new();
let alice = harness.enrol(ALICE);
let caller = harness.caller_for(&alice, "nonce-alice-wants-the-key");
// alice is intrinsically admin of `alice`. That is not a server grant.
let refused = harness
.billing()
.appliance_identity(harness.authed(&caller, ApplianceIdentityRequest {}))
.await
.expect_err("owning one namespace must not answer a server-scoped question");
assert_eq!(refused.code(), Code::PermissionDenied);
harness.plane.grant_server(ALICE, GrantLevel::Admin);
let caller = harness.caller_for(&alice, "nonce-alice-is-operator-1");
harness
.billing()
.appliance_identity(harness.authed(&caller, ApplianceIdentityRequest {}))
.await
.expect("a server admin may read it");
}
/// **A purpose this appliance does not mint is refused at the mint**, rather
/// than being signed and refused at the till.
///
/// `verify_ticket` compares the purpose, so a ticket minted for an invented
/// verb would be rejected on the other side anyway — but rejected as a
/// signature failure the customer cannot act on, from a box that had already
/// signed something it should not have.
#[tokio::test]
async fn a_purpose_this_appliance_does_not_mint_is_refused() {
let harness = Harness::new();
let alice = harness.enrol(ALICE);
let caller = harness.caller_for(&alice, "nonce-alice-invents-verb");
let before = harness.plane.actor_tickets_signed();
let refused = harness
.billing()
.mint_actor_ticket(harness.authed(
&caller,
MintActorTicketRequest {
namespace: ALICE.to_owned(),
// A verb no version of this appliance mints. It was `"order"`
// until orders became real, which is precisely the reason the
// purpose travels as a WORD: a build that predates a verb
// refuses it instead of signing something whose meaning it does
// not know.
purpose: "refund".to_owned(),
caps: no_caps(),
},
))
.await
.expect_err("this build mints two verbs and `refund` is neither");
assert_eq!(refused.code(), Code::InvalidArgument);
assert_eq!(harness.plane.actor_tickets_signed(), before);
// An EMPTY purpose is the default and means renew: a console that never
// learned the field still works, and the ticket still says the word.
let caller = harness.caller_for(&alice, "nonce-alice-omits-purpose");
let minted = harness
.billing()
.mint_actor_ticket(harness.authed(
&caller,
MintActorTicketRequest {
namespace: ALICE.to_owned(),
purpose: String::new(),
caps: no_caps(),
},
))
.await
.expect("an omitted purpose is renewal, the purpose that predates the field")
.into_inner();
let ticket: ActorTicket = serde_json::from_slice(&minted.ticket).expect("parses");
assert_eq!(ticket.purpose, PURPOSE_RENEW);
}
// ───────────────────────────────────────────────────────────────────────────
// Cancel: the tenant's own unpaid order, or a refund — monetize's verb
// ───────────────────────────────────────────────────────────────────────────
/// monetize's word for the ticket `Purchase.Cancel` and `Purchase.Abandon`
/// spend (monetize `feat/thin-ui-2026-10-04`, `monetize_embed::PURPOSE_CANCEL`).
/// The literal: the published `monetize-embed` this tree resolves predates the
/// constant.
const CANCEL: &str = "cancel";
fn cancel(namespace: &str) -> MintActorTicketRequest {
MintActorTicketRequest {
namespace: namespace.to_owned(),
purpose: CANCEL.to_owned(),
caps: no_caps(),
}
}
/// ★ **A cancel ticket is minted for the tenant's own namespace, names no caps,
/// and is no other verb's ticket** (2026-10-04: monetize scopes Status/Abandon
/// by tenant and actor ticket and adds Cancel; a person cancelling their OWN
/// unpaid order needs no more than ordering it took — the account admin's
/// `Admin` on the namespace, the line every purpose here shares).
#[tokio::test]
async fn a_cancel_ticket_is_minted_names_no_caps_and_is_no_other_verbs() {
let harness = Harness::new();
let key = advertised_key(&harness).await;
let alice = harness.enrol(ALICE);
let caller = harness.caller_for(&alice, "nonce-alice-cancels");
let minted = harness
.billing()
.mint_actor_ticket(harness.authed(&caller, cancel(ALICE)))
.await
.expect("the account admin may mint a cancel ticket for their own namespace")
.into_inner();
let ticket = monetize_embed::verify_ticket(
&minted.ticket,
&key,
"gunnar",
ALICE,
CANCEL,
&no_caps(),
now_ms(),
)
.expect("the ticket this appliance minted must verify as a cancel");
assert!(ticket.caps.is_empty(), "a cancel ticket covers no amount");
for other in [PURPOSE_RENEW, PURPOSE_READ] {
assert!(
monetize_embed::verify_ticket(
&minted.ticket,
&key,
"gunnar",
ALICE,
other,
&no_caps(),
now_ms()
)
.is_err(),
"a cancel ticket was spendable as {other:?}"
);
}
}
/// A cancel names no caps (refused by name, nothing signed), and is refused
/// for a namespace the caller does not hold (nothing signed).
#[tokio::test]
async fn a_cancel_naming_an_amount_or_another_namespace_is_refused_and_nothing_is_signed() {
let harness = Harness::new();
let alice = harness.enrol(ALICE);
let before = harness.plane.actor_tickets_signed();
let caller = harness.caller_for(&alice, "nonce-alice-cancels-caps");
let refused = harness
.billing()
.mint_actor_ticket(harness.authed(
&caller,
MintActorTicketRequest {
namespace: ALICE.to_owned(),
purpose: CANCEL.to_owned(),
caps: ten_gib(),
},
))
.await
.expect_err("a cancel buys nothing, so it names no amount");
assert_eq!(refused.code(), Code::InvalidArgument, "{refused:?}");
let caller = harness.caller_for(&alice, "nonce-alice-cancels-beta");
let refused = harness
.billing()
.mint_actor_ticket(harness.authed(&caller, cancel(BETA)))
.await
.expect_err("alice must not mint a cancel ticket naming beta");
assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
assert_eq!(
harness.plane.actor_tickets_signed(),
before,
"a refused cancel was signed"
);
}
// ───────────────────────────────────────────────────────────────────────────
// Orders: the same gate, and an AMOUNT the appliance has to vouch for
// ───────────────────────────────────────────────────────────────────────────
/// **An order ticket verifies for the caps it names, and for no others.**
///
/// The cross-repo contract test for the order half, mirroring
/// [`the_ticket_a_tenant_gets_verifies_under_the_key_the_appliance_advertises`]
/// for renewal: minted by the real handler, checked with
/// `monetize_embed::verify_ticket`, which is the function monetize itself
/// calls on `Purchase.Start`.
///
/// ★ The second half is the one that matters. The ticket is genuine, signed by
/// this very appliance, for the right tenant, product and verb — and the order
/// it is presented with asks for a hundred times the room. Without the caps in
/// the signed message the appliance would be vouching for "this human may buy"
/// and whatever held the ticket would choose how much: a blank cheque, signed
/// here.
#[tokio::test]
async fn an_order_ticket_verifies_for_the_caps_it_names_and_not_for_others() {
let harness = Harness::new();
let key = advertised_key(&harness).await;
let alice = harness.enrol(ALICE);
let caller = harness.caller_for(&alice, "nonce-alice-orders-room-1");
let minted = harness
.billing()
.mint_actor_ticket(harness.authed(&caller, order(ALICE)))
.await
.expect("alice may order for alice")
.into_inner();
let ticket = monetize_embed::verify_ticket(
&minted.ticket,
&key,
"gunnar",
ALICE,
PURPOSE_ORDER,
&ten_gib(),
now_ms(),
)
.expect("the appliance's own order ticket must verify under the key it advertises");
assert_eq!(ticket.tenant, ALICE);
assert_eq!(ticket.purpose, PURPOSE_ORDER);
assert_eq!(
ticket.caps,
ten_gib(),
"the caps the console asked for must be the caps the appliance signed, meter for \
meter — a ticket that covers something else is a ticket for another order"
);
assert_eq!(
minted.expires_unix_ms, ticket.expires_unix_ms as u64,
"the expiry in the envelope must be the expiry in the signed claims"
);
// ★ The same genuine ticket, against an order a hundred times the size.
let hundredfold = Caps::from([("pack_bytes".to_owned(), 1_073_741_824_000u64)]);
assert!(
monetize_embed::verify_ticket(
&minted.ticket,
&key,
"gunnar",
ALICE,
PURPOSE_ORDER,
&hundredfold,
now_ms(),
)
.is_err(),
"alice's genuine ticket for 10 GiB bought a hundred times that. The appliance's \
signature is now a blank cheque"
);
// And still not another tenant's, which is the renewal rule unchanged.
assert!(
monetize_embed::verify_ticket(
&minted.ticket,
&key,
"gunnar",
BETA,
PURPOSE_ORDER,
&ten_gib(),
now_ms(),
)
.is_err(),
"alice's genuine order ticket bought beta room"
);
}
/// ★ **A caller may not mint an ORDER ticket for a namespace they do not
/// hold, and nothing is signed.**
///
/// The same assertion as
/// [`a_caller_may_not_mint_a_ticket_for_a_namespace_they_do_not_hold`], driven
/// down the order path, because the grant check must not have been weakened for
/// orders on the way — and an order is the request with an amount attached, so
/// a hole here is worth more than a hole in renewal.
///
/// The signature counter is the second half and not decoration: a handler that
/// signed first and checked afterwards would satisfy a test that only read the
/// `Status`, having already put a valid, spendable order ticket for another
/// tenant through whatever sits between here and the caller.
#[tokio::test]
async fn a_caller_may_not_mint_an_order_ticket_for_a_namespace_they_do_not_hold() {
let harness = Harness::new();
let alice = harness.enrol(ALICE);
let caller = harness.caller_for(&alice, "nonce-alice-orders-for-beta");
let before = harness.plane.actor_tickets_signed();
let refused = harness
.billing()
.mint_actor_ticket(harness.authed(&caller, order(BETA)))
.await
.expect_err("alice must not mint an order ticket naming beta");
assert_eq!(refused.code(), Code::PermissionDenied);
assert_eq!(
harness.plane.actor_tickets_signed(),
before,
"the appliance SIGNED an order for a namespace the caller does not hold. The grant \
check must run before the key is touched, on the order path exactly as on renewal"
);
// Her own namespace, same request shape: the refusal above is about the
// namespace and not about alice or about orders being shut.
let caller = harness.caller_for(&alice, "nonce-alice-orders-for-self");
harness
.billing()
.mint_actor_ticket(harness.authed(&caller, order(ALICE)))
.await
.expect("alice holds alice");
}
/// **An order with no caps is refused by name**, rather than minted as a
/// ticket that covers nothing.
///
/// An order names an AMOUNT. A ticket for `order` that covers no meters is a
/// client that has confused the two paths — it wanted `renew` — and the remedy
/// is to say which path it wanted, not to sign something monetize would then
/// refuse as a signature failure the customer cannot act on.
#[tokio::test]
async fn an_order_with_no_caps_is_refused() {
let harness = Harness::new();
let alice = harness.enrol(ALICE);
let caller = harness.caller_for(&alice, "nonce-alice-empty-order-1");
let before = harness.plane.actor_tickets_signed();
let refused = harness
.billing()
.mint_actor_ticket(harness.authed(
&caller,
MintActorTicketRequest {
namespace: ALICE.to_owned(),
purpose: PURPOSE_ORDER.to_owned(),
caps: no_caps(),
},
))
.await
.expect_err("an order that names no amount is not an order");
assert_eq!(refused.code(), Code::InvalidArgument);
assert!(
refused.message().contains("caps"),
"the refusal must name the field the client got wrong: {refused:?}"
);
assert_eq!(
harness.plane.actor_tickets_signed(),
before,
"an order covering nothing was signed anyway"
);
}
/// **A renewal carrying caps is refused by name**, and it is refused HERE
/// rather than at the till.
///
/// `verify_ticket` refuses a renewal ticket that carries caps — a renewal sells
/// another period of what is already held, and the caps come off the stored
/// fact — so minting one would produce a ticket that cannot be spent. The
/// customer would see a signature failure on the page whose whole job is to
/// take money, from a box that had already signed something it should not have.
#[tokio::test]
async fn a_renewal_carrying_caps_is_refused() {
let harness = Harness::new();
let alice = harness.enrol(ALICE);
let caller = harness.caller_for(&alice, "nonce-alice-renew-w-caps");
let before = harness.plane.actor_tickets_signed();
let refused = harness
.billing()
.mint_actor_ticket(harness.authed(
&caller,
MintActorTicketRequest {
namespace: ALICE.to_owned(),
purpose: PURPOSE_RENEW.to_owned(),
caps: ten_gib(),
},
))
.await
.expect_err("a renewal names no caps");
assert_eq!(refused.code(), Code::InvalidArgument);
assert!(
refused.message().contains("caps"),
"the refusal must name the field the client got wrong: {refused:?}"
);
assert_eq!(harness.plane.actor_tickets_signed(), before);
// And the same for the EMPTY purpose, which means renew: a console that
// predates the purpose field but learned about caps must not slip an
// amount past the rule by omitting the word.
let caller = harness.caller_for(&alice, "nonce-alice-omit-purp-caps");
let refused = harness
.billing()
.mint_actor_ticket(harness.authed(
&caller,
MintActorTicketRequest {
namespace: ALICE.to_owned(),
purpose: String::new(),
caps: ten_gib(),
},
))
.await
.expect_err("an omitted purpose is renewal, and a renewal names no caps");
assert_eq!(refused.code(), Code::InvalidArgument);
assert_eq!(harness.plane.actor_tickets_signed(), before);
}
/// An empty namespace is `INVALID_ARGUMENT` and not a grant question.
///
/// `owns_account` is a string equality, and the double and the real table both
/// guard it with `!namespace.is_empty()` — but a request that reached the grant
/// check with an empty scope would be one refactor away from asking "does
/// anybody own ''", and the answer to that must never be able to become yes.
#[tokio::test]
async fn an_empty_namespace_is_refused_before_the_grant_check() {
let harness = Harness::new();
let alice = harness.enrol(ALICE);
let caller = harness.caller_for(&alice, "nonce-alice-empty-ns-01");
let refused = harness
.billing()
.mint_actor_ticket(harness.authed(&caller, renew("")))
.await
.expect_err("an unnamed tenant is not a tenant");
assert_eq!(refused.code(), Code::InvalidArgument);
}
/// **A grant is a grant**: a principal with an explicit `Admin` grant on a
/// namespace they do not own mints for it, exactly as the owner does.
///
/// The scope check is `Identity::may`, and `owns_account` is one of its two
/// answers. This pins the other, so "gunnar enforces the tenant scope" cannot
/// quietly narrow to "gunnar compares two strings" — an organisation's
/// administrator is a real case, and a renewal path that only ever worked for
/// a principal whose id happened to equal the namespace would fail it.
#[tokio::test]
async fn an_explicit_namespace_admin_may_mint_for_a_namespace_they_do_not_own() {
let harness = Harness::new();
let key = advertised_key(&harness).await;
let admin = harness.enrol("org-admin");
harness
.plane
.grant("org-admin", BETA, "", GrantLevel::Admin)
.expect("granted");
let caller = harness.caller_for(&admin, "nonce-orgadmin-mints-01");
let minted = harness
.billing()
.mint_actor_ticket(harness.authed(&caller, renew(BETA)))
.await
.expect("an Admin grant on beta is what this RPC asks for")
.into_inner();
let ticket = monetize_embed::verify_ticket(
&minted.ticket,
&key,
"gunnar",
BETA,
PURPOSE_RENEW,
&no_caps(),
now_ms(),
)
.expect("verifies");
assert_eq!(ticket.tenant, BETA);
}
/// ★ **The known exclusion, pinned rather than left to be discovered.**
///
/// `Write` on a namespace is NOT enough. That is a ruling and not an oversight:
/// see the handler's doc comment for the whole of it, including the case it
/// knowingly excludes — an organisation member who holds only `Write` and who
/// may well be the person who actually pays the bill.
///
/// The test exists so that widening it later is a deliberate act with a red
/// test attached, and so that nobody reads `Admin` in the handler as a typo for
/// the level the neighbouring RPCs take.
#[tokio::test]
async fn a_write_grant_is_deliberately_not_enough() {
let harness = Harness::new();
let writer = harness.enrol("contributor");
harness
.plane
.grant("contributor", BETA, "", GrantLevel::Write)
.expect("granted");
let caller = harness.caller_for(&writer, "nonce-writer-tries-01");
let before = harness.plane.actor_tickets_signed();
let refused = harness
.billing()
.mint_actor_ticket(harness.authed(&caller, renew(BETA)))
.await
.expect_err(
"Write on a namespace minted a ticket. If that is now intended, this test is the \
place to say so out loud — the handler's doc comment records the ruling",
);
assert_eq!(refused.code(), Code::PermissionDenied);
assert_eq!(harness.plane.actor_tickets_signed(), before);
}
/// **A red-first instrument, used the way `tests/auth.rs` uses it.**
///
/// `force_may(Some(false))` must make the happy path go red. Without this, a
/// handler that had been hard-wired to allow would satisfy every allow test in
/// this file just as well as a correct one — and the allow tests are what give
/// the refusal tests their meaning.
#[tokio::test]
async fn the_grant_check_is_really_consulted() {
let harness = Harness::new();
let alice = harness.enrol(ALICE);
let caller = harness.caller_for(&alice, "nonce-alice-forced-deny-1");
harness.plane.force_may(Some(false));
let refused = harness
.billing()
.mint_actor_ticket(harness.authed(&caller, renew(ALICE)))
.await
.expect_err("with `may` forced to false nothing may be minted");
assert_eq!(refused.code(), Code::PermissionDenied);
harness.plane.force_may(None);
let caller = harness.caller_for(&alice, "nonce-alice-forced-restored");
harness
.billing()
.mint_actor_ticket(harness.authed(&caller, renew(ALICE)))
.await
.expect("the instrument is not sticky");
}
// ───────────────────────────────────────────────────────────────────────────
// A private Gunnar's owner, at its seller (private ownership (c), 2026-10-07)
// ───────────────────────────────────────────────────────────────────────────
/// monetize's word for the ticket a private Gunnar's console presents at the
/// SELLER's monetize (`EstateOwner.Ladder` / `ChangePlan`):
/// `monetize_embed::ticket::PURPOSE_ESTATE`. The literal: the published
/// `monetize-embed` this tree resolves predates the constant.
const ESTATE: &str = "estate";
fn estate(namespace: &str) -> MintActorTicketRequest {
MintActorTicketRequest {
namespace: namespace.to_owned(),
purpose: ESTATE.to_owned(),
caps: no_caps(),
}
}
/// ★ **The estate's own gunnar vouches for its owner at the seller**: an
/// `estate` ticket is minted for the namespace the caller is `Admin` of — on
/// a sold estate the order's account, whose principal id IS its namespace —
/// names no caps, verifies under the key this appliance advertises (the one
/// the seller recorded at install) as an `estate` ticket for that tenant, and
/// is no other verb's ticket.
#[tokio::test]
async fn an_estate_ticket_is_minted_for_the_owners_own_namespace_and_is_no_other_verbs() {
let harness = Harness::new();
let key = advertised_key(&harness).await;
let alice = harness.enrol(ALICE);
let caller = harness.caller_for(&alice, "nonce-alice-at-the-seller");
let minted = harness
.billing()
.mint_actor_ticket(harness.authed(&caller, estate(ALICE)))
.await
.expect("the owner may mint an estate ticket for their own namespace")
.into_inner();
let ticket = monetize_embed::verify_ticket(
&minted.ticket,
&key,
"gunnar",
ALICE,
ESTATE,
&no_caps(),
now_ms(),
)
.expect("the ticket this appliance minted must verify as an estate ticket");
assert!(
ticket.caps.is_empty(),
"an estate ticket covers no amount: the seller prices a plan change itself"
);
for other in [PURPOSE_RENEW, PURPOSE_READ, PURPOSE_ORDER] {
assert!(
monetize_embed::verify_ticket(
&minted.ticket,
&key,
"gunnar",
ALICE,
other,
&no_caps(),
now_ms()
)
.is_err(),
"an estate ticket was spendable as {other:?}"
);
}
assert!(
monetize_embed::verify_ticket(
&minted.ticket,
&key,
"gunnar",
BETA,
ESTATE,
&no_caps(),
now_ms()
)
.is_err(),
"an estate ticket for alice must not speak for beta"
);
}
/// An estate ticket names no caps, and is refused for a namespace the caller
/// does not hold — nothing is signed either way.
#[tokio::test]
async fn an_estate_ticket_naming_an_amount_or_another_namespace_is_refused_and_nothing_is_signed() {
let harness = Harness::new();
let alice = harness.enrol(ALICE);
let before = harness.plane.actor_tickets_signed();
let caller = harness.caller_for(&alice, "nonce-alice-estate-caps");
let refused = harness
.billing()
.mint_actor_ticket(harness.authed(
&caller,
MintActorTicketRequest {
namespace: ALICE.to_owned(),
purpose: ESTATE.to_owned(),
caps: ten_gib(),
},
))
.await
.expect_err("an estate ticket names no amount");
assert_eq!(refused.code(), Code::InvalidArgument, "{refused:?}");
let caller = harness.caller_for(&alice, "nonce-alice-estate-beta");
let refused = harness
.billing()
.mint_actor_ticket(harness.authed(&caller, estate(BETA)))
.await
.expect_err("alice must not mint an estate ticket naming beta");
assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
assert_eq!(
harness.plane.actor_tickets_signed(),
before,
"a refused estate ticket was signed"
);
}
// ───────────────────────────────────────────────────────────────────────────
// Members never buy (owner, 2026-10-07: a customer is an ACCOUNT that pays)
// ───────────────────────────────────────────────────────────────────────────
/// `name`, enrolled by `account` the way "Add users" enrols a member: a NEW
/// principal bound to the account, no namespace of its own, `Read` on the
/// account's — with one key, the shape a console's per-person key has.
fn member(harness: &Harness, account: &str, name: &str) -> ssh_key::PrivateKey {
harness
.plane
.enroll_employee(
account,
&gunnar_grpc::EmployeeAsk {
id: name.to_owned(),
name: name.to_owned(),
..Default::default()
},
)
.expect("the account enrols its member");
let key = mint_key();
harness
.plane
.add_key(
name,
&key.public_key().to_openssh().expect("openssh"),
name,
None,
"",
None,
)
.expect("the member's key");
key
}
/// ★ **A member of an account is never minted a ticket — for its own name or
/// the account's — and is told why in plain words.**
///
/// The walkthrough of 2026-10-07 measured the hole: `ben`, an employee `acme`
/// enrolled, pressed "Grow your account", and this appliance vouched for an
/// order for tenant `ben` — `owns_account("ben", "ben")` is a string equality,
/// and no namespace `ben` exists. He paid, and bought room for nothing. A
/// member uses the account's room; more of it is the account admin's to buy.
#[tokio::test]
async fn a_member_of_an_account_is_refused_a_ticket_by_name() {
let harness = Harness::new();
let alice = harness.enrol(ALICE);
let ben = member(&harness, ALICE, "ben");
let before = harness.plane.actor_tickets_signed();
for (nonce, asked) in [
("nonce-ben-orders-for-ben", order("ben")),
("nonce-ben-renews-ben", renew("ben")),
("nonce-ben-reads-ben", read("ben")),
("nonce-ben-orders-for-alice", order(ALICE)),
] {
let caller = harness.caller_for(&ben, nonce);
let refused = harness
.billing()
.mint_actor_ticket(harness.authed(&caller, asked))
.await
.expect_err("a member buys nothing: the account's admin does");
assert_eq!(
refused.code(),
Code::PermissionDenied,
"{nonce}: {refused:?}"
);
let words = refused.message();
assert!(
words.contains("ben is a member of alice") && words.contains("Ask the admin of alice"),
"{nonce}: the refusal names the account and whom to ask: {words}"
);
}
assert_eq!(
harness.plane.actor_tickets_signed(),
before,
"nothing was signed for a member"
);
// The account's own admin still buys for the account.
let caller = harness.caller_for(&alice, "nonce-alice-still-orders");
harness
.billing()
.mint_actor_ticket(harness.authed(&caller, order(ALICE)))
.await
.expect("the account admin orders room for the account");
}
/// ★ **No ticket for a name that has no namespace.** Room is bought FOR a
/// namespace; a ticket for one this box does not hold would sell room that
/// lands nowhere — the walkthrough's "your room is ready" for a namespace
/// that does not exist. Refused by name, after the grant check (so it tells a
/// stranger nothing), for the name's own principal and for a server admin.
#[tokio::test]
async fn a_name_with_no_namespace_is_refused_a_ticket() {
let harness = Harness::new();
// A principal whose namespace was never made: `owns_account` holds by
// string equality, so the grant check alone would let it through.
let ghost = harness.enrol("ghost");
let operator = harness.enrol("operator");
harness.plane.grant_server("operator", GrantLevel::Admin);
let before = harness.plane.actor_tickets_signed();
for (key, nonce, asked) in [
(&ghost, "nonce-ghost-orders", order("ghost")),
(&ghost, "nonce-ghost-renews", renew("ghost")),
(
&operator,
"nonce-operator-orders-for-nobody",
order("nobody"),
),
] {
let caller = harness.caller_for(key, nonce);
let refused = harness
.billing()
.mint_actor_ticket(harness.authed(&caller, asked))
.await
.expect_err("room is bought for a namespace that exists");
assert_eq!(refused.code(), Code::NotFound, "{nonce}: {refused:?}");
assert!(
refused.message().contains("there is no namespace"),
"{nonce}: the refusal says why: {}",
refused.message()
);
}
assert_eq!(
harness.plane.actor_tickets_signed(),
before,
"nothing was signed"
);
// A stranger asking about a name it does not hold learns nothing: the
// grant check answers first, the same way for a name that exists and one
// that does not.
let caller = harness.caller_for(&ghost, "nonce-ghost-asks-for-nobody");
let refused = harness
.billing()
.mint_actor_ticket(harness.authed(&caller, order("nobody")))
.await
.expect_err("not ghost's to ask");
assert_eq!(refused.code(), Code::PermissionDenied);
}