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::NodeId;
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 = "relay:{0}")]
70    Relay(RelayCap),
71    #[strum(to_string = "metrics:{0}")]
72    Metrics(MetricsCap),
73}
74
75impl FromStr for Cap {
76    type Err = anyhow::Error;
77
78    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
79        if s == "all" {
80            Ok(Self::All)
81        } else if let Some((domain, inner)) = s.split_once(":") {
82            Ok(match domain {
83                "metrics" => Self::Metrics(MetricsCap::from_str(inner)?),
84                "relay" => Self::Relay(RelayCap::from_str(inner)?),
85                _ => bail!("invalid cap domain"),
86            })
87        } else {
88            Err(anyhow::anyhow!("invalid cap string"))
89        }
90    }
91}
92
93cap_enum!(
94    pub enum MetricsCap {
95        PutAny,
96    }
97);
98
99cap_enum!(
100    pub enum RelayCap {
101        Use,
102    }
103);
104
105impl Caps {
106    pub fn new(caps: impl IntoIterator<Item = impl Into<Cap>>) -> Self {
107        Self::V0(CapSet::new(caps))
108    }
109
110    pub fn all() -> Self {
111        Self::new([Cap::All])
112    }
113
114    pub fn extend(self, caps: impl IntoIterator<Item = impl Into<Cap>>) -> Self {
115        let Self::V0(mut set) = self;
116        set.extend(caps.into_iter().map(Into::into));
117        Self::V0(set)
118    }
119
120    pub fn from_strs<'a>(strs: impl IntoIterator<Item = &'a str>) -> Result<Self> {
121        Ok(Self::V0(CapSet::from_strs(strs)?))
122    }
123
124    pub fn to_strings(&self) -> Vec<String> {
125        let Self::V0(set) = self;
126        set.to_strings()
127    }
128}
129
130impl Capability for Caps {
131    fn permits(&self, other: &Self) -> bool {
132        let Self::V0(slf) = self;
133        let Self::V0(other) = other;
134        slf.permits(other)
135    }
136}
137
138impl From<Cap> for Caps {
139    fn from(cap: Cap) -> Self {
140        Self::new([cap])
141    }
142}
143
144impl Capability for Cap {
145    fn permits(&self, other: &Self) -> bool {
146        match (self, other) {
147            (Cap::All, _) => true,
148            (Cap::Relay(slf), Cap::Relay(other)) => slf.permits(other),
149            (Cap::Metrics(slf), Cap::Metrics(other)) => slf.permits(other),
150            (_, _) => false,
151        }
152    }
153}
154
155impl Capability for MetricsCap {
156    fn permits(&self, other: &Self) -> bool {
157        match (self, other) {
158            (MetricsCap::PutAny, MetricsCap::PutAny) => true,
159        }
160    }
161}
162
163impl Capability for RelayCap {
164    fn permits(&self, other: &Self) -> bool {
165        match (self, other) {
166            (RelayCap::Use, RelayCap::Use) => true,
167        }
168    }
169}
170
171#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Serialize, Deserialize)]
172pub struct CapSet<C: Capability + Ord>(BTreeSet<C>);
173
174impl<C: Capability + Ord> Default for CapSet<C> {
175    fn default() -> Self {
176        Self(BTreeSet::new())
177    }
178}
179
180impl<C: Capability + Ord> CapSet<C> {
181    pub fn new(set: impl IntoIterator<Item = impl Into<C>>) -> Self {
182        Self(BTreeSet::from_iter(set.into_iter().map(Into::into)))
183    }
184
185    pub fn iter(&self) -> impl Iterator<Item = &'_ C> + '_ {
186        self.0.iter()
187    }
188
189    pub fn is_empty(&self) -> bool {
190        self.0.is_empty()
191    }
192
193    pub fn len(&self) -> usize {
194        self.0.len()
195    }
196
197    pub fn contains(&self, cap: impl Into<C>) -> bool {
198        let cap = cap.into();
199        self.0.contains(&cap)
200    }
201
202    pub fn extend(&mut self, caps: impl IntoIterator<Item = impl Into<C>>) {
203        self.0.extend(caps.into_iter().map(Into::into));
204    }
205
206    pub fn insert(&mut self, cap: impl Into<C>) -> bool {
207        self.0.insert(cap.into())
208    }
209
210    pub fn from_strs<'a, E>(strs: impl IntoIterator<Item = &'a str>) -> Result<Self>
211    where
212        C: FromStr<Err = E>,
213        Result<C, E>: anyhow::Context<C, E>,
214    {
215        let mut caps = Self::default();
216        for s in strs {
217            let cap = C::from_str(s).with_context(|| format!("Unknown capability: {s}"))?;
218            caps.insert(cap);
219        }
220        Ok(caps)
221    }
222
223    pub fn to_strings(&self) -> Vec<String>
224    where
225        C: fmt::Display,
226    {
227        self.iter().map(ToString::to_string).collect()
228    }
229}
230
231impl<C: Capability + Ord> Capability for CapSet<C> {
232    fn permits(&self, other: &Self) -> bool {
233        other
234            .iter()
235            .all(|other_cap| self.iter().any(|self_cap| self_cap.permits(other_cap)))
236    }
237}
238
239/// Create an rcan token for the api access.
240pub fn create_api_token(
241    user_ssh_key: &SshPrivateKey,
242    local_node_id: NodeId,
243    max_age: Duration,
244    capability: Caps,
245) -> Result<Rcan<Caps>> {
246    let issuer: SigningKey = user_ssh_key
247        .key_data()
248        .ed25519()
249        .context("only Ed25519 keys supported")?
250        .private
251        .clone()
252        .into();
253
254    let audience = local_node_id.public();
255    let can =
256        Rcan::issuing_builder(&issuer, audience, capability).sign(Expires::valid_for(max_age));
257    Ok(can)
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    #[test]
265    fn smoke() {
266        let all = Caps::default()
267            .extend([RelayCap::Use])
268            .extend([MetricsCap::PutAny]);
269
270        // test to-and-from string conversion
271        println!("all:     {all:?}");
272        let strings = all.to_strings();
273        println!("strings: {strings:?}");
274        let parsed = Caps::from_strs(strings.iter().map(|s| s.as_str())).unwrap();
275        assert_eq!(all, parsed);
276
277        // manual parsing from strings
278        let s = ["metrics:put-any", "relay:use"];
279        let caps = Caps::from_strs(s).unwrap();
280        assert_eq!(
281            caps,
282            Caps::new([MetricsCap::PutAny]).extend([RelayCap::Use])
283        );
284
285        let full = Caps::new([Cap::All]);
286
287        assert!(full.permits(&full));
288        assert!(full.permits(&all));
289        assert!(!all.permits(&full));
290
291        let metrics = Caps::new([MetricsCap::PutAny]);
292        let relay = Caps::new([RelayCap::Use]);
293
294        for cap in [&metrics, &relay] {
295            assert!(full.permits(cap));
296            assert!(all.permits(cap));
297            assert!(!cap.permits(&full));
298            assert!(!cap.permits(&all));
299        }
300
301        assert!(!metrics.permits(&relay));
302        assert!(!relay.permits(&metrics));
303    }
304}