Skip to main content

iroh_services/
caps.rs

1use std::{collections::BTreeSet, fmt, str::FromStr};
2
3use anyhow::{Context, Result, bail};
4use iroh::{EndpointId, SecretKey};
5use n0_future::time::Duration;
6use rcan::{Capability, Expires, Rcan};
7use serde::{Deserialize, Serialize};
8
9pub(crate) const DEFAULT_CAP_EXPIRY: Duration = Duration::from_hours(24 * 30); // 1 month
10
11macro_rules! cap_enum(
12    ($enum:item) => {
13        #[derive(
14            Debug,
15            Eq,
16            PartialEq,
17            Ord,
18            PartialOrd,
19            Serialize,
20            Deserialize,
21            Clone,
22            Copy,
23            strum::Display,
24            strum::EnumString,
25        )]
26        #[strum(serialize_all = "kebab-case")]
27        #[serde(rename_all = "kebab-case")]
28        $enum
29    }
30);
31
32#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)]
33#[serde(rename_all = "kebab-case")]
34pub enum Caps {
35    V0(CapSet<Cap>),
36}
37
38impl Default for Caps {
39    fn default() -> Self {
40        Self::V0(CapSet::default())
41    }
42}
43
44impl std::ops::Deref for Caps {
45    type Target = CapSet<Cap>;
46
47    fn deref(&self) -> &Self::Target {
48        let Self::V0(slf) = self;
49        slf
50    }
51}
52
53/// A capability is the capacity to do something. Capabilities are embedded
54/// within signed tokens that dictate who created them, and who they apply to.
55/// Caps follow the [object capability model], where possession of a valid
56/// capability token is the canonical source of authorization. This is different
57/// from an access control list approach where users authenticate, and their
58/// current set of capabilities are stored within a database.
59///
60/// [object capability model]: https://en.wikipedia.org/wiki/Object-capability_model
61#[derive(
62    Debug,
63    Eq,
64    PartialEq,
65    Ord,
66    PartialOrd,
67    Serialize,
68    Deserialize,
69    Clone,
70    Copy,
71    derive_more::From,
72    strum::Display,
73)]
74#[serde(rename_all = "kebab-case")]
75pub enum Cap {
76    #[strum(to_string = "all")]
77    All,
78    #[strum(to_string = "client")]
79    Client,
80    #[strum(to_string = "relay:{0}")]
81    Relay(RelayCap),
82    #[strum(to_string = "metrics:{0}")]
83    Metrics(MetricsCap),
84    #[strum(to_string = "net-diagnostics:{0}")]
85    NetDiagnostics(NetDiagnosticsCap),
86    #[strum(to_string = "logs:{0}")]
87    Logs(LogsCap),
88}
89
90impl FromStr for Cap {
91    type Err = anyhow::Error;
92
93    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
94        if s == "all" {
95            Ok(Self::All)
96        } else if let Some((domain, inner)) = s.split_once(":") {
97            Ok(match domain {
98                "metrics" => Self::Metrics(MetricsCap::from_str(inner)?),
99                "relay" => Self::Relay(RelayCap::from_str(inner)?),
100                "net-diagnostics" => Self::NetDiagnostics(NetDiagnosticsCap::from_str(inner)?),
101                "logs" => Self::Logs(LogsCap::from_str(inner)?),
102                _ => bail!("invalid cap domain"),
103            })
104        } else {
105            Err(anyhow::anyhow!("invalid cap string"))
106        }
107    }
108}
109
110cap_enum!(
111    pub enum MetricsCap {
112        PutAny,
113    }
114);
115
116cap_enum!(
117    pub enum RelayCap {
118        Use,
119    }
120);
121
122cap_enum!(
123    pub enum NetDiagnosticsCap {
124        PutAny,
125        GetAny,
126    }
127);
128
129cap_enum!(
130    /// Capabilities for the log collection feature.
131    pub enum LogsCap {
132        /// Permits the bearer to push log lines to the cloud.
133        Push,
134        /// Permits the bearer to set the log level filter on the issuer at runtime.
135        SetLevel,
136        /// Permits the bearer to ask the issuer for the contents of its
137        /// local rolling log file.
138        Fetch,
139    }
140);
141
142impl Caps {
143    pub fn new(caps: impl IntoIterator<Item = impl Into<Cap>>) -> Self {
144        Self::V0(CapSet::new(caps))
145    }
146
147    /// the class of capabilities that iroh-services will accept when deriving from a
148    /// shared secret like an [ApiSecret]. These should be "client" capabilities:
149    /// typically for users of an app
150    ///
151    /// [ApiSecret]: crate::api_secret::ApiSecret
152    pub fn for_shared_secret() -> Self {
153        Self::new([Cap::Client])
154    }
155
156    /// The maximum set of capabilities. iroh-services will only accept these capabilities
157    /// when deriving from a secret that is registered with iroh-services, like an SSH key
158    pub fn all() -> Self {
159        Self::new([Cap::All])
160    }
161
162    pub fn extend(self, caps: impl IntoIterator<Item = impl Into<Cap>>) -> Self {
163        let Self::V0(mut set) = self;
164        set.extend(caps.into_iter().map(Into::into));
165        Self::V0(set)
166    }
167
168    pub fn from_strs<'a>(strs: impl IntoIterator<Item = &'a str>) -> Result<Self> {
169        Ok(Self::V0(CapSet::from_strs(strs)?))
170    }
171
172    pub fn to_strings(&self) -> Vec<String> {
173        let Self::V0(set) = self;
174        set.to_strings()
175    }
176}
177
178impl Capability for Caps {
179    fn permits(&self, other: &Self) -> bool {
180        let Self::V0(slf) = self;
181        let Self::V0(other) = other;
182        slf.permits(other)
183    }
184}
185
186impl From<Cap> for Caps {
187    fn from(cap: Cap) -> Self {
188        Self::new([cap])
189    }
190}
191
192impl Capability for Cap {
193    fn permits(&self, other: &Self) -> bool {
194        match (self, other) {
195            (Cap::All, _) => true,
196            (Cap::Client, other) => client_capabilities(other),
197            (Cap::Relay(slf), Cap::Relay(other)) => slf.permits(other),
198            (Cap::Metrics(slf), Cap::Metrics(other)) => slf.permits(other),
199            (Cap::NetDiagnostics(slf), Cap::NetDiagnostics(other)) => slf.permits(other),
200            (Cap::Logs(slf), Cap::Logs(other)) => slf.permits(other),
201            (_, _) => false,
202        }
203    }
204}
205
206fn client_capabilities(other: &Cap) -> bool {
207    match other {
208        Cap::All => false,
209        Cap::Client => true,
210        Cap::Relay(RelayCap::Use) => true,
211        Cap::Metrics(MetricsCap::PutAny) => true,
212        Cap::NetDiagnostics(NetDiagnosticsCap::PutAny) => true,
213        Cap::NetDiagnostics(NetDiagnosticsCap::GetAny) => true,
214        Cap::Logs(LogsCap::Push) => true,
215        Cap::Logs(LogsCap::SetLevel) => true,
216        Cap::Logs(LogsCap::Fetch) => true,
217    }
218}
219
220impl Capability for MetricsCap {
221    fn permits(&self, other: &Self) -> bool {
222        match (self, other) {
223            (MetricsCap::PutAny, MetricsCap::PutAny) => true,
224        }
225    }
226}
227
228impl Capability for RelayCap {
229    fn permits(&self, other: &Self) -> bool {
230        match (self, other) {
231            (RelayCap::Use, RelayCap::Use) => true,
232        }
233    }
234}
235
236impl Capability for NetDiagnosticsCap {
237    fn permits(&self, other: &Self) -> bool {
238        match (self, other) {
239            (NetDiagnosticsCap::PutAny, NetDiagnosticsCap::PutAny) => true,
240            (NetDiagnosticsCap::GetAny, NetDiagnosticsCap::GetAny) => true,
241            (_, _) => false,
242        }
243    }
244}
245
246impl Capability for LogsCap {
247    fn permits(&self, other: &Self) -> bool {
248        match (self, other) {
249            (LogsCap::Push, LogsCap::Push) => true,
250            (LogsCap::SetLevel, LogsCap::SetLevel) => true,
251            (LogsCap::Fetch, LogsCap::Fetch) => true,
252            (_, _) => false,
253        }
254    }
255}
256
257/// A set of capabilities
258#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Serialize, Deserialize)]
259pub struct CapSet<C: Capability + Ord>(BTreeSet<C>);
260
261impl<C: Capability + Ord> Default for CapSet<C> {
262    fn default() -> Self {
263        Self(BTreeSet::new())
264    }
265}
266
267impl<C: Capability + Ord> CapSet<C> {
268    pub fn new(set: impl IntoIterator<Item = impl Into<C>>) -> Self {
269        Self(BTreeSet::from_iter(set.into_iter().map(Into::into)))
270    }
271
272    pub fn iter(&self) -> impl Iterator<Item = &'_ C> + '_ {
273        self.0.iter()
274    }
275
276    pub fn is_empty(&self) -> bool {
277        self.0.is_empty()
278    }
279
280    pub fn len(&self) -> usize {
281        self.0.len()
282    }
283
284    pub fn contains(&self, cap: impl Into<C>) -> bool {
285        let cap = cap.into();
286        self.0.contains(&cap)
287    }
288
289    pub fn extend(&mut self, caps: impl IntoIterator<Item = impl Into<C>>) {
290        self.0.extend(caps.into_iter().map(Into::into));
291    }
292
293    pub fn insert(&mut self, cap: impl Into<C>) -> bool {
294        self.0.insert(cap.into())
295    }
296
297    pub fn from_strs<'a, E>(strs: impl IntoIterator<Item = &'a str>) -> Result<Self>
298    where
299        C: FromStr<Err = E>,
300        Result<C, E>: anyhow::Context<C, E>,
301    {
302        let mut caps = Self::default();
303        for s in strs {
304            let cap = C::from_str(s).with_context(|| format!("Unknown capability: {s}"))?;
305            caps.insert(cap);
306        }
307        Ok(caps)
308    }
309
310    pub fn to_strings(&self) -> Vec<String>
311    where
312        C: fmt::Display,
313    {
314        self.iter().map(ToString::to_string).collect()
315    }
316}
317
318impl<C: Capability + Ord> Capability for CapSet<C> {
319    fn permits(&self, other: &Self) -> bool {
320        other
321            .iter()
322            .all(|other_cap| self.iter().any(|self_cap| self_cap.permits(other_cap)))
323    }
324}
325
326/// Create an rcan token for the api access from a PEM-encoded OpenSSH ed25519
327/// private key.
328#[cfg(not(target_arch = "wasm32"))]
329pub fn create_api_token_from_openssh_pem(
330    pem: &str,
331    local_id: EndpointId,
332    max_age: Duration,
333    capability: Caps,
334) -> Result<Rcan<Caps>> {
335    let seed = crate::openssh::parse_ed25519_private_key(pem)?;
336    let issuer = ed25519_dalek::SigningKey::from_bytes(&seed);
337    let audience = local_id.as_verifying_key();
338    let can =
339        Rcan::issuing_builder(&issuer, audience, capability).sign(Expires::valid_for(max_age));
340    Ok(can)
341}
342
343/// Create an rcan token that grants capabilities to a remote endpoint.
344/// The local endpoint is the issuer (granter), and the remote endpoint is the
345/// audience (grantee).
346pub fn create_grant_token(
347    local_secret: SecretKey,
348    remote_id: EndpointId,
349    max_age: Duration,
350    capability: Caps,
351) -> Result<Rcan<Caps>> {
352    let issuer = ed25519_dalek::SigningKey::from_bytes(&local_secret.to_bytes());
353    let audience = remote_id.as_verifying_key();
354    let can =
355        Rcan::issuing_builder(&issuer, audience, capability).sign(Expires::valid_for(max_age));
356    Ok(can)
357}
358
359/// Create an rcan token for the api access from an iroh secret key
360pub fn create_api_token_from_secret_key(
361    private_key: SecretKey,
362    local_id: EndpointId,
363    max_age: Duration,
364    capability: Caps,
365) -> Result<Rcan<Caps>> {
366    let issuer = ed25519_dalek::SigningKey::from_bytes(&private_key.to_bytes());
367    let audience = local_id.as_verifying_key();
368    let can =
369        Rcan::issuing_builder(&issuer, audience, capability).sign(Expires::valid_for(max_age));
370    Ok(can)
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    #[test]
378    fn smoke() {
379        let all = Caps::default()
380            .extend([RelayCap::Use])
381            .extend([MetricsCap::PutAny]);
382
383        // test to-and-from string conversion
384        println!("all:     {all:?}");
385        let strings = all.to_strings();
386        println!("strings: {strings:?}");
387        let parsed = Caps::from_strs(strings.iter().map(|s| s.as_str())).unwrap();
388        assert_eq!(all, parsed);
389
390        // manual parsing from strings
391        let s = ["metrics:put-any", "relay:use"];
392        let caps = Caps::from_strs(s).unwrap();
393        assert_eq!(
394            caps,
395            Caps::new([MetricsCap::PutAny]).extend([RelayCap::Use])
396        );
397
398        let full = Caps::new([Cap::All]);
399
400        assert!(full.permits(&full));
401        assert!(full.permits(&all));
402        assert!(!all.permits(&full));
403
404        let metrics = Caps::new([MetricsCap::PutAny]);
405        let relay = Caps::new([RelayCap::Use]);
406
407        for cap in [&metrics, &relay] {
408            assert!(full.permits(cap));
409            assert!(all.permits(cap));
410            assert!(!cap.permits(&full));
411            assert!(!cap.permits(&all));
412        }
413
414        assert!(!metrics.permits(&relay));
415        assert!(!relay.permits(&metrics));
416    }
417
418    #[test]
419    fn client_caps() {
420        let client = Caps::new([Cap::Client]);
421
422        let all = Caps::new([Cap::All]);
423        let metrics = Caps::new([MetricsCap::PutAny]);
424        let relay = Caps::new([RelayCap::Use]);
425        assert!(client.permits(&metrics));
426        assert!(client.permits(&relay));
427        assert!(!client.permits(&all));
428    }
429}