use runeauth::{Alternative, Check, Condition, ConditionChecker, Restriction, Rune, RuneError};
use std::fmt::Display;
use std::time::{SystemTime, UNIX_EPOCH};
pub trait Restrictor {
fn generate(self) -> Result<Vec<Restriction>, RuneError>;
}
pub struct RuneFactory;
impl RuneFactory {
pub fn carve<T: Restrictor + Copy>(origin: &Rune, append: &[T]) -> Result<String, RuneError> {
let restrictions = append.into_iter().try_fold(Vec::new(), |mut acc, res| {
let mut r = res.generate()?;
acc.append(&mut r);
Ok(acc)
})?;
let mut originc = origin.clone();
restrictions.into_iter().for_each(|r| {
let _ = originc.add_restriction(r);
});
Ok(originc.to_base64())
}
}
#[derive(Clone, Copy)]
pub enum DefRules<'a> {
ReadOnly,
Pay,
Add(&'a [DefRules<'a>]),
}
impl<'a> Restrictor for DefRules<'a> {
fn generate(self) -> Result<Vec<Restriction>, RuneError> {
match self {
DefRules::ReadOnly => {
let a: Vec<Restriction> = vec![Restriction::new(vec![
alternative("method", Condition::BeginsWith, "Get").unwrap(),
alternative("method", Condition::BeginsWith, "List").unwrap(),
])
.unwrap()];
Ok(a)
}
DefRules::Pay => {
let a =
vec![Restriction::new(vec![
alternative("method", Condition::Equal, "pay").unwrap()
])
.unwrap()];
Ok(a)
}
DefRules::Add(rules) => {
let alt_set =
rules
.into_iter()
.try_fold(Vec::new(), |mut acc: Vec<Alternative>, rule| {
let mut alts = rule
.generate()?
.into_iter()
.flat_map(|r| r.alternatives)
.collect();
acc.append(&mut alts);
Ok(acc)
})?;
let a = vec![Restriction::new(alt_set)?];
Ok(a)
}
}
}
}
impl<'a> Display for DefRules<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DefRules::ReadOnly => write!(f, "readonly"),
DefRules::Pay => write!(f, "pay"),
DefRules::Add(rules) => {
write!(
f,
"{}",
rules.into_iter().fold(String::new(), |acc, r| {
if acc.is_empty() {
format!("{}", r)
} else {
format!("{}|{}", acc, r)
}
})
)
}
}
}
}
fn alternative(field: &str, cond: Condition, value: &str) -> Result<Alternative, RuneError> {
Alternative::new(field.to_string(), cond, value.to_string(), false)
}
#[derive(Clone)]
pub struct Context {
pub method: String,
pub pubkey: String,
pub unique_id: String,
pub time: SystemTime,
}
impl Check for Context {
fn check_alternative(&self, alt: &Alternative) -> anyhow::Result<(), RuneError> {
let value = match alt.get_field().as_str() {
"" => self.unique_id.clone(),
"method" => self.method.clone(),
"pubkey" => self.pubkey.clone(),
"time" => self
.time
.duration_since(UNIX_EPOCH)
.map_err(|e| {
RuneError::Unknown(format!("Can not extract seconds from timestamp {:?}", e))
})?
.as_secs()
.to_string(),
_ => String::new(), };
ConditionChecker { value }.check_alternative(alt)
}
}
#[cfg(test)]
mod tests {
use super::{Context, DefRules, RuneFactory};
use base64::{engine::general_purpose, Engine as _};
use runeauth::{Alternative, Condition, Restriction, Rune};
use std::time::SystemTime;
#[test]
fn test_carve_readonly_rune() {
let seed = [0; 32];
let mr = Rune::new_master_rune(&seed, vec![], None, None).unwrap();
let carved = RuneFactory::carve(&mr, &[DefRules::ReadOnly]).unwrap();
let carved_byt = general_purpose::URL_SAFE.decode(&carved).unwrap();
let carved_restr = String::from_utf8(carved_byt[32..].to_vec()).unwrap(); assert_eq!(carved_restr, *"method^Get|method^List");
let carved_rune = Rune::from_base64(&carved).unwrap();
assert!(mr.is_authorized(&carved_rune));
}
#[test]
fn test_carve_disjunction_rune() {
let seed = [0; 32];
let mr = Rune::new_master_rune(&seed, vec![], None, None).unwrap();
let carved =
RuneFactory::carve(&mr, &[DefRules::Add(&[DefRules::ReadOnly, DefRules::Pay])])
.unwrap();
let carved_byt = general_purpose::URL_SAFE.decode(&carved).unwrap();
let carved_restr = String::from_utf8(carved_byt[32..].to_vec()).unwrap(); assert_eq!(carved_restr, *"method^Get|method^List|method=pay");
let carved_rune = Rune::from_base64(&carved).unwrap();
assert!(mr.is_authorized(&carved_rune));
}
#[test]
fn test_defrules_display() {
let r = DefRules::Pay;
assert_eq!(format!("{}", r), "pay");
let r = DefRules::Add(&[DefRules::Pay]);
assert_eq!(format!("{}", r), "pay");
let r = DefRules::Add(&[DefRules::Pay, DefRules::ReadOnly]);
assert_eq!(format!("{}", r), "pay|readonly");
}
#[test]
fn test_context_check() {
let seedsecret = &[0; 32];
let mr = Rune::new_master_rune(seedsecret, vec![], None, None).unwrap();
let r1 = Rune::new(
mr.authcode(),
vec![Restriction::new(vec![Alternative::new(
String::from("pubkey"),
Condition::Equal,
String::from("020000000000000000"),
false,
)
.unwrap()])
.unwrap()],
None,
None,
)
.unwrap();
let r2 = Rune::new(
mr.authcode(),
vec![Restriction::new(vec![Alternative::new(
String::from("method"),
Condition::Equal,
String::from("GetInfo"),
false,
)
.unwrap()])
.unwrap()],
None,
None,
)
.unwrap();
let r3 = Rune::new(
mr.authcode(),
vec![Restriction::new(vec![Alternative::new(
String::from("pubkey"),
Condition::Missing,
String::new(),
false,
)
.unwrap()])
.unwrap()],
None,
None,
)
.unwrap();
let r4 = Rune::new(
mr.authcode(),
vec![Restriction::new(vec![Alternative::new(
String::from("method"),
Condition::Missing,
String::new(),
false,
)
.unwrap()])
.unwrap()],
None,
None,
)
.unwrap();
let ctx = Context {
method: String::new(),
pubkey: String::from("020000000000000000"),
time: SystemTime::now(),
unique_id: String::new(),
};
assert!(r1.are_restrictions_met(ctx).is_ok());
let ctx = Context {
method: String::from("ListFunds"),
pubkey: String::from("020000000000000000"),
time: SystemTime::now(),
unique_id: String::new(),
};
assert!(r1.are_restrictions_met(ctx).is_ok());
let ctx = Context {
method: String::from("GetInfo"),
pubkey: String::new(),
time: SystemTime::now(),
unique_id: String::new(),
};
assert!(r2.are_restrictions_met(ctx).is_ok());
let ctx = Context {
method: String::from("GetInfo"),
pubkey: String::from("020000000000000000"),
time: SystemTime::now(),
unique_id: String::new(),
};
assert!(r2.are_restrictions_met(ctx).is_ok());
let ctx = Context {
method: String::from("GetInfo"),
pubkey: String::new(),
time: SystemTime::now(),
unique_id: String::new(),
};
assert!(r3.are_restrictions_met(ctx).is_ok());
let ctx = Context {
method: String::new(),
pubkey: String::from("020000000000000000"),
time: SystemTime::now(),
unique_id: String::new(),
};
assert!(r4.are_restrictions_met(ctx).is_ok());
let ctx = Context {
method: String::from("ListFunds"),
pubkey: String::from("030000"),
time: SystemTime::now(),
unique_id: String::new(),
};
assert!(r1.are_restrictions_met(ctx).is_err());
let ctx = Context {
method: String::from("ListFunds"),
pubkey: String::from("030000"),
time: SystemTime::now(),
unique_id: String::new(),
};
assert!(r2.are_restrictions_met(ctx).is_err());
let ctx = Context {
method: String::new(),
pubkey: String::from("030000"),
time: SystemTime::now(),
unique_id: String::new(),
};
assert!(r3.are_restrictions_met(ctx).is_err());
let ctx = Context {
method: String::from("GetInfo"),
pubkey: String::new(),
time: SystemTime::now(),
unique_id: String::new(),
};
assert!(r4.are_restrictions_met(ctx).is_err());
}
}