gl_client/
runes.rs

1use runeauth::{Alternative, Check, Condition, ConditionChecker, Restriction, Rune, RuneError};
2use std::fmt::Display;
3use std::time::{SystemTime, UNIX_EPOCH};
4
5/// Represents an entity that can provide restrictions.
6///
7/// The `Restrictor` trait should be implemented by types that are able to
8/// produce a list of `Restriction`s. The `generate` method returns a `Result`
9/// containing a vector of `Restriction`s or a `RuneError` in case of any error.
10pub trait Restrictor {
11    /// Retrieves the restrictions associated with the current instance.
12    ///
13    /// # Returns
14    /// A `Result` containing a `Vec` of `Restriction`s. In the event of
15    /// failure, returns a `RuneError`.
16    fn generate(self) -> Result<Vec<Restriction>, RuneError>;
17}
18
19/// A factory responsible for carving runes.
20///
21/// `RuneFactory` provides utility functions to manipulate and produce runes
22/// with certain characteristics, such as additional restrictions.
23pub struct RuneFactory;
24
25impl RuneFactory {
26    /// Combines an original `Rune` with a list of restricters,
27    /// and produces a new rune in base64 format.
28    ///
29    /// # Parameters
30    /// - `origin`: A reference to the original `Rune` that will serve as the
31    /// base.
32    /// - `append`: A `Vec` containing entities that implement the `Restrictor`
33    /// trait.
34    ///
35    /// # Returns
36    /// A `Result` containing a `String` representing the carved rune in base64 format.
37    /// In the event of any failure during the carving process, returns a `RuneError`.
38    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            // Changes are applied in place, as well as returned, so
48            // this is ok.
49            let _ = originc.add_restriction(r);
50        });
51
52        Ok(originc.to_base64())
53    }
54}
55
56/// Predefined rule sets to generate `Restriction`s from.
57#[derive(Clone, Copy)]
58pub enum DefRules<'a> {
59    /// Represents a rule set where only read operations are allowed. This
60    /// translates to a `Restriction` that is "method^Get|method^List".
61    ReadOnly,
62    /// Represents a rule set where only the `pay` method is allowed. This
63    /// translates to a `Restriction` that is "method=pay".
64    Pay,
65    /// A special rule that adds the alternatives of the given `DefRules`
66    /// in a disjunctive set. Example: Add(vec![ReadOnly, Pay]) translates
67    /// to a `Restriction` that is "method^Get|method^List|method=pay".
68    Add(&'a [DefRules<'a>]),
69}
70
71impl<'a> Restrictor for DefRules<'a> {
72    /// Generate the actual `Restriction` entities based on the predefined rule
73    /// sets.
74    ///
75    /// # Returns
76    /// A `Result` containing a vector of `Restriction` entities or a `RuneError`
77    /// if there's any error while generating the restrictions.
78    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
138/// Creates an `Alternative` based on the provided field, condition, and value.
139///
140/// This function is a shorthand for creating new `Alternative` entities
141/// without having to manually wrap field and value into `String`.
142///
143/// # Parameters
144/// - `field`: The field on which the alternative is based.
145/// - `cond`: The condition to check against the field.
146/// - `value`: The value to match with the condition against the field.
147///
148/// # Returns
149///
150/// A result containing the created `Alternative` or a `RuneError` if there's
151/// any error in the creation.
152fn alternative(field: &str, cond: Condition, value: &str) -> Result<Alternative, RuneError> {
153    Alternative::new(field.to_string(), cond, value.to_string(), false)
154}
155
156/// A context struct that holds information relevant to check a command against
157/// a rune.
158#[derive(Clone)]
159pub struct Context {
160    // The rpc method associated with the request.
161    pub method: String,
162    // The public key associated with the request.
163    pub pubkey: String,
164    // The unique id.
165    pub unique_id: String,
166    // The timestamp associated with the request.
167    pub time: SystemTime,
168    // Todo (nepet): Add param field that uses enum or serde to store the params  of a call.
169}
170
171/// Implementation of the `Check` trait for the `Context` struct, allowing it to
172/// perform checks on rune alternatives.
173impl Check for Context {
174    /// Performs a check on an alternative based on the context's fields.
175    ///
176    /// # Arguments
177    ///
178    /// * `alt` - The alternative to check against the context.
179    ///
180    /// # Returns
181    ///
182    /// * `Ok(())` if the check is successful, an `Err` containing a `RuneError` otherwise.
183    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(), // If we don't know the field we can not set it!
197        };
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        // Carve a new rune from the master rune with given restrictions.
215        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(); // Strip off the authcode to inspect the restrictions.
219        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        // Carve a new rune from the master rune with given restrictions.
231        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(); // Strip off the authcode to inspect the restrictions.
237        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        // r1 restrictions: "pubkey=020000000000000000"
259        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        // r2 restrictions: "method=GetInfo"
275        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        // r3 restrictions: "pubkey!"
291        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        // r4 restriction: "method!"
307        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        // These should succeed.
323        // Check with method="", pubkey=020000000000000000
324        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        // Check with method="ListFunds", pubkey=020000000000000000
332        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        // Check with method="GetInfo", pubkey=""
340        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        // Check with method="GetInfo", pubkey="020000000000000000"
348        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        // Check with method="GetInfo", pubkey=""
356        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        // Check with method="", pubkey="020000"
364        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        // These should fail.
373        // Check with method="ListFunds", pubkey=030000, wrong pubkey.
374        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        // Check with method="ListFunds", pubkey=030000, wrong method.
382        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        // Check with pubkey=030000, pubkey present.
390        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        // Check with method="GetInfo", method present.
398        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}