iroh_n0des/
caps.rs

1use std::{collections::BTreeSet, fmt, str::FromStr, time::Duration};
2
3use anyhow::{Context, Result, bail};
4use ed25519_dalek::SigningKey;
5use iroh::{EndpointId, SecretKey};
6use rcan::{Capability, Expires, Rcan};
7use serde::{Deserialize, Serialize};
8use ssh_key::PrivateKey as SshPrivateKey;
9
10macro_rules! cap_enum(
11    ($enum:item) => {
12        #[derive(
13            Debug,
14            Eq,
15            PartialEq,
16            Ord,
17            PartialOrd,
18            Serialize,
19            Deserialize,
20            Clone,
21            Copy,
22            strum::Display,
23            strum::EnumString,
24        )]
25        #[strum(serialize_all = "kebab-case")]
26        #[serde(rename_all = "kebab-case")]
27        $enum
28    }
29);
30
31#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)]
32#[serde(rename_all = "kebab-case")]
33pub enum Caps {
34    V0(CapSet<Cap>),
35}
36
37impl Default for Caps {
38    fn default() -> Self {
39        Self::V0(CapSet::default())
40    }
41}
42
43impl std::ops::Deref for Caps {
44    type Target = CapSet<Cap>;
45
46    fn deref(&self) -> &Self::Target {
47        let Self::V0(slf) = self;
48        slf
49    }
50}
51
52#[derive(
53    Debug,
54    Eq,
55    PartialEq,
56    Ord,
57    PartialOrd,
58    Serialize,
59    Deserialize,
60    Clone,
61    Copy,
62    derive_more::From,
63    strum::Display,
64)]
65#[serde(rename_all = "kebab-case")]
66pub enum Cap {
67    #[strum(to_string = "all")]
68    All,
69    #[strum(to_string = "client")]
70    Client,
71    #[strum(to_string = "relay:{0}")]
72    Relay(RelayCap),
73    #[strum(to_string = "metrics:{0}")]
74    Metrics(MetricsCap),
75}
76
77impl FromStr for Cap {
78    type Err = anyhow::Error;
79
80    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
81        if s == "all" {
82            Ok(Self::All)
83        } else if let Some((domain, inner)) = s.split_once(":") {
84            Ok(match domain {
85                "metrics" => Self::Metrics(MetricsCap::from_str(inner)?),
86                "relay" => Self::Relay(RelayCap::from_str(inner)?),
87                _ => bail!("invalid cap domain"),
88            })
89        } else {
90            Err(anyhow::anyhow!("invalid cap string"))
91        }
92    }
93}
94
95cap_enum!(
96    pub enum MetricsCap {
97        PutAny,
98    }
99);
100
101cap_enum!(
102    pub enum RelayCap {
103        Use,
104    }
105);
106
107impl Caps {
108    pub fn new(caps: impl IntoIterator<Item = impl Into<Cap>>) -> Self {
109        Self::V0(CapSet::new(caps))
110    }
111
112    /// the class of capabilities that n0des will accept when deriving from a
113    /// shared secret like a [`N0desTicket`]. These should be "client" capabilities:
114    /// typically for users of an app
115    pub fn for_shared_secret() -> Self {
116        Self::new([Cap::Client])
117    }
118
119    /// The maximum set of capabilities. n0des will only accept these capabilities
120    /// when deriving from a secret that is registered with n0des, like an SSH key
121    pub fn all() -> Self {
122        Self::new([Cap::All])
123    }
124
125    pub fn extend(self, caps: impl IntoIterator<Item = impl Into<Cap>>) -> Self {
126        let Self::V0(mut set) = self;
127        set.extend(caps.into_iter().map(Into::into));
128        Self::V0(set)
129    }
130
131    pub fn from_strs<'a>(strs: impl IntoIterator<Item = &'a str>) -> Result<Self> {
132        Ok(Self::V0(CapSet::from_strs(strs)?))
133    }
134
135    pub fn to_strings(&self) -> Vec<String> {
136        let Self::V0(set) = self;
137        set.to_strings()
138    }
139}
140
141impl Capability for Caps {
142    fn permits(&self, other: &Self) -> bool {
143        let Self::V0(slf) = self;
144        let Self::V0(other) = other;
145        slf.permits(other)
146    }
147}
148
149impl From<Cap> for Caps {
150    fn from(cap: Cap) -> Self {
151        Self::new([cap])
152    }
153}
154
155impl Capability for Cap {
156    fn permits(&self, other: &Self) -> bool {
157        match (self, other) {
158            (Cap::All, _) => true,
159            (Cap::Client, other) => client_capabilities(other),
160            (Cap::Relay(slf), Cap::Relay(other)) => slf.permits(other),
161            (Cap::Metrics(slf), Cap::Metrics(other)) => slf.permits(other),
162            (_, _) => false,
163        }
164    }
165}
166
167fn client_capabilities(other: &Cap) -> bool {
168    match other {
169        Cap::All => false,
170        Cap::Client => true,
171        Cap::Relay(RelayCap::Use) => true,
172        Cap::Metrics(MetricsCap::PutAny) => true,
173    }
174}
175
176impl Capability for MetricsCap {
177    fn permits(&self, other: &Self) -> bool {
178        match (self, other) {
179            (MetricsCap::PutAny, MetricsCap::PutAny) => true,
180        }
181    }
182}
183
184impl Capability for RelayCap {
185    fn permits(&self, other: &Self) -> bool {
186        match (self, other) {
187            (RelayCap::Use, RelayCap::Use) => true,
188        }
189    }
190}
191
192#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Serialize, Deserialize)]
193pub struct CapSet<C: Capability + Ord>(BTreeSet<C>);
194
195impl<C: Capability + Ord> Default for CapSet<C> {
196    fn default() -> Self {
197        Self(BTreeSet::new())
198    }
199}
200
201impl<C: Capability + Ord> CapSet<C> {
202    pub fn new(set: impl IntoIterator<Item = impl Into<C>>) -> Self {
203        Self(BTreeSet::from_iter(set.into_iter().map(Into::into)))
204    }
205
206    pub fn iter(&self) -> impl Iterator<Item = &'_ C> + '_ {
207        self.0.iter()
208    }
209
210    pub fn is_empty(&self) -> bool {
211        self.0.is_empty()
212    }
213
214    pub fn len(&self) -> usize {
215        self.0.len()
216    }
217
218    pub fn contains(&self, cap: impl Into<C>) -> bool {
219        let cap = cap.into();
220        self.0.contains(&cap)
221    }
222
223    pub fn extend(&mut self, caps: impl IntoIterator<Item = impl Into<C>>) {
224        self.0.extend(caps.into_iter().map(Into::into));
225    }
226
227    pub fn insert(&mut self, cap: impl Into<C>) -> bool {
228        self.0.insert(cap.into())
229    }
230
231    pub fn from_strs<'a, E>(strs: impl IntoIterator<Item = &'a str>) -> Result<Self>
232    where
233        C: FromStr<Err = E>,
234        Result<C, E>: anyhow::Context<C, E>,
235    {
236        let mut caps = Self::default();
237        for s in strs {
238            let cap = C::from_str(s).with_context(|| format!("Unknown capability: {s}"))?;
239            caps.insert(cap);
240        }
241        Ok(caps)
242    }
243
244    pub fn to_strings(&self) -> Vec<String>
245    where
246        C: fmt::Display,
247    {
248        self.iter().map(ToString::to_string).collect()
249    }
250}
251
252impl<C: Capability + Ord> Capability for CapSet<C> {
253    fn permits(&self, other: &Self) -> bool {
254        other
255            .iter()
256            .all(|other_cap| self.iter().any(|self_cap| self_cap.permits(other_cap)))
257    }
258}
259
260/// Create an rcan token for the api access.
261pub fn create_api_token(
262    user_ssh_key: &SshPrivateKey,
263    local_id: EndpointId,
264    max_age: Duration,
265    capability: Caps,
266) -> Result<Rcan<Caps>> {
267    let issuer: SigningKey = user_ssh_key
268        .key_data()
269        .ed25519()
270        .context("only Ed25519 keys supported")?
271        .private
272        .clone()
273        .into();
274
275    let audience = local_id.as_verifying_key();
276    let can =
277        Rcan::issuing_builder(&issuer, audience, capability).sign(Expires::valid_for(max_age));
278    Ok(can)
279}
280
281/// Create an rcan token for the api access from an iroh secret key
282pub fn create_api_token_from_secret_key(
283    private_key: SecretKey,
284    local_id: EndpointId,
285    max_age: Duration,
286    capability: Caps,
287) -> Result<Rcan<Caps>> {
288    let issuer = SigningKey::from_bytes(&private_key.to_bytes());
289    let audience = local_id.as_verifying_key();
290    let can =
291        Rcan::issuing_builder(&issuer, audience, capability).sign(Expires::valid_for(max_age));
292    Ok(can)
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn smoke() {
301        let all = Caps::default()
302            .extend([RelayCap::Use])
303            .extend([MetricsCap::PutAny]);
304
305        // test to-and-from string conversion
306        println!("all:     {all:?}");
307        let strings = all.to_strings();
308        println!("strings: {strings:?}");
309        let parsed = Caps::from_strs(strings.iter().map(|s| s.as_str())).unwrap();
310        assert_eq!(all, parsed);
311
312        // manual parsing from strings
313        let s = ["metrics:put-any", "relay:use"];
314        let caps = Caps::from_strs(s).unwrap();
315        assert_eq!(
316            caps,
317            Caps::new([MetricsCap::PutAny]).extend([RelayCap::Use])
318        );
319
320        let full = Caps::new([Cap::All]);
321
322        assert!(full.permits(&full));
323        assert!(full.permits(&all));
324        assert!(!all.permits(&full));
325
326        let metrics = Caps::new([MetricsCap::PutAny]);
327        let relay = Caps::new([RelayCap::Use]);
328
329        for cap in [&metrics, &relay] {
330            assert!(full.permits(cap));
331            assert!(all.permits(cap));
332            assert!(!cap.permits(&full));
333            assert!(!cap.permits(&all));
334        }
335
336        assert!(!metrics.permits(&relay));
337        assert!(!relay.permits(&metrics));
338    }
339
340    #[test]
341    fn client_caps() {
342        let client = Caps::new([Cap::Client]);
343
344        let all = Caps::new([Cap::All]);
345        let metrics = Caps::new([MetricsCap::PutAny]);
346        let relay = Caps::new([RelayCap::Use]);
347        assert!(client.permits(&metrics));
348        assert!(client.permits(&relay));
349        assert!(!client.permits(&all));
350    }
351}