use super::actor::ActorId;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct LeaseScope(pub String);
impl std::fmt::Display for LeaseScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<&str> for LeaseScope {
fn from(value: &str) -> Self {
LeaseScope(value.to_string())
}
}
impl LeaseScope {
pub fn permits(&self, requested: &str) -> bool {
let (granted_ns, granted_path) = split_scope(&self.0);
let (requested_ns, requested_path) = split_scope(requested);
if granted_ns != requested_ns {
return false;
}
match granted_path {
None => true,
Some(granted_glob) => requested_path
.is_some_and(|requested_path| glob_permits(granted_glob, requested_path)),
}
}
}
fn split_scope(scope: &str) -> (&str, Option<&str>) {
match scope.split_once(':') {
Some((namespace, path)) => (namespace, Some(path)),
None => (scope, None),
}
}
fn glob_permits(granted_glob: &str, requested_path: &str) -> bool {
match granted_glob.strip_suffix("/**") {
Some(prefix) => {
requested_path == prefix || requested_path.starts_with(&format!("{prefix}/"))
}
None => granted_glob == requested_path,
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ActorLease {
id: String,
actor: ActorId,
scope: LeaseScope,
issued_by: ActorId,
issued_at_unix_secs: u64,
expires_at_unix_secs: u64,
reason: String,
}
impl ActorLease {
pub fn id(&self) -> &str {
&self.id
}
pub fn actor(&self) -> &ActorId {
&self.actor
}
pub fn scope(&self) -> &LeaseScope {
&self.scope
}
pub fn issued_by(&self) -> &ActorId {
&self.issued_by
}
pub fn issued_at_unix_secs(&self) -> u64 {
self.issued_at_unix_secs
}
pub fn expires_at_unix_secs(&self) -> u64 {
self.expires_at_unix_secs
}
pub fn reason(&self) -> &str {
&self.reason
}
pub fn covers(&self, requested: &str, now_unix_secs: u64) -> bool {
is_active(self, now_unix_secs) && self.scope.permits(requested)
}
}
const SOVEREIGN_SCOPE: &str = "sovereign";
pub struct GrantRequest {
pub actor: ActorId,
pub scope: LeaseScope,
pub issued_by: ActorId,
pub issued_at_unix_secs: u64,
pub ttl_secs: u64,
pub reason: String,
}
pub fn grant(request: GrantRequest) -> anyhow::Result<ActorLease> {
if request.scope.0.eq_ignore_ascii_case(SOVEREIGN_SCOPE) {
anyhow::bail!("a lease can never grant sovereign authority");
}
Ok(ActorLease {
id: Uuid::new_v4().to_string(),
actor: request.actor,
scope: request.scope,
issued_by: request.issued_by,
issued_at_unix_secs: request.issued_at_unix_secs,
expires_at_unix_secs: request.issued_at_unix_secs.saturating_add(request.ttl_secs),
reason: request.reason,
})
}
pub fn is_active(lease: &ActorLease, now_unix_secs: u64) -> bool {
now_unix_secs < lease.expires_at_unix_secs()
}
#[cfg(test)]
mod tests {
use super::*;
fn actor(id: &str) -> ActorId {
ActorId(id.to_string())
}
fn request(scope: &str, issued_at: u64, ttl_secs: u64) -> GrantRequest {
GrantRequest {
actor: actor("agent-1"),
scope: LeaseScope(scope.into()),
issued_by: actor("supervisor"),
issued_at_unix_secs: issued_at,
ttl_secs,
reason: "test".into(),
}
}
#[test]
fn grant_rejects_sovereign_scope_case_insensitively() {
for scope in ["sovereign", "Sovereign", "SOVEREIGN"] {
let error = grant(request(scope, 0, 60)).unwrap_err();
assert!(error.to_string().contains("sovereign"));
}
}
#[test]
fn grant_accepts_an_ordinary_scoped_capability() {
let lease = grant(request("repo.read", 1000, 60)).unwrap();
assert_eq!(lease.scope(), &LeaseScope("repo.read".into()));
assert_eq!(lease.expires_at_unix_secs(), 1060);
assert!(!lease.id().is_empty());
}
#[test]
fn is_active_reflects_the_half_open_expiry_boundary() {
let lease = grant(request("repo.read", 1000, 60)).unwrap();
assert!(is_active(&lease, 1059));
assert!(!is_active(&lease, 1060));
assert!(!is_active(&lease, 2000));
}
#[test]
fn ttl_saturates_instead_of_overflowing_at_the_u64_boundary() {
let lease = grant(request("repo.read", u64::MAX - 1, 100)).unwrap();
assert_eq!(lease.expires_at_unix_secs(), u64::MAX);
}
#[test]
fn actor_lease_fields_are_private_so_grant_is_the_only_constructor() {
let lease = grant(request("repo.read", 0, 60)).unwrap();
assert_eq!(lease.actor(), &actor("agent-1"));
assert_eq!(lease.issued_by(), &actor("supervisor"));
assert_eq!(lease.issued_at_unix_secs(), 0);
assert_eq!(lease.reason(), "test");
}
#[test]
fn permits_matches_the_same_unqualified_namespace() {
assert!(LeaseScope("repo.read".into()).permits("repo.read"));
}
#[test]
fn permits_denies_a_different_namespace() {
assert!(!LeaseScope("repo.read".into()).permits("repo.write:src/lib.rs"));
}
#[test]
fn permits_whole_namespace_grant_covers_any_path_scoped_request() {
assert!(LeaseScope("repo.write".into()).permits("repo.write:src/lib.rs"));
assert!(LeaseScope("repo.write".into()).permits("repo.write"));
}
#[test]
fn permits_glob_covers_the_prefix_itself_and_everything_under_it() {
let scope = LeaseScope("repo.write:src/**".into());
assert!(scope.permits("repo.write:src"));
assert!(scope.permits("repo.write:src/lib.rs"));
assert!(scope.permits("repo.write:src/os/identity/lease.rs"));
}
#[test]
fn permits_glob_denies_a_path_outside_the_prefix() {
let scope = LeaseScope("repo.write:src/**".into());
assert!(!scope.permits("repo.write:secrets.env"));
assert!(!scope.permits("repo.write:srcbackup/lib.rs"));
}
#[test]
fn permits_denies_an_unqualified_request_against_a_path_scoped_grant() {
let scope = LeaseScope("repo.write:src/**".into());
assert!(!scope.permits("repo.write"));
}
#[test]
fn covers_combines_expiry_and_scope_matching() {
let lease = grant(request("repo.write:src/**", 1000, 60)).unwrap();
assert!(lease.covers("repo.write:src/lib.rs", 1059));
assert!(!lease.covers("repo.write:src/lib.rs", 1060), "expired");
assert!(!lease.covers("repo.write:secrets.env", 1000), "wrong path");
}
}