1use runeauth::{Alternative, Check, Condition, ConditionChecker, Restriction, Rune, RuneError};
2use std::fmt::Display;
3use std::time::{SystemTime, UNIX_EPOCH};
4
5pub trait Restrictor {
11 fn generate(self) -> Result<Vec<Restriction>, RuneError>;
17}
18
19pub struct RuneFactory;
24
25impl RuneFactory {
26 pub fn carve<T: Restrictor + Copy>(origin: &Rune, append: &[T]) -> Result<String, RuneError> {
39 let restrictions = append.into_iter().try_fold(Vec::new(), |mut acc, res| {
40 let mut r = res.generate()?;
41 acc.append(&mut r);
42 Ok(acc)
43 })?;
44
45 let mut originc = origin.clone();
46 restrictions.into_iter().for_each(|r| {
47 let _ = originc.add_restriction(r);
50 });
51
52 Ok(originc.to_base64())
53 }
54}
55
56#[derive(Clone, Copy)]
58pub enum DefRules<'a> {
59 ReadOnly,
62 Pay,
65 Add(&'a [DefRules<'a>]),
69}
70
71impl<'a> Restrictor for DefRules<'a> {
72 fn generate(self) -> Result<Vec<Restriction>, RuneError> {
79 match self {
80 DefRules::ReadOnly => {
81 let a: Vec<Restriction> = vec![Restriction::new(vec![
82 alternative("method", Condition::BeginsWith, "Get").unwrap(),
83 alternative("method", Condition::BeginsWith, "List").unwrap(),
84 ])
85 .unwrap()];
86 Ok(a)
87 }
88 DefRules::Pay => {
89 let a =
90 vec![Restriction::new(vec![
91 alternative("method", Condition::Equal, "pay").unwrap()
92 ])
93 .unwrap()];
94 Ok(a)
95 }
96 DefRules::Add(rules) => {
97 let alt_set =
98 rules
99 .into_iter()
100 .try_fold(Vec::new(), |mut acc: Vec<Alternative>, rule| {
101 let mut alts = rule
102 .generate()?
103 .into_iter()
104 .flat_map(|r| r.alternatives)
105 .collect();
106 acc.append(&mut alts);
107 Ok(acc)
108 })?;
109 let a = vec![Restriction::new(alt_set)?];
110 Ok(a)
111 }
112 }
113 }
114}
115
116impl<'a> Display for DefRules<'a> {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 match self {
119 DefRules::ReadOnly => write!(f, "readonly"),
120 DefRules::Pay => write!(f, "pay"),
121 DefRules::Add(rules) => {
122 write!(
123 f,
124 "{}",
125 rules.into_iter().fold(String::new(), |acc, r| {
126 if acc.is_empty() {
127 format!("{}", r)
128 } else {
129 format!("{}|{}", acc, r)
130 }
131 })
132 )
133 }
134 }
135 }
136}
137
138fn alternative(field: &str, cond: Condition, value: &str) -> Result<Alternative, RuneError> {
153 Alternative::new(field.to_string(), cond, value.to_string(), false)
154}
155
156#[derive(Clone)]
159pub struct Context {
160 pub method: String,
162 pub pubkey: String,
164 pub unique_id: String,
166 pub time: SystemTime,
168 }
170
171impl Check for Context {
174 fn check_alternative(&self, alt: &Alternative) -> anyhow::Result<(), RuneError> {
184 let value = match alt.get_field().as_str() {
185 "" => self.unique_id.clone(),
186 "method" => self.method.clone(),
187 "pubkey" => self.pubkey.clone(),
188 "time" => self
189 .time
190 .duration_since(UNIX_EPOCH)
191 .map_err(|e| {
192 RuneError::Unknown(format!("Can not extract seconds from timestamp {:?}", e))
193 })?
194 .as_secs()
195 .to_string(),
196 _ => String::new(), };
198 ConditionChecker { value }.check_alternative(alt)
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::{Context, DefRules, RuneFactory};
205 use base64::{engine::general_purpose, Engine as _};
206 use runeauth::{Alternative, Condition, Restriction, Rune};
207 use std::time::SystemTime;
208
209 #[test]
210 fn test_carve_readonly_rune() {
211 let seed = [0; 32];
212 let mr = Rune::new_master_rune(&seed, vec![], None, None).unwrap();
213
214 let carved = RuneFactory::carve(&mr, &[DefRules::ReadOnly]).unwrap();
216
217 let carved_byt = general_purpose::URL_SAFE.decode(&carved).unwrap();
218 let carved_restr = String::from_utf8(carved_byt[32..].to_vec()).unwrap(); assert_eq!(carved_restr, *"method^Get|method^List");
220
221 let carved_rune = Rune::from_base64(&carved).unwrap();
222 assert!(mr.is_authorized(&carved_rune));
223 }
224
225 #[test]
226 fn test_carve_disjunction_rune() {
227 let seed = [0; 32];
228 let mr = Rune::new_master_rune(&seed, vec![], None, None).unwrap();
229
230 let carved =
232 RuneFactory::carve(&mr, &[DefRules::Add(&[DefRules::ReadOnly, DefRules::Pay])])
233 .unwrap();
234
235 let carved_byt = general_purpose::URL_SAFE.decode(&carved).unwrap();
236 let carved_restr = String::from_utf8(carved_byt[32..].to_vec()).unwrap(); assert_eq!(carved_restr, *"method^Get|method^List|method=pay");
238
239 let carved_rune = Rune::from_base64(&carved).unwrap();
240 assert!(mr.is_authorized(&carved_rune));
241 }
242
243 #[test]
244 fn test_defrules_display() {
245 let r = DefRules::Pay;
246 assert_eq!(format!("{}", r), "pay");
247 let r = DefRules::Add(&[DefRules::Pay]);
248 assert_eq!(format!("{}", r), "pay");
249 let r = DefRules::Add(&[DefRules::Pay, DefRules::ReadOnly]);
250 assert_eq!(format!("{}", r), "pay|readonly");
251 }
252
253 #[test]
254 fn test_context_check() {
255 let seedsecret = &[0; 32];
256 let mr = Rune::new_master_rune(seedsecret, vec![], None, None).unwrap();
257
258 let r1 = Rune::new(
260 mr.authcode(),
261 vec![Restriction::new(vec![Alternative::new(
262 String::from("pubkey"),
263 Condition::Equal,
264 String::from("020000000000000000"),
265 false,
266 )
267 .unwrap()])
268 .unwrap()],
269 None,
270 None,
271 )
272 .unwrap();
273
274 let r2 = Rune::new(
276 mr.authcode(),
277 vec![Restriction::new(vec![Alternative::new(
278 String::from("method"),
279 Condition::Equal,
280 String::from("GetInfo"),
281 false,
282 )
283 .unwrap()])
284 .unwrap()],
285 None,
286 None,
287 )
288 .unwrap();
289
290 let r3 = Rune::new(
292 mr.authcode(),
293 vec![Restriction::new(vec![Alternative::new(
294 String::from("pubkey"),
295 Condition::Missing,
296 String::new(),
297 false,
298 )
299 .unwrap()])
300 .unwrap()],
301 None,
302 None,
303 )
304 .unwrap();
305
306 let r4 = Rune::new(
308 mr.authcode(),
309 vec![Restriction::new(vec![Alternative::new(
310 String::from("method"),
311 Condition::Missing,
312 String::new(),
313 false,
314 )
315 .unwrap()])
316 .unwrap()],
317 None,
318 None,
319 )
320 .unwrap();
321
322 let ctx = Context {
325 method: String::new(),
326 pubkey: String::from("020000000000000000"),
327 time: SystemTime::now(),
328 unique_id: String::new(),
329 };
330 assert!(r1.are_restrictions_met(ctx).is_ok());
331 let ctx = Context {
333 method: String::from("ListFunds"),
334 pubkey: String::from("020000000000000000"),
335 time: SystemTime::now(),
336 unique_id: String::new(),
337 };
338 assert!(r1.are_restrictions_met(ctx).is_ok());
339 let ctx = Context {
341 method: String::from("GetInfo"),
342 pubkey: String::new(),
343 time: SystemTime::now(),
344 unique_id: String::new(),
345 };
346 assert!(r2.are_restrictions_met(ctx).is_ok());
347 let ctx = Context {
349 method: String::from("GetInfo"),
350 pubkey: String::from("020000000000000000"),
351 time: SystemTime::now(),
352 unique_id: String::new(),
353 };
354 assert!(r2.are_restrictions_met(ctx).is_ok());
355 let ctx = Context {
357 method: String::from("GetInfo"),
358 pubkey: String::new(),
359 time: SystemTime::now(),
360 unique_id: String::new(),
361 };
362 assert!(r3.are_restrictions_met(ctx).is_ok());
363 let ctx = Context {
365 method: String::new(),
366 pubkey: String::from("020000000000000000"),
367 time: SystemTime::now(),
368 unique_id: String::new(),
369 };
370 assert!(r4.are_restrictions_met(ctx).is_ok());
371
372 let ctx = Context {
375 method: String::from("ListFunds"),
376 pubkey: String::from("030000"),
377 time: SystemTime::now(),
378 unique_id: String::new(),
379 };
380 assert!(r1.are_restrictions_met(ctx).is_err());
381 let ctx = Context {
383 method: String::from("ListFunds"),
384 pubkey: String::from("030000"),
385 time: SystemTime::now(),
386 unique_id: String::new(),
387 };
388 assert!(r2.are_restrictions_met(ctx).is_err());
389 let ctx = Context {
391 method: String::new(),
392 pubkey: String::from("030000"),
393 time: SystemTime::now(),
394 unique_id: String::new(),
395 };
396 assert!(r3.are_restrictions_met(ctx).is_err());
397 let ctx = Context {
399 method: String::from("GetInfo"),
400 pubkey: String::new(),
401 time: SystemTime::now(),
402 unique_id: String::new(),
403 };
404 assert!(r4.are_restrictions_met(ctx).is_err());
405 }
406}