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