use std::collections::BTreeSet;
use std::fmt;
use std::rc::Rc;
use crate::budget::Budget;
use crate::error::{Error, Result};
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct CapId(pub [u8; 16]);
impl CapId {
pub fn derive(secret: &[u8; 32], serial: u64, parent: Option<&CapId>) -> CapId {
let mut hasher = blake3::Hasher::new();
hasher.update(b"active-rpc/cap");
hasher.update(secret);
hasher.update(&serial.to_le_bytes());
hasher.update(&parent.map(|id| id.0).unwrap_or([0u8; 16]));
let digest = hasher.finalize();
let mut id = [0u8; 16];
id.copy_from_slice(&digest.as_bytes()[..16]);
CapId(id)
}
pub fn hex(self) -> String {
let mut out = String::with_capacity(32);
for byte in self.0 {
out.push_str(&format!("{byte:02x}"));
}
out
}
pub fn correlation_id(self) -> String {
correlation(&self.0)
}
}
fn correlation(bytes: &[u8; 16]) -> String {
let mut hasher = blake3::Hasher::new();
hasher.update(b"active-rpc/capid-correlation");
hasher.update(bytes);
let digest = hasher.finalize();
let mut out = String::with_capacity(16);
for byte in &digest.as_bytes()[..8] {
out.push_str(&format!("{byte:02x}"));
}
out
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct CapToken(pub u128);
impl CapToken {
pub fn correlation_id(self) -> String {
correlation(&self.0.to_be_bytes())
}
}
impl fmt::Debug for CapToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "CapToken({})", self.correlation_id())
}
}
impl fmt::Display for CapToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.correlation_id())
}
}
impl fmt::Debug for CapId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "CapId({})", self.correlation_id())
}
}
impl fmt::Display for CapId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.correlation_id())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ProviderId(pub u64);
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ToolPolicy {
pub allowed: BTreeSet<String>,
pub max_arg_bytes: u64,
pub allow_process: bool,
}
impl ToolPolicy {
pub fn allow(names: &[&str]) -> Self {
Self {
allowed: names.iter().map(|name| name.to_string()).collect(),
max_arg_bytes: 1 << 20,
allow_process: false,
}
}
pub fn is_attenuation_of(&self, parent: &ToolPolicy) -> bool {
self.allowed.is_subset(&parent.allowed)
&& self.max_arg_bytes <= parent.max_arg_bytes
&& (!self.allow_process || parent.allow_process)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CapAttrs {
pub methods: BTreeSet<String>,
pub budget: Budget,
pub read_only: bool,
pub redact: bool,
pub path_scope: Option<String>,
pub tool_policy: Option<ToolPolicy>,
}
impl CapAttrs {
pub fn new(methods: &[&str]) -> Self {
Self {
methods: methods.iter().map(|method| method.to_string()).collect(),
budget: Budget::default(),
read_only: false,
redact: false,
path_scope: None,
tool_policy: None,
}
}
pub fn with_methods(mut self, methods: &[&str]) -> Self {
self.methods = methods.iter().map(|method| method.to_string()).collect();
self
}
pub fn with_budget(mut self, budget: Budget) -> Self {
self.budget = budget;
self
}
pub fn with_path(mut self, path: impl Into<String>) -> Self {
self.path_scope = Some(path.into());
self
}
pub fn read_only(mut self) -> Self {
self.read_only = true;
self
}
pub fn redacted(mut self) -> Self {
self.redact = true;
self
}
pub fn with_tool_policy(mut self, policy: ToolPolicy) -> Self {
self.tool_policy = Some(policy);
self
}
pub fn is_attenuation_of(&self, parent: &CapAttrs) -> bool {
let methods_ok = self.methods.is_subset(&parent.methods) && !self.methods.is_empty();
let budget_ok = self.budget.is_attenuation_of(&parent.budget);
let read_only_ok = !parent.read_only || self.read_only;
let redact_ok = !parent.redact || self.redact;
let path_ok = match (&self.path_scope, &parent.path_scope) {
(None, None) => true,
(Some(_), None) => true,
(Some(child), Some(parent)) => is_path_prefix(parent, child),
(None, Some(_)) => false,
};
let tool_ok = match (&self.tool_policy, &parent.tool_policy) {
(_, None) => true,
(None, Some(_)) => false,
(Some(policy), Some(parent)) => policy.is_attenuation_of(parent),
};
methods_ok && budget_ok && read_only_ok && redact_ok && path_ok && tool_ok
}
pub fn attenuate(&self, child: CapAttrs) -> Result<CapAttrs> {
if child.methods.is_empty() {
return Err(Error::InvalidArgument("attenuation grants no methods"));
}
if !child.methods.is_subset(&self.methods) {
return Err(Error::Escalation("method set grows"));
}
if !child.budget.is_attenuation_of(&self.budget) {
return Err(Error::Escalation("budget grows"));
}
if self.read_only && !child.read_only {
return Err(Error::Escalation("read-only cannot be lifted"));
}
if self.redact && !child.redact {
return Err(Error::Escalation("redaction cannot be lifted"));
}
match (&child.path_scope, &self.path_scope) {
(_, None) => {}
(Some(child_path), Some(parent)) if is_path_prefix(parent, child_path) => {}
(Some(_), Some(_)) => return Err(Error::Escalation("path scope widens")),
(None, Some(_)) => return Err(Error::Escalation("path scope removed")),
}
match (&child.tool_policy, &self.tool_policy) {
(_, None) => {}
(Some(policy), Some(parent)) if policy.is_attenuation_of(parent) => {}
_ => return Err(Error::Escalation("tool policy widens")),
}
let mut attrs = child;
attrs.budget = attrs.budget.intersect(&self.budget);
Ok(attrs)
}
pub fn grants(&self, method: &str) -> bool {
self.methods.contains(method)
}
pub fn covers_path(&self, path: &str) -> bool {
match &self.path_scope {
None => true,
Some(scope) => is_path_prefix(scope, path),
}
}
}
pub fn is_path_prefix(parent: &str, child: &str) -> bool {
if parent == child {
return true;
}
child
.strip_prefix(parent)
.map(|rest| rest.starts_with('.'))
.unwrap_or(false)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CapRecord {
pub id: CapId,
pub provider: ProviderId,
pub object: u64,
pub attrs: CapAttrs,
pub parent: Option<CapId>,
pub refs: u32,
pub revoked: bool,
}
impl CapRecord {
pub fn new(id: CapId, provider: ProviderId, object: u64, attrs: CapAttrs) -> Self {
Self {
id,
provider,
object,
attrs,
parent: None,
refs: 1,
revoked: false,
}
}
}
pub trait CapControl {
fn retain(&self, id: CapId);
fn release(&self, id: CapId);
}
pub struct CapHandle {
control: Rc<dyn CapControl>,
id: CapId,
owning: bool,
}
impl CapHandle {
pub fn new(control: Rc<dyn CapControl>, id: CapId) -> Self {
Self {
control,
id,
owning: true,
}
}
pub fn borrowed(control: Rc<dyn CapControl>, id: CapId) -> Self {
Self {
control,
id,
owning: false,
}
}
pub fn id(&self) -> CapId {
self.id
}
pub fn into_id(mut self) -> CapId {
self.owning = false;
self.id
}
}
impl Clone for CapHandle {
fn clone(&self) -> Self {
if self.owning {
self.control.retain(self.id);
}
Self {
control: Rc::clone(&self.control),
id: self.id,
owning: self.owning,
}
}
}
impl Drop for CapHandle {
fn drop(&mut self) {
if self.owning {
self.control.release(self.id);
}
}
}
impl fmt::Debug for CapHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CapHandle").field("id", &self.id).finish()
}
}