gunnar-grpc 3.0.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:?}");
        }
    }
}