1use std::collections::BTreeSet;
14use std::fmt;
15use std::rc::Rc;
16
17use crate::budget::Budget;
18use crate::error::{Error, Result};
19
20#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
22pub struct CapId(pub [u8; 16]);
23
24impl CapId {
25 pub fn derive(secret: &[u8; 32], serial: u64, parent: Option<&CapId>) -> CapId {
30 let mut hasher = blake3::Hasher::new();
31 hasher.update(b"active-rpc/cap");
32 hasher.update(secret);
33 hasher.update(&serial.to_le_bytes());
34 hasher.update(&parent.map(|id| id.0).unwrap_or([0u8; 16]));
35 let digest = hasher.finalize();
36 let mut id = [0u8; 16];
37 id.copy_from_slice(&digest.as_bytes()[..16]);
38 CapId(id)
39 }
40
41 pub fn hex(self) -> String {
43 let mut out = String::with_capacity(32);
44 for byte in self.0 {
45 out.push_str(&format!("{byte:02x}"));
46 }
47 out
48 }
49}
50
51impl fmt::Debug for CapId {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 write!(f, "CapId({})", self.hex())
54 }
55}
56
57impl fmt::Display for CapId {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 f.write_str(&self.hex())
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
65pub struct ProviderId(pub u64);
66
67#[derive(Debug, Clone, PartialEq, Eq, Default)]
69pub struct ToolPolicy {
70 pub allowed: BTreeSet<String>,
72 pub max_arg_bytes: u64,
74 pub allow_process: bool,
76}
77
78impl ToolPolicy {
79 pub fn allow(names: &[&str]) -> Self {
81 Self {
82 allowed: names.iter().map(|name| name.to_string()).collect(),
83 max_arg_bytes: 1 << 20,
84 allow_process: false,
85 }
86 }
87
88 pub fn is_attenuation_of(&self, parent: &ToolPolicy) -> bool {
90 self.allowed.is_subset(&parent.allowed)
91 && self.max_arg_bytes <= parent.max_arg_bytes
92 && (!self.allow_process || parent.allow_process)
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct CapAttrs {
99 pub methods: BTreeSet<String>,
101 pub budget: Budget,
103 pub read_only: bool,
105 pub redact: bool,
107 pub path_scope: Option<String>,
109 pub tool_policy: Option<ToolPolicy>,
111}
112
113impl CapAttrs {
114 pub fn new(methods: &[&str]) -> Self {
116 Self {
117 methods: methods.iter().map(|method| method.to_string()).collect(),
118 budget: Budget::default(),
119 read_only: false,
120 redact: false,
121 path_scope: None,
122 tool_policy: None,
123 }
124 }
125
126 pub fn with_methods(mut self, methods: &[&str]) -> Self {
128 self.methods = methods.iter().map(|method| method.to_string()).collect();
129 self
130 }
131
132 pub fn with_budget(mut self, budget: Budget) -> Self {
134 self.budget = budget;
135 self
136 }
137
138 pub fn with_path(mut self, path: impl Into<String>) -> Self {
140 self.path_scope = Some(path.into());
141 self
142 }
143
144 pub fn read_only(mut self) -> Self {
146 self.read_only = true;
147 self
148 }
149
150 pub fn redacted(mut self) -> Self {
152 self.redact = true;
153 self
154 }
155
156 pub fn with_tool_policy(mut self, policy: ToolPolicy) -> Self {
158 self.tool_policy = Some(policy);
159 self
160 }
161
162 pub fn is_attenuation_of(&self, parent: &CapAttrs) -> bool {
164 let methods_ok = self.methods.is_subset(&parent.methods) && !self.methods.is_empty();
165 let budget_ok = self.budget.is_attenuation_of(&parent.budget);
166 let read_only_ok = !parent.read_only || self.read_only;
167 let redact_ok = !parent.redact || self.redact;
168 let path_ok = match (&self.path_scope, &parent.path_scope) {
169 (None, None) => true,
170 (Some(_), None) => true,
171 (Some(child), Some(parent)) => is_path_prefix(parent, child),
172 (None, Some(_)) => false,
173 };
174 let tool_ok = match (&self.tool_policy, &parent.tool_policy) {
175 (_, None) => true,
178 (None, Some(_)) => false,
180 (Some(policy), Some(parent)) => policy.is_attenuation_of(parent),
181 };
182 methods_ok && budget_ok && read_only_ok && redact_ok && path_ok && tool_ok
183 }
184
185 pub fn attenuate(&self, child: CapAttrs) -> Result<CapAttrs> {
187 if child.methods.is_empty() {
188 return Err(Error::InvalidArgument("attenuation grants no methods"));
189 }
190 if !child.methods.is_subset(&self.methods) {
191 return Err(Error::Escalation("method set grows"));
192 }
193 if !child.budget.is_attenuation_of(&self.budget) {
194 return Err(Error::Escalation("budget grows"));
195 }
196 if self.read_only && !child.read_only {
197 return Err(Error::Escalation("read-only cannot be lifted"));
198 }
199 if self.redact && !child.redact {
200 return Err(Error::Escalation("redaction cannot be lifted"));
201 }
202 match (&child.path_scope, &self.path_scope) {
203 (_, None) => {}
204 (Some(child_path), Some(parent)) if is_path_prefix(parent, child_path) => {}
205 (Some(_), Some(_)) => return Err(Error::Escalation("path scope widens")),
206 (None, Some(_)) => return Err(Error::Escalation("path scope removed")),
207 }
208 match (&child.tool_policy, &self.tool_policy) {
209 (_, None) => {}
210 (Some(policy), Some(parent)) if policy.is_attenuation_of(parent) => {}
211 _ => return Err(Error::Escalation("tool policy widens")),
212 }
213 let mut attrs = child;
214 attrs.budget = attrs.budget.intersect(&self.budget);
215 Ok(attrs)
216 }
217
218 pub fn grants(&self, method: &str) -> bool {
220 self.methods.contains(method)
221 }
222
223 pub fn covers_path(&self, path: &str) -> bool {
225 match &self.path_scope {
226 None => true,
227 Some(scope) => is_path_prefix(scope, path),
228 }
229 }
230}
231
232pub fn is_path_prefix(parent: &str, child: &str) -> bool {
234 if parent == child {
235 return true;
236 }
237 child
238 .strip_prefix(parent)
239 .map(|rest| rest.starts_with('.'))
240 .unwrap_or(false)
241}
242
243#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct CapRecord {
246 pub id: CapId,
248 pub provider: ProviderId,
250 pub object: u64,
252 pub attrs: CapAttrs,
254 pub parent: Option<CapId>,
256 pub refs: u32,
258 pub revoked: bool,
260}
261
262impl CapRecord {
263 pub fn new(id: CapId, provider: ProviderId, object: u64, attrs: CapAttrs) -> Self {
265 Self {
266 id,
267 provider,
268 object,
269 attrs,
270 parent: None,
271 refs: 1,
272 revoked: false,
273 }
274 }
275}
276
277pub trait CapControl {
282 fn retain(&self, id: CapId);
284 fn release(&self, id: CapId);
286}
287
288pub struct CapHandle {
293 control: Rc<dyn CapControl>,
294 id: CapId,
295 owning: bool,
297}
298
299impl CapHandle {
300 pub fn new(control: Rc<dyn CapControl>, id: CapId) -> Self {
302 Self {
303 control,
304 id,
305 owning: true,
306 }
307 }
308
309 pub fn borrowed(control: Rc<dyn CapControl>, id: CapId) -> Self {
311 Self {
312 control,
313 id,
314 owning: false,
315 }
316 }
317
318 pub fn id(&self) -> CapId {
319 self.id
320 }
321
322 pub fn into_id(mut self) -> CapId {
327 self.owning = false;
328 self.id
329 }
330}
331
332impl Clone for CapHandle {
333 fn clone(&self) -> Self {
338 if self.owning {
339 self.control.retain(self.id);
340 }
341 Self {
342 control: Rc::clone(&self.control),
343 id: self.id,
344 owning: self.owning,
345 }
346 }
347}
348
349impl Drop for CapHandle {
350 fn drop(&mut self) {
351 if self.owning {
352 self.control.release(self.id);
353 }
354 }
355}
356
357impl fmt::Debug for CapHandle {
358 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359 f.debug_struct("CapHandle")
360 .field("id", &self.id.hex())
361 .finish()
362 }
363}