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