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}
85
86impl FromStr for Cap {
87    type Err = anyhow::Error;
88
89    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
90        if s == "all" {
91            Ok(Self::All)
92        } else if let Some((domain, inner)) = s.split_once(":") {
93            Ok(match domain {
94                "metrics" => Self::Metrics(MetricsCap::from_str(inner)?),
95                "relay" => Self::Relay(RelayCap::from_str(inner)?),
96                "net-diagnostics" => Self::NetDiagnostics(NetDiagnosticsCap::from_str(inner)?),
97                _ => bail!("invalid cap domain"),
98            })
99        } else {
100            Err(anyhow::anyhow!("invalid cap string"))
101        }
102    }
103}
104
105cap_enum!(
106    pub enum MetricsCap {
107        PutAny,
108    }
109);
110
111cap_enum!(
112    pub enum RelayCap {
113        Use,
114    }
115);
116
117cap_enum!(
118    pub enum NetDiagnosticsCap {
119        PutAny,
120        GetAny,
121    }
122);
123
124impl Caps {
125    pub fn new(caps: impl IntoIterator<Item = impl Into<Cap>>) -> Self {
126        Self::V0(CapSet::new(caps))
127    }
128
129    /// the class of capabilities that iroh-services will accept when deriving from a
130    /// shared secret like an [ApiSecret]. These should be "client" capabilities:
131    /// typically for users of an app
132    ///
133    /// [ApiSecret]: crate::api_secret::ApiSecret
134    pub fn for_shared_secret() -> Self {
135        Self::new([Cap::Client])
136    }
137
138    /// The maximum set of capabilities. iroh-services will only accept these capabilities
139    /// when deriving from a secret that is registered with iroh-services, like an SSH key
140    pub fn all() -> Self {
141        Self::new([Cap::All])
142    }
143
144    pub fn extend(self, caps: impl IntoIterator<Item = impl Into<Cap>>) -> Self {
145        let Self::V0(mut set) = self;
146        set.extend(caps.into_iter().map(Into::into));
147        Self::V0(set)
148    }
149
150    pub fn from_strs<'a>(strs: impl IntoIterator<Item = &'a str>) -> Result<Self> {
151        Ok(Self::V0(CapSet::from_strs(strs)?))
152    }
153
154    pub fn to_strings(&self) -> Vec<String> {
155        let Self::V0(set) = self;
156        set.to_strings()
157    }
158}
159
160impl Capability for Caps {
161    fn permits(&self, other: &Self) -> bool {
162        let Self::V0(slf) = self;
163        let Self::V0(other) = other;
164        slf.permits(other)
165    }
166}
167
168impl From<Cap> for Caps {
169    fn from(cap: Cap) -> Self {
170        Self::new([cap])
171    }
172}
173
174impl Capability for Cap {
175    fn permits(&self, other: &Self) -> bool {
176        match (self, other) {
177            (Cap::All, _) => true,
178            (Cap::Client, other) => client_capabilities(other),
179            (Cap::Relay(slf), Cap::Relay(other)) => slf.permits(other),
180            (Cap::Metrics(slf), Cap::Metrics(other)) => slf.permits(other),
181            (Cap::NetDiagnostics(slf), Cap::NetDiagnostics(other)) => slf.permits(other),
182            (_, _) => false,
183        }
184    }
185}
186
187fn client_capabilities(other: &Cap) -> bool {
188    match other {
189        Cap::All => false,
190        Cap::Client => true,
191        Cap::Relay(RelayCap::Use) => true,
192        Cap::Metrics(MetricsCap::PutAny) => true,
193        Cap::NetDiagnostics(NetDiagnosticsCap::PutAny) => true,
194        Cap::NetDiagnostics(NetDiagnosticsCap::GetAny) => true,
195    }
196}
197
198impl Capability for MetricsCap {
199    fn permits(&self, other: &Self) -> bool {
200        match (self, other) {
201            (MetricsCap::PutAny, MetricsCap::PutAny) => true,
202        }
203    }
204}
205
206impl Capability for RelayCap {
207    fn permits(&self, other: &Self) -> bool {
208        match (self, other) {
209            (RelayCap::Use, RelayCap::Use) => true,
210        }
211    }
212}
213
214impl Capability for NetDiagnosticsCap {
215    fn permits(&self, other: &Self) -> bool {
216        match (self, other) {
217            (NetDiagnosticsCap::PutAny, NetDiagnosticsCap::PutAny) => true,
218            (NetDiagnosticsCap::GetAny, NetDiagnosticsCap::GetAny) => true,
219            (_, _) => false,
220        }
221    }
222}
223
224/// A set of capabilities
225#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Serialize, Deserialize)]
226pub struct CapSet<C: Capability + Ord>(BTreeSet<C>);
227
228impl<C: Capability + Ord> Default for CapSet<C> {
229    fn default() -> Self {
230        Self(BTreeSet::new())
231    }
232}
233
234impl<C: Capability + Ord> CapSet<C> {
235    pub fn new(set: impl IntoIterator<Item = impl Into<C>>) -> Self {
236        Self(BTreeSet::from_iter(set.into_iter().map(Into::into)))
237    }
238
239    pub fn iter(&self) -> impl Iterator<Item = &'_ C> + '_ {
240        self.0.iter()
241    }
242
243    pub fn is_empty(&self) -> bool {
244        self.0.is_empty()
245    }
246
247    pub fn len(&self) -> usize {
248        self.0.len()
249    }
250
251    pub fn contains(&self, cap: impl Into<C>) -> bool {
252        let cap = cap.into();
253        self.0.contains(&cap)
254    }
255
256    pub fn extend(&mut self, caps: impl IntoIterator<Item = impl Into<C>>) {
257        self.0.extend(caps.into_iter().map(Into::into));
258    }
259
260    pub fn insert(&mut self, cap: impl Into<C>) -> bool {
261        self.0.insert(cap.into())
262    }
263
264    pub fn from_strs<'a, E>(strs: impl IntoIterator<Item = &'a str>) -> Result<Self>
265    where
266        C: FromStr<Err = E>,
267        Result<C, E>: anyhow::Context<C, E>,
268    {
269        let mut caps = Self::default();
270        for s in strs {
271            let cap = C::from_str(s).with_context(|| format!("Unknown capability: {s}"))?;
272            caps.insert(cap);
273        }
274        Ok(caps)
275    }
276
277    pub fn to_strings(&self) -> Vec<String>
278    where
279        C: fmt::Display,
280    {
281        self.iter().map(ToString::to_string).collect()
282    }
283}
284
285impl<C: Capability + Ord> Capability for CapSet<C> {
286    fn permits(&self, other: &Self) -> bool {
287        other
288            .iter()
289            .all(|other_cap| self.iter().any(|self_cap| self_cap.permits(other_cap)))
290    }
291}
292
293/// Create an rcan token for the api access.
294#[cfg(feature = "ssh-key")]
295pub fn create_api_token_from_ssh_key(
296    user_ssh_key: &ssh_key::PrivateKey,
297    local_id: EndpointId,
298    max_age: Duration,
299    capability: Caps,
300) -> Result<Rcan<Caps>> {
301    let issuer: ed25519_dalek::SigningKey = user_ssh_key
302        .key_data()
303        .ed25519()
304        .context("only Ed25519 keys supported")?
305        .private
306        .clone()
307        .into();
308
309    let audience = local_id.as_verifying_key();
310    let can =
311        Rcan::issuing_builder(&issuer, audience, capability).sign(Expires::valid_for(max_age));
312    Ok(can)
313}
314
315/// Create an rcan token that grants capabilities to a remote endpoint.
316/// The local endpoint is the issuer (granter), and the remote endpoint is the
317/// audience (grantee).
318pub fn create_grant_token(
319    local_secret: SecretKey,
320    remote_id: EndpointId,
321    max_age: Duration,
322    capability: Caps,
323) -> Result<Rcan<Caps>> {
324    let issuer = ed25519_dalek::SigningKey::from_bytes(&local_secret.to_bytes());
325    let audience = remote_id.as_verifying_key();
326    let can =
327        Rcan::issuing_builder(&issuer, audience, capability).sign(Expires::valid_for(max_age));
328    Ok(can)
329}
330
331/// Create an rcan token for the api access from an iroh secret key
332pub fn create_api_token_from_secret_key(
333    private_key: SecretKey,
334    local_id: EndpointId,
335    max_age: Duration,
336    capability: Caps,
337) -> Result<Rcan<Caps>> {
338    let issuer = ed25519_dalek::SigningKey::from_bytes(&private_key.to_bytes());
339    let audience = local_id.as_verifying_key();
340    let can =
341        Rcan::issuing_builder(&issuer, audience, capability).sign(Expires::valid_for(max_age));
342    Ok(can)
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn smoke() {
351        let all = Caps::default()
352            .extend([RelayCap::Use])
353            .extend([MetricsCap::PutAny]);
354
355        // test to-and-from string conversion
356        println!("all:     {all:?}");
357        let strings = all.to_strings();
358        println!("strings: {strings:?}");
359        let parsed = Caps::from_strs(strings.iter().map(|s| s.as_str())).unwrap();
360        assert_eq!(all, parsed);
361
362        // manual parsing from strings
363        let s = ["metrics:put-any", "relay:use"];
364        let caps = Caps::from_strs(s).unwrap();
365        assert_eq!(
366            caps,
367            Caps::new([MetricsCap::PutAny]).extend([RelayCap::Use])
368        );
369
370        let full = Caps::new([Cap::All]);
371
372        assert!(full.permits(&full));
373        assert!(full.permits(&all));
374        assert!(!all.permits(&full));
375
376        let metrics = Caps::new([MetricsCap::PutAny]);
377        let relay = Caps::new([RelayCap::Use]);
378
379        for cap in [&metrics, &relay] {
380            assert!(full.permits(cap));
381            assert!(all.permits(cap));
382            assert!(!cap.permits(&full));
383            assert!(!cap.permits(&all));
384        }
385
386        assert!(!metrics.permits(&relay));
387        assert!(!relay.permits(&metrics));
388    }
389
390    #[test]
391    fn client_caps() {
392        let client = Caps::new([Cap::Client]);
393
394        let all = Caps::new([Cap::All]);
395        let metrics = Caps::new([MetricsCap::PutAny]);
396        let relay = Caps::new([RelayCap::Use]);
397        assert!(client.permits(&metrics));
398        assert!(client.permits(&relay));
399        assert!(!client.permits(&all));
400    }
401}