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