gunnar-grpc 1.5.0

The tonic service implementations and the event bus behind gunnar's gRPC control plane.
Documentation
//! `Entitlement`. The paid state per namespace — **the metered appliance's
//! service**, compiled only with the `metered` feature.
//!
//! `Set` is the one writer and it is `Admin` level on the server: it is
//! monetize (or an operator with the server's admin key) telling this box
//! what a tenant has paid for. The signature check, the attestation line and
//! the control-document write are all the control plane's
//! (`ControlPlane::entitlement_set`); this handler authorises, validates the
//! shape, and reports. A fact that does not verify comes back as a gRPC
//! error naming the reason — `PERMISSION_DENIED` for a signature the
//! configured monetize key did not make, `INVALID_ARGUMENT` for a fact that
//! is not even well formed — never as a field a caller could overlook.
//!
//! The three reads are server-wide `Read`: a plan is the provider's business
//! with the tenant, and the tenant's own view of it is the billing page, not
//! this RPC.

use gunnar_proto::entitlement_server::Entitlement;
use gunnar_proto::{
    EntitlementFact, EntitlementState, GetEntitlementRequest, GetEntitlementResponse, GrantLevel,
    ListEntitlementsRequest, ListEntitlementsResponse, SetEntitlementRequest, SetEntitlementResponse,
};
use tonic::{Request, Response, Status};

use crate::auth::Access;
use crate::service::EntitlementService;

/// The checks a fact must pass before the control plane is asked to verify
/// its signature: cheap, shape-only, and answered as `INVALID_ARGUMENT` so a
/// malformed push is told apart from a forged one.
///
/// `pub` so `tests/entitlement_caps_door.rs` can assert the refusals directly.
/// This is the door itself — the handler below calls it before it looks at
/// anything else — so a test of this function is a test of the door and not of
/// a copy of it.
pub fn well_formed(fact: &EntitlementFact) -> Result<(), Status> {
    if fact.namespace.is_empty() {
        return Err(Status::invalid_argument("fact.namespace is required"));
    }
    if fact.plan.is_empty() {
        return Err(Status::invalid_argument("fact.plan is required"));
    }
    if fact.state() == EntitlementState::Unspecified {
        return Err(Status::invalid_argument(
            "fact.state is unspecified; a fact names one of Free, Paid, Grace, Suspended, Retention",
        ));
    }
    if fact.source.is_empty() {
        return Err(Status::invalid_argument(
            "fact.source is required: `operator` or `payment:<vendor>:<reference>`",
        ));
    }
    if fact.signature.len() != 64 {
        return Err(Status::invalid_argument(format!(
            "fact.signature is {} byte(s); an Ed25519 signature is 64",
            fact.signature.len()
        )));
    }
    // **The stamp is both halves or neither.** An issue time with no signature
    // over it is not a weaker fact, it is one whose monotonic half anybody could
    // have written — and the control plane refuses it as forged a moment later.
    // Saying so HERE, in the same words as the signature length above, tells a
    // caller which field is wrong instead of only that the fact did not verify.
    match (fact.issued_unix_ms, fact.issued_signature.len()) {
        (None, 0) | (Some(_), 64) => {}
        (None, n) => {
            return Err(Status::invalid_argument(format!(
                "fact.issued_signature is {n} byte(s) but fact.issued_unix_ms is absent; \
                 a fact carries both halves of the stamp or neither"
            )))
        }
        (Some(_), n) => {
            return Err(Status::invalid_argument(format!(
                "fact.issued_signature is {n} byte(s); the Ed25519 signature over \
                 fact.issued_unix_ms is 64"
            )))
        }
    }
    // **A cap this appliance can neither refuse on nor report is refused HERE,
    // at the sale, rather than carried silently for the life of the plan.**
    //
    // `caps` is an open `map<string, uint64>` and this handler used to look
    // straight past it: a fact could name any key and gunnar stored it, logged
    // an attestation line for it and then did nothing with it, ever. The
    // operator found out from a tenant three months later, or never.
    //
    // The alternative that was tried on paper and rejected: log a warning and
    // accept. A warning is what the attestation line already was — a trace
    // nobody reads until someone is already unhappy — and the doctrine on this
    // box is that a refusal is a message. `crate::service::meters` carries the
    // measured blast radius: this cannot reach the boot path, and an
    // `INVALID_ARGUMENT` leaves the tenant's PREVIOUS fact in force, so the
    // worst case is an operator with until `paid_until` to fix an order.
    //
    // A cap on a MEASURED meter is not refused — see
    // [`crate::service::meters::caps_gunnar_cannot_honour`] for why selling a
    // labelled measurement is honest and refusing it would not be.
    let unknown = crate::service::meters::caps_gunnar_cannot_honour(fact.caps.keys());
    if !unknown.is_empty() {
        return Err(Status::invalid_argument(format!(
            "fact.caps names {} this appliance can neither enforce nor report, so a tenant \
             would be sold a number nothing on this box ever reads; it honours {:?}",
            unknown
                .iter()
                .map(|meter| format!("`{meter}`"))
                .collect::<Vec<_>>()
                .join(", "),
            crate::service::meters::honourable_caps(),
        )));
    }
    Ok(())
}

#[tonic::async_trait]
impl Entitlement for EntitlementService {
    async fn get(
        &self,
        request: Request<GetEntitlementRequest>,
    ) -> Result<Response<GetEntitlementResponse>, Status> {
        // **The namespace FIRST, then the grant on it.**
        //
        // The order is the point. An empty name is a malformed request and is
        // answered as one, before any grant is consulted — the same shape
        // `Billing.MintActorTicket` uses — so a caller cannot learn anything
        // about what they may reach by varying a name they did not supply.
        let caller = self.ctx.caller(&request)?;
        let namespace = request.into_inner().namespace;
        if namespace.is_empty() {
            return Err(Status::invalid_argument("namespace is required"));
        }
        // **`Access::Server` until 2026-09-17, and this is a WIDENING that
        // takes nothing away.**
        //
        // A `Scope::Server` grant satisfies `Access::namespace(..)` —
        // `Scope::covers_namespace` answers true for `Server` — so every
        // caller that could read an entitlement before still can, on every
        // namespace, unchanged. What it adds is exactly one capability: a
        // principal may read the entitlement of a namespace it holds.
        //
        // It is added because a tenant could not otherwise be shown their own
        // renewal date. The console reads this with the CALLER's key (the one
        // `gunnar-ui`'s `TenantKeys` enrolled on their own principal), so with
        // a server-scoped requirement the answer was `PermissionDenied` for
        // the one person the fact is about.
        //
        // `Entitlement.Set` is deliberately NOT touched: writing a fact is
        // monetize's, it is verified against monetize's key, and nothing about
        // reading your own plan argues for being able to grant yourself one.
        self.ctx.require(
            &caller,
            Access::namespace(&namespace),
            GrantLevel::Read,
            "Entitlement.Get",
        )?;
        let fact = self.ctx.control.entitlement_get(&namespace)?;
        Ok(Response::new(GetEntitlementResponse { fact }))
    }

    async fn set(
        &self,
        request: Request<SetEntitlementRequest>,
    ) -> Result<Response<SetEntitlementResponse>, Status> {
        let caller =
            self.ctx
                .authorize(&request, Access::Server, GrantLevel::Admin, "Entitlement.Set")?;
        let fact = request
            .into_inner()
            .fact
            .ok_or_else(|| Status::invalid_argument("fact is required"))?;
        well_formed(&fact)?;
        let namespace = fact.namespace.clone();
        // **A plan is set for an account that exists** (round 5.3; the members
        // walkthrough of 2026-10-07: ben, a member of acme, paid for room for
        // tenant `ben`, and this box took the fact although no namespace `ben`
        // exists — room that lands nowhere, and a shop that now knows a member
        // as a customer). `Billing.MintActorTicket` refuses to sell for such a
        // name; this is the other end of the same rule. A paid signup makes the
        // principal — and with it the namespace — before it sets the fact, so
        // no honest order arrives here first. NOT_FOUND, which monetize's
        // gunnar plugin files as final: the fact can never be enforced here.
        // Asked after the Admin check, so only the provider learns it; nothing
        // is verified, logged or stored.
        //
        // **Unless this box's log FOLLOWS another's** (wiring tracer B
        // 2026-10-08): a main being restored has not replayed a namespace made
        // after the twin's snapshot yet, and NOT_FOUND is final at monetize —
        // the dead-main watch's re-push of a paid cap would be dropped. Such a
        // box records no fact anyway; it says "try again", in the txlog's own
        // words (monetize's `reads_as_restoring`).
        if self.ctx.control.log_follows() {
            return Err(Status::unavailable(
                "txlog: this box is a twin (or restoring); it originates no transactions — the \
                 fact is taken once it opens as the primary: try again",
            ));
        }
        if self.ctx.control.namespace(&namespace)?.is_none() {
            return Err(Status::not_found(format!(
                "there is no namespace {namespace} on this Gunnar, so there is no account to set \
                 a plan for: an entitlement is set for an account that exists. Nothing was \
                 verified, logged or stored"
            )));
        }
        let source = fact.source.clone();
        // What the account held before: the audit entry says what the money
        // did — bought, prolonged, upgraded — in the account's own log.
        let before = self.ctx.control.entitlement_get(&namespace).ok().flatten();
        let (plan, paid_until, caps) = (fact.plan.clone(), fact.paid_until_unix_ms, fact.caps.clone());
        let (state, attestation_seq) = self.ctx.control.entitlement_set(fact)?;
        let kind = match &before {
            None => "billing.bought",
            Some(prev) if prev.plan != plan || prev.caps != caps => "billing.upgraded",
            Some(prev) if paid_until.unwrap_or(0) > prev.paid_until_unix_ms.unwrap_or(0) => "billing.prolonged",
            Some(_) => "billing.changed",
        };
        self.ctx.account_event(
            self.ctx.account_of_namespace(&namespace),
            crate::enterprise::AccountAuditEvent::by(&caller, kind, namespace.clone()).detail(format!(
                "plan {plan}, {state:?}{}, {source}",
                paid_until.map(|t| format!(", paid until {t}")).unwrap_or_default()
            )),
        );
        tracing::info!(
            target: crate::auth::AUDIT_TARGET,
            event = "entitlement.set",
            principal = %caller.principal_id,
            %namespace,
            state = ?state,
            %source,
            attestation_seq,
            "entitlement fact accepted"
        );
        Ok(Response::new(SetEntitlementResponse {
            state: state as i32,
            attestation_seq,
        }))
    }

    async fn list(
        &self,
        request: Request<ListEntitlementsRequest>,
    ) -> Result<Response<ListEntitlementsResponse>, Status> {
        self.ctx
            .authorize(&request, Access::Server, GrantLevel::Read, "Entitlement.List")?;
        let facts = self.ctx.control.entitlement_list()?;
        // What the box HOLDS and does not enforce, beside what it enforces: an empty
        // `facts` alone is also what a box with no customers answers.
        let refused_at_boot = self.ctx.control.entitlement_refused_at_boot()?;
        Ok(Response::new(ListEntitlementsResponse { facts, refused_at_boot }))
    }
}