fapolicy_rules/
db.rs

1/*
2 * Copyright Concurrent Technologies Corporation 2021
3 *
4 * This Source Code Form is subject to the terms of the Mozilla Public
5 * License, v. 2.0. If a copy of the MPL was not distributed with this
6 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
7 */
8
9use std::collections::btree_map::Iter;
10use std::collections::BTreeMap;
11use std::fmt::{Display, Formatter};
12
13use Entry::*;
14
15use crate::{Rule, Set};
16
17#[derive(Clone, Debug)]
18pub struct RuleEntry {
19    pub id: usize,
20    pub text: String,
21    pub origin: Origin,
22    pub valid: bool,
23    pub msg: Option<String>,
24    _fk: usize,
25}
26
27#[derive(Clone, Debug)]
28pub struct SetEntry {
29    pub name: String,
30    pub text: String,
31    pub origin: Origin,
32    pub valid: bool,
33    pub msg: Option<String>,
34    _fk: usize,
35}
36
37#[derive(Clone, Debug)]
38pub struct CommentEntry {
39    pub text: String,
40    pub origin: Origin,
41    _fk: usize,
42}
43
44/// Rule Definition
45/// Can be valid or invalid
46/// When invalid it provides the text definition
47/// When valid the text definition can be rendered from the ADTs
48#[derive(Clone, Debug)]
49pub enum Entry {
50    // rules
51    ValidRule(Rule),
52    RuleWithWarning(Rule, String),
53    Invalid { text: String, error: String },
54
55    // sets
56    ValidSet(Set),
57    SetWithWarning(Set, String),
58    InvalidSet { text: String, error: String },
59
60    // other
61    Comment(String),
62}
63
64impl Display for Entry {
65    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
66        let txt = match self {
67            ValidRule(r) | RuleWithWarning(r, _) => r.to_string(),
68            ValidSet(r) | SetWithWarning(r, _) => r.to_string(),
69            Invalid { text, .. } => text.clone(),
70            InvalidSet { text, .. } => text.clone(),
71            Comment(text) => format!("#{}", text),
72        };
73        f.write_fmt(format_args!("{}", txt))
74    }
75}
76
77impl Entry {
78    fn diagnostic_messages(&self) -> Option<String> {
79        match self {
80            RuleWithWarning(_, w) | SetWithWarning(_, w) => Some(w.clone()),
81            Invalid { error, .. } | InvalidSet { error, .. } => Some(error.clone()),
82            _ => None,
83        }
84    }
85}
86
87fn is_valid(def: &Entry) -> bool {
88    !matches!(def, Invalid { .. } | InvalidSet { .. })
89}
90
91fn is_rule(def: &Entry) -> bool {
92    matches!(def, ValidRule(_) | RuleWithWarning(..) | Invalid { .. })
93}
94
95fn is_set(def: &Entry) -> bool {
96    matches!(def, ValidSet(_) | SetWithWarning(..) | InvalidSet { .. })
97}
98
99fn is_comment(def: &Entry) -> bool {
100    matches!(def, Comment(_))
101}
102
103type Origin = String;
104type DbEntry = (Origin, Entry);
105
106/// Rules Database
107/// A container for rules and their metadata
108#[derive(Clone, Debug, Default)]
109pub struct DB {
110    model: BTreeMap<usize, DbEntry>,
111    rules: BTreeMap<usize, RuleEntry>,
112    sets: BTreeMap<usize, SetEntry>,
113    comments: BTreeMap<usize, CommentEntry>,
114}
115
116impl From<Vec<(Origin, Entry)>> for DB {
117    fn from(s: Vec<(String, Entry)>) -> Self {
118        DB::from_sources(s)
119    }
120}
121
122impl DB {
123    /// Construct DB using the provided RuleDefs and associated sources
124    pub(crate) fn from_sources(defs: Vec<(Origin, Entry)>) -> Self {
125        let model: BTreeMap<usize, DbEntry> = defs
126            .into_iter()
127            .enumerate()
128            .map(|(i, (source, d))| (i, (source, d)))
129            .collect();
130
131        let rules: BTreeMap<usize, RuleEntry> = model
132            .iter()
133            .filter(|(_fk, (_, e))| is_rule(e))
134            .enumerate()
135            .map(|(id, (fk, (o, e)))| RuleEntry {
136                id: id + 1,
137                text: e.to_string(),
138                origin: o.clone(),
139                valid: is_valid(e),
140                msg: e.diagnostic_messages(),
141                _fk: *fk,
142            })
143            .map(|e| (e.id, e))
144            .collect();
145
146        let sets: BTreeMap<usize, SetEntry> = model
147            .iter()
148            .enumerate()
149            .map(|(fk, v)| (v, fk))
150            .filter(|((_, (_, m)), _)| is_set(m))
151            .map(|((id, (o, e)), fk)| {
152                (
153                    *id,
154                    SetEntry {
155                        // todo;; extract the set name
156                        name: "_".to_string(),
157                        text: e.to_string(),
158                        origin: o.clone(),
159                        valid: is_valid(e),
160                        msg: e.diagnostic_messages(),
161                        _fk: fk,
162                    },
163                )
164            })
165            .collect();
166
167        let comments = model
168            .iter()
169            .enumerate()
170            .map(|(fk, v)| (v, fk))
171            .filter(|((_, (_, m)), _)| is_comment(m))
172            .map(|((id, (o, e)), fk)| {
173                (
174                    *id,
175                    CommentEntry {
176                        text: e.to_string(),
177                        origin: o.clone(),
178                        _fk: fk,
179                    },
180                )
181            })
182            .collect();
183
184        Self {
185            model,
186            rules,
187            sets,
188            comments,
189        }
190    }
191
192    /// Get the number of RuleDefs
193    pub fn len(&self) -> usize {
194        self.model.len()
195    }
196
197    /// Test if there are any RuleDefs in this DB
198    pub fn is_empty(&self) -> bool {
199        self.model.is_empty()
200    }
201
202    /// Get a RuleEntry ref by ID
203    pub fn rule(&self, num: usize) -> Option<&RuleEntry> {
204        self.rules.get(&num)
205    }
206
207    /// Get a RuleEntry ref by FK
208    pub fn rule_rev(&self, fk: usize) -> Option<&RuleEntry> {
209        self.rules.iter().find(|(_, e)| e._fk == fk).map(|(_, e)| e)
210    }
211
212    /// Get a vec of all RuleEntry refs
213    pub fn rules(&self) -> Vec<&RuleEntry> {
214        self.rules.values().collect()
215    }
216
217    /// Get a vec of all SetEntry refs
218    pub fn sets(&self) -> Vec<&SetEntry> {
219        self.sets.values().collect()
220    }
221
222    /// Get a vec of all CommentEntry refs
223    pub fn comments(&self) -> Vec<&CommentEntry> {
224        self.comments.values().collect()
225    }
226
227    pub fn entry(&self, num: usize) -> Option<&Entry> {
228        self.model.get(&num).map(|(_, e)| e)
229    }
230
231    /// Get a model iterator
232    pub fn iter(&self) -> Iter<'_, usize, DbEntry> {
233        self.model.iter()
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use crate::{Decision, Object, Permission, Subject};
240
241    use super::*;
242
243    impl From<Rule> for Entry {
244        fn from(r: Rule) -> Self {
245            ValidRule(r)
246        }
247    }
248
249    impl DB {
250        fn from_source(origin: Origin, defs: Vec<Entry>) -> Self {
251            DB::from_sources(defs.into_iter().map(|d| (origin.clone(), d)).collect())
252        }
253    }
254
255    impl Entry {
256        pub fn unwrap(&self) -> Rule {
257            match self {
258                ValidRule(val) => val.clone(),
259                RuleWithWarning(val, _) => val.clone(),
260                _ => {
261                    panic!("called unwrap on an invalid rule or set def")
262                }
263            }
264        }
265    }
266
267    fn any_all_all(decision: Decision) -> Entry {
268        Rule::new(Subject::all(), Permission::Any, Object::all(), decision).into()
269    }
270
271    #[test]
272    fn default_db_is_empty() {
273        assert!(DB::default().is_empty());
274    }
275
276    #[test]
277    fn db_create() {
278        let r1: Entry = Rule::new(
279            Subject::all(),
280            Permission::Any,
281            Object::all(),
282            Decision::Allow,
283        )
284        .into();
285        let source = "foo.rules".to_string();
286        let db: DB = vec![(source, r1)].into();
287        assert!(!db.is_empty());
288        assert!(db.rule(1).is_some());
289    }
290
291    #[test]
292    fn db_create_single_source() {
293        let r1 = any_all_all(Decision::Allow);
294        let r2 = any_all_all(Decision::Deny);
295
296        let source = "/foo/bar.rules";
297        let db: DB = DB::from_source(source.to_string(), vec![r1, r2]);
298        assert_eq!(db.rule(1).unwrap().origin, source);
299        assert_eq!(db.rule(2).unwrap().origin, source);
300    }
301
302    #[test]
303    fn db_create_each_source() {
304        let r1 = any_all_all(Decision::Allow);
305        let r2 = any_all_all(Decision::Deny);
306
307        let source1 = "/foo.rules";
308        let source2 = "/bar.rules";
309        let db: DB = DB::from_sources(vec![(source1.to_string(), r1), (source2.to_string(), r2)]);
310        assert_eq!(db.rule(1).unwrap().origin, source1);
311        assert_eq!(db.rule(2).unwrap().origin, source2);
312    }
313
314    #[test]
315    fn maintain_order() {
316        let source = "foo.rules".to_string();
317        let subjs = vec!["fee", "fi", "fo", "fum", "this", "is", "such", "fun"];
318        let rules: Vec<(String, Entry)> = subjs
319            .iter()
320            .map(|s| {
321                (
322                    source.clone(),
323                    Rule::new(
324                        Subject::from_exe(s),
325                        Permission::Any,
326                        Object::all(),
327                        Decision::Allow,
328                    )
329                    .into(),
330                )
331            })
332            .collect();
333
334        let db: DB = rules.into();
335        assert!(!db.is_empty());
336        assert_eq!(db.len(), 8);
337
338        for s in subjs.iter().enumerate() {
339            assert_eq!(db.entry(s.0).unwrap().unwrap().subj.exe().unwrap(), *s.1);
340        }
341    }
342
343    #[test]
344    fn test_prefixed_comment() {
345        assert!(Comment("sometext".to_string()).to_string().starts_with('#'))
346    }
347}