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 {
48 let mut out = String::with_capacity(32);
49 for byte in self.0 {
50 out.push_str(&format!("{byte:02x}"));
51 }
52 out
53 }
54
55 pub fn correlation_id(self) -> String {
60 correlation(&self.0)
61 }
62}
63
64fn correlation(bytes: &[u8; 16]) -> String {
66 let mut hasher = blake3::Hasher::new();
67 hasher.update(b"active-rpc/capid-correlation");
68 hasher.update(bytes);
69 let digest = hasher.finalize();
70 let mut out = String::with_capacity(16);
71 for byte in &digest.as_bytes()[..8] {
72 out.push_str(&format!("{byte:02x}"));
73 }
74 out
75}
76
77#[derive(Clone, Copy, PartialEq, Eq, Hash)]
83pub struct CapToken(pub u128);
84
85impl CapToken {
86 pub fn correlation_id(self) -> String {
88 correlation(&self.0.to_be_bytes())
89 }
90}
91
92impl fmt::Debug for CapToken {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 write!(f, "CapToken({})", self.correlation_id())
95 }
96}
97
98impl fmt::Display for CapToken {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 f.write_str(&self.correlation_id())
101 }
102}
103
104impl fmt::Debug for CapId {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 write!(f, "CapId({})", self.correlation_id())
109 }
110}
111
112impl fmt::Display for CapId {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 f.write_str(&self.correlation_id())
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
122pub struct ProviderId(pub u64);
123
124#[derive(Debug, Clone, PartialEq, Eq, Default)]
126pub struct ToolPolicy {
127 pub allowed: BTreeSet<String>,
129 pub max_arg_bytes: u64,
131 pub allow_process: bool,
133}
134
135impl ToolPolicy {
136 pub fn allow(names: &[&str]) -> Self {
138 Self {
139 allowed: names.iter().map(|name| name.to_string()).collect(),
140 max_arg_bytes: 1 << 20,
141 allow_process: false,
142 }
143 }
144
145 pub fn is_attenuation_of(&self, parent: &ToolPolicy) -> bool {
147 self.allowed.is_subset(&parent.allowed)
148 && self.max_arg_bytes <= parent.max_arg_bytes
149 && (!self.allow_process || parent.allow_process)
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct CapAttrs {
156 pub methods: BTreeSet<String>,
158 pub budget: Budget,
160 pub read_only: bool,
162 pub redact: bool,
164 pub path_scope: Option<String>,
166 pub tool_policy: Option<ToolPolicy>,
168}
169
170impl CapAttrs {
171 pub fn new(methods: &[&str]) -> Self {
173 Self {
174 methods: methods.iter().map(|method| method.to_string()).collect(),
175 budget: Budget::default(),
176 read_only: false,
177 redact: false,
178 path_scope: None,
179 tool_policy: None,
180 }
181 }
182
183 pub fn with_methods(mut self, methods: &[&str]) -> Self {
185 self.methods = methods.iter().map(|method| method.to_string()).collect();
186 self
187 }
188
189 pub fn with_budget(mut self, budget: Budget) -> Self {
191 self.budget = budget;
192 self
193 }
194
195 pub fn with_path(mut self, path: impl Into<String>) -> Self {
197 self.path_scope = Some(path.into());
198 self
199 }
200
201 pub fn read_only(mut self) -> Self {
203 self.read_only = true;
204 self
205 }
206
207 pub fn redacted(mut self) -> Self {
209 self.redact = true;
210 self
211 }
212
213 pub fn with_tool_policy(mut self, policy: ToolPolicy) -> Self {
215 self.tool_policy = Some(policy);
216 self
217 }
218
219 pub fn is_attenuation_of(&self, parent: &CapAttrs) -> bool {
221 let methods_ok = self.methods.is_subset(&parent.methods) && !self.methods.is_empty();
222 let budget_ok = self.budget.is_attenuation_of(&parent.budget);
223 let read_only_ok = !parent.read_only || self.read_only;
224 let redact_ok = !parent.redact || self.redact;
225 let path_ok = match (&self.path_scope, &parent.path_scope) {
226 (None, None) => true,
227 (Some(_), None) => true,
228 (Some(child), Some(parent)) => is_path_prefix(parent, child),
229 (None, Some(_)) => false,
230 };
231 let tool_ok = match (&self.tool_policy, &parent.tool_policy) {
232 (_, None) => true,
235 (None, Some(_)) => false,
237 (Some(policy), Some(parent)) => policy.is_attenuation_of(parent),
238 };
239 methods_ok && budget_ok && read_only_ok && redact_ok && path_ok && tool_ok
240 }
241
242 pub fn attenuate(&self, child: CapAttrs) -> Result<CapAttrs> {
244 if child.methods.is_empty() {
245 return Err(Error::InvalidArgument("attenuation grants no methods"));
246 }
247 if !child.methods.is_subset(&self.methods) {
248 return Err(Error::Escalation("method set grows"));
249 }
250 if !child.budget.is_attenuation_of(&self.budget) {
251 return Err(Error::Escalation("budget grows"));
252 }
253 if self.read_only && !child.read_only {
254 return Err(Error::Escalation("read-only cannot be lifted"));
255 }
256 if self.redact && !child.redact {
257 return Err(Error::Escalation("redaction cannot be lifted"));
258 }
259 match (&child.path_scope, &self.path_scope) {
260 (_, None) => {}
261 (Some(child_path), Some(parent)) if is_path_prefix(parent, child_path) => {}
262 (Some(_), Some(_)) => return Err(Error::Escalation("path scope widens")),
263 (None, Some(_)) => return Err(Error::Escalation("path scope removed")),
264 }
265 match (&child.tool_policy, &self.tool_policy) {
266 (_, None) => {}
267 (Some(policy), Some(parent)) if policy.is_attenuation_of(parent) => {}
268 _ => return Err(Error::Escalation("tool policy widens")),
269 }
270 let mut attrs = child;
271 attrs.budget = attrs.budget.intersect(&self.budget);
272 Ok(attrs)
273 }
274
275 pub fn grants(&self, method: &str) -> bool {
277 self.methods.contains(method)
278 }
279
280 pub fn covers_path(&self, path: &str) -> bool {
282 match &self.path_scope {
283 None => true,
284 Some(scope) => is_path_prefix(scope, path),
285 }
286 }
287}
288
289pub fn is_path_prefix(parent: &str, child: &str) -> bool {
291 if parent == child {
292 return true;
293 }
294 child
295 .strip_prefix(parent)
296 .map(|rest| rest.starts_with('.'))
297 .unwrap_or(false)
298}
299
300#[derive(Debug, Clone, PartialEq, Eq)]
302pub struct CapRecord {
303 pub id: CapId,
305 pub provider: ProviderId,
307 pub object: u64,
309 pub attrs: CapAttrs,
311 pub parent: Option<CapId>,
313 pub refs: u32,
315 pub revoked: bool,
317}
318
319impl CapRecord {
320 pub fn new(id: CapId, provider: ProviderId, object: u64, attrs: CapAttrs) -> Self {
322 Self {
323 id,
324 provider,
325 object,
326 attrs,
327 parent: None,
328 refs: 1,
329 revoked: false,
330 }
331 }
332}
333
334pub trait CapControl {
339 fn retain(&self, id: CapId);
341 fn release(&self, id: CapId);
343}
344
345pub struct CapHandle {
350 control: Rc<dyn CapControl>,
351 id: CapId,
352 owning: bool,
354}
355
356impl CapHandle {
357 pub fn new(control: Rc<dyn CapControl>, id: CapId) -> Self {
359 Self {
360 control,
361 id,
362 owning: true,
363 }
364 }
365
366 pub fn borrowed(control: Rc<dyn CapControl>, id: CapId) -> Self {
368 Self {
369 control,
370 id,
371 owning: false,
372 }
373 }
374
375 pub fn id(&self) -> CapId {
377 self.id
378 }
379
380 pub fn into_id(mut self) -> CapId {
385 self.owning = false;
386 self.id
387 }
388}
389
390impl Clone for CapHandle {
391 fn clone(&self) -> Self {
396 if self.owning {
397 self.control.retain(self.id);
398 }
399 Self {
400 control: Rc::clone(&self.control),
401 id: self.id,
402 owning: self.owning,
403 }
404 }
405}
406
407impl Drop for CapHandle {
408 fn drop(&mut self) {
409 if self.owning {
410 self.control.release(self.id);
411 }
412 }
413}
414
415impl fmt::Debug for CapHandle {
416 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
417 f.debug_struct("CapHandle").field("id", &self.id).finish()
418 }
419}