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