gunnar-grpc 1.5.0

The tonic service implementations and the event bus behind gunnar's gRPC control plane.
Documentation
//! **`Sealed`: a named sealed slot** (2026-10-09) — the METERED appliance's,
//! absent without the feature.
//!
//! Bytes under a NAME, sealed under the server's KEK and written as one
//! `Secret` frame (the twin, snapshots and a recharge carry it — nothing of
//! that is this module's: the plane stores through the sealed, logged secret
//! store every secret goes through). This module is the ACL, and the ACL is
//! ONE rule, [`decide`]:
//!
//! * a slot that EXISTS is read and written by its owner, a machine principal,
//!   and by nobody else — an admin included;
//! * a slot that does NOT exist is created by a machine principal for itself,
//!   or by a server admin FOR a machine principal it names (the one way a slot
//!   reaches a machine that cannot write it yet); the owner never changes.
//!
//! The server knows no meaning of any slot: it does not parse a value, and a
//! name is only checked for its shape ([`check_name`]).

use gunnar_proto::sealed_server::Sealed;
use gunnar_proto::{SealedGetRequest, SealedGetResponse, SealedPutRequest, SealedPutResponse};
use tonic::{Request, Response, Status};

use crate::auth::{Access, Caller};
use crate::service::SealedService;
use gunnar_proto::GrantLevel;

/// The longest slot value: the slot is a sealed secret, not a store.
pub const MAX_VALUE: usize = 256 * 1024;

/// Pure: **a slot name** is `[a-z0-9-]`, 1 to 64 characters.
pub fn check_name(name: &str) -> Result<(), String> {
    let ok = !name.is_empty() && name.len() <= 64 && name.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-');
    if ok {
        Ok(())
    } else {
        Err(format!("a slot name is [a-z0-9-], 1 to 64 characters; {name:?} is not"))
    }
}

/// What one call wants of a slot.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Want<'a> {
    /// Read it.
    Get,
    /// Write it; `named_owner` is the principal id the caller names as the
    /// owner of a slot being created (`None`: the caller itself).
    Put { named_owner: Option<&'a str> },
}

/// Pure: **the one rule.** `existing_owner` is the slot's owner when it
/// exists; `caller_is_admin` whether the caller holds server Admin (read only
/// when it names another owner). Answers the owner the slot has (or gets), or
/// why not.
pub fn decide(existing_owner: Option<&str>, caller: &Caller, want: Want<'_>, caller_is_admin: bool) -> Result<String, String> {
    if !caller.authenticated {
        return Err("a slot is its owner's: an unauthenticated caller reaches none".into());
    }
    match (existing_owner, want) {
        (Some(owner), Want::Put { named_owner: Some(named) }) if named != owner => {
            Err("a slot's owner does not change".into())
        }
        (Some(owner), _) if owner == caller.principal_id && caller.machine => Ok(owner.to_string()),
        (Some(_), _) => Err("the slot is another machine's".into()),
        (None, Want::Get) => Err("no such slot".into()),
        (None, Want::Put { named_owner: None }) if caller.machine => Ok(caller.principal_id.clone()),
        (None, Want::Put { named_owner: None }) => Err("only a machine principal owns a slot".into()),
        (None, Want::Put { named_owner: Some(named) }) if named == caller.principal_id && caller.machine => Ok(named.to_string()),
        (None, Want::Put { named_owner: Some(named) }) if caller_is_admin => Ok(named.to_string()),
        (None, Want::Put { named_owner: Some(_) }) => Err("only a server admin creates a slot for another machine".into()),
    }
}

#[tonic::async_trait]
impl Sealed for SealedService {
    async fn put(&self, request: Request<SealedPutRequest>) -> Result<Response<SealedPutResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        let r = request.into_inner();
        check_name(&r.name).map_err(Status::invalid_argument)?;
        if r.value.len() > MAX_VALUE {
            return Err(Status::invalid_argument(format!("a slot holds at most {MAX_VALUE} bytes; {} were sent", r.value.len())));
        }
        // The owner named BY NAME, resolved to its id: a machine principal that exists.
        let named = match r.owner.trim() {
            "" => None,
            name => {
                let p = self.ctx.control.principal_by_name(name)?.ok_or_else(|| Status::invalid_argument(format!("no principal is named {name:?}")))?;
                if p.kind != gunnar_proto::PrincipalKind::Machine as i32 {
                    return Err(Status::invalid_argument(format!("{name:?} is not a machine principal: only a machine owns a slot")));
                }
                Some(p.id)
            }
        };
        let existing = self.ctx.control.sealed_get(&r.name)?.map(|(owner, _)| owner);
        let admin = match (&existing, &named) {
            (None, Some(id)) if *id != caller.principal_id => self.ctx.require(&caller, Access::Server, GrantLevel::Admin, "Sealed.Put").is_ok(),
            _ => false,
        };
        let owner = decide(existing.as_deref(), &caller, Want::Put { named_owner: named.as_deref() }, admin).map_err(Status::permission_denied)?;
        let created = self.ctx.control.sealed_put(&r.name, &owner, &r.value)?;
        tracing::info!(slot = %r.name, owner = %owner, caller = %caller.principal_id, created, bytes = r.value.len(), "Sealed.Put");
        Ok(Response::new(SealedPutResponse { created }))
    }

    async fn get(&self, request: Request<SealedGetRequest>) -> Result<Response<SealedGetResponse>, Status> {
        let caller = self.ctx.caller(&request)?;
        let r = request.into_inner();
        check_name(&r.name).map_err(Status::invalid_argument)?;
        let slot = self.ctx.control.sealed_get(&r.name)?;
        let owner = slot.as_ref().map(|(o, _)| o.as_str());
        decide(owner, &caller, Want::Get, false).map_err(Status::permission_denied)?;
        let (_, value) = slot.expect("decide answered Ok only for an existing slot");
        Ok(Response::new(SealedGetResponse { value }))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn who(id: &str, machine: bool) -> Caller {
        Caller { principal_id: id.into(), fingerprint: "SHA256:x".into(), machine, authenticated: true }
    }

    /// ★ **The slot ACL** (F4, the owner 2026-10-09): a slot is its owner machine's alone.
    #[test]
    fn a_slot_is_read_and_written_by_its_owner_machine_alone() {
        let front = who("front-certify", true);
        let other = who("monetize", true);
        let human = who("alice", false);
        // Created by a machine for itself; a human owns none.
        assert_eq!(decide(None, &front, Want::Put { named_owner: None }, false), Ok("front-certify".into()));
        assert!(decide(None, &human, Want::Put { named_owner: None }, true).is_err(), "only a machine owns a slot, an admin human included");
        // Created by an admin FOR a machine it names — and only by an admin.
        assert_eq!(decide(None, &human, Want::Put { named_owner: Some("front-certify") }, true), Ok("front-certify".into()));
        assert!(decide(None, &human, Want::Put { named_owner: Some("front-certify") }, false).is_err());
        assert!(decide(None, &other, Want::Put { named_owner: Some("front-certify") }, false).is_err(), "a machine names no other owner without admin");
        // Once it exists: the owner reads and writes; nobody else — the admin that named it included.
        assert!(decide(Some("front-certify"), &front, Want::Get, false).is_ok());
        assert!(decide(Some("front-certify"), &front, Want::Put { named_owner: None }, false).is_ok());
        assert!(decide(Some("front-certify"), &human, Want::Get, true).is_err(), "an admin reads no machine's slot");
        assert!(decide(Some("front-certify"), &human, Want::Put { named_owner: Some("front-certify") }, true).is_err(), "nor writes it");
        assert!(decide(Some("front-certify"), &other, Want::Get, true).is_err());
        assert!(decide(Some("front-certify"), &front, Want::Put { named_owner: Some("monetize") }, true).unwrap_err().contains("does not change"));
        // The owner's key as a HUMAN principal of the same id is not the owner machine.
        assert!(decide(Some("front-certify"), &who("front-certify", false), Want::Get, true).is_err());
        // Nothing for the unauthenticated, and no slot to read is no slot.
        assert!(decide(Some("front-certify"), &Caller { authenticated: false, ..front.clone() }, Want::Get, false).is_err());
        assert_eq!(decide(None, &front, Want::Get, false), Err("no such slot".into()));
    }

    #[test]
    fn a_slot_name_is_a_short_plain_word() {
        assert!(check_name("installer-material").is_ok());
        for bad in ["", "Material", "a/b", "a.b", &"x".repeat(65)] {
            assert!(check_name(bad).is_err(), "{bad:?}");
        }
    }
}