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
239pub 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 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 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}