ctoolbox/workspace/ipc/auth/
capability.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// An unguessable capability token bound to a single connection/process.
5#[derive(
6    Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default,
7)]
8pub struct CapabilityToken(pub String);
9
10/// A fully-resolved capability set derived from a token.
11#[derive(Debug, Clone, Serialize, Deserialize, Default)]
12pub struct CapabilitySet {
13    /// Allowed methods per service, including optional quotas/limits.
14    pub allowed: HashMap<ServiceName, Vec<MethodRule>>,
15    /// Optional global quotas or ceilings.
16    pub global_limits: Option<GlobalLimits>,
17}
18
19/// Logical service identifier (human-readable, stable across versions).
20#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
21pub struct ServiceName(pub String);
22
23/// Pattern/rule controlling access to a method with optional quotas.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct MethodRule {
26    pub method: MethodSelector,
27    pub quotas: Option<QuotaSet>,
28}
29
30/// A method selector can be exact or wildcard.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub enum MethodSelector {
33    /// Exact service.method string, e.g., "storage.get".
34    Exact(String),
35    /// Prefix match, e.g., "network." allows "network.get" and "network.post".
36    Prefix(String),
37    /// Allow all methods in the service.
38    Any,
39}
40
41impl MethodSelector {
42    /// Check if this selector matches the given service and method.
43    pub fn matches(&self, service: &str, method: &str) -> bool {
44        let full_name = format!("{service}.{method}");
45        match self {
46            MethodSelector::Exact(s) => s == method || s == &full_name,
47            MethodSelector::Prefix(prefix) => {
48                method.starts_with(prefix) || full_name.starts_with(prefix)
49            }
50            MethodSelector::Any => true,
51        }
52    }
53}
54
55/// Quotas applicable to a method.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct QuotaSet {
58    /// Optional bytes/sec rate limit.
59    pub bytes_per_sec: Option<u64>,
60    /// Optional ops/sec limit.
61    pub ops_per_sec: Option<u64>,
62    /// Optional burst capacity.
63    pub burst: Option<u64>,
64}
65
66impl QuotaSet {
67    /// Compute the effective burst capacity for a bytes/sec token bucket.
68    ///
69    /// If `burst` is not specified, this defaults to allowing a 1-second burst
70    /// at the configured rate.
71    pub fn effective_burst_bytes(&self) -> Option<u64> {
72        let Some(rate) = self.bytes_per_sec else {
73            return None;
74        };
75        Some(self.burst.unwrap_or(rate))
76    }
77
78    /// Compute the effective burst capacity for an ops/sec token bucket.
79    ///
80    /// If `burst` is not specified, this defaults to allowing a 1-second burst
81    /// at the configured rate.
82    pub fn effective_burst_ops(&self) -> Option<u64> {
83        let Some(rate) = self.ops_per_sec else {
84            return None;
85        };
86        Some(self.burst.unwrap_or(rate))
87    }
88}
89
90/// Global limits across the connection/process.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct GlobalLimits {
93    pub max_concurrent_requests: Option<u32>,
94    pub max_streams: Option<u32>,
95    pub max_blob_bytes: Option<u64>,
96}
97
98impl GlobalLimits {
99    /// Whether this set contains any configured limits.
100    pub fn is_empty(&self) -> bool {
101        self.max_concurrent_requests.is_none()
102            && self.max_streams.is_none()
103            && self.max_blob_bytes.is_none()
104    }
105}
106
107/// A capability bundle given to the workspace to spawn a child process.
108#[derive(Debug, Clone, Serialize, Deserialize, Default)]
109pub struct CapabilityBundle {
110    /// Initial capability token to authenticate the child’s control connection.
111    pub token: CapabilityToken,
112    /// Initial capability set bound to the connection.
113    pub capabilities: CapabilitySet,
114}
115
116/// Validates a capability token and derives a bound `CapabilitySet`.
117/// Implementations should avoid panics and return errors for invalid tokens.
118pub trait TokenValidator: Send + Sync {
119    /// Validate a token, producing a `CapabilitySet` on success.
120    fn validate(
121        &self,
122        token: &CapabilityToken,
123    ) -> Result<CapabilitySet, anyhow::Error>;
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use anyhow::Result;
130
131    /// A simple fake validator for tests. Accepts "ok", rejects others.
132    struct FakeTokenValidator;
133
134    impl TokenValidator for FakeTokenValidator {
135        fn validate(
136            &self,
137            token: &CapabilityToken,
138        ) -> Result<CapabilitySet, anyhow::Error> {
139            if token.0 == "ok" {
140                Ok(CapabilitySet::default())
141            } else {
142                Err(anyhow::anyhow!("invalid token"))
143            }
144        }
145    }
146
147    #[crate::ctb_test]
148    fn fake_validator_ok() -> Result<()> {
149        let v = FakeTokenValidator;
150        let set = v.validate(&CapabilityToken("ok".into()))?;
151        let _ = set; // success
152        Ok(())
153    }
154
155    #[crate::ctb_test]
156    fn fake_validator_err() -> Result<()> {
157        let v = FakeTokenValidator;
158        let res = v.validate(&CapabilityToken("bad".into()));
159        assert!(res.is_err());
160        Ok(())
161    }
162}