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