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