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:?}");
}
}
}