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;
pub const MAX_VALUE: usize = 256 * 1024;
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"))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Want<'a> {
Get,
Put { named_owner: Option<&'a str> },
}
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())));
}
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 }
}
#[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);
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");
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");
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"));
assert!(decide(Some("front-certify"), &who("front-certify", false), Want::Get, true).is_err());
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:?}");
}
}
}