archivist_lib/pipeline/
allowlist.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::error::Error;
6use anyhow::anyhow;
7use diagnostics_hierarchy::HierarchyMatcher;
8use fidl_fuchsia_diagnostics::{Selector, TreeNames};
9use fidl_fuchsia_inspect::DEFAULT_TREE_NAME;
10use moniker::ExtendedMoniker;
11use selectors::SelectorExt;
12use std::collections::HashMap;
13use std::sync::Arc;
14
15#[derive(Debug, Clone)]
16enum ComponentAllowlistState {
17    // Mapping of Inspect tree names to HierarchMatchers.
18    FilteringEnabled {
19        names_allowlist: Arc<HashMap<String, HierarchyMatcher>>,
20        all_allowlist: Option<Arc<HierarchyMatcher>>,
21    },
22
23    // Indicates that the privacy pipeline is enabled, and NO data for this component is
24    // approved for exfiltration by Archivist.
25    AllFilteredOut,
26
27    // No privacy pipeline is enabled.
28    FilteringDisabled,
29}
30
31pub enum PrivacyExplicitOption<T> {
32    // Privacy pipeline is enabled and `Self` holds a `T`.
33    Found(T),
34
35    // Privacy pipeline is enabled and no data is held in `Self`.
36    NotFound,
37
38    // Privacy pipeline is disabled.
39    FilteringDisabled,
40}
41
42#[derive(Debug, Clone)]
43pub struct ComponentAllowlist(ComponentAllowlistState);
44
45impl ComponentAllowlist {
46    pub fn all_filtered_out(&self) -> bool {
47        matches!(self.0, ComponentAllowlistState::AllFilteredOut)
48    }
49
50    pub fn matcher(&self, name: &str) -> PrivacyExplicitOption<&HierarchyMatcher> {
51        match &self.0 {
52            ComponentAllowlistState::FilteringEnabled { names_allowlist, all_allowlist } => {
53                if let Some(matcher) = names_allowlist.get(name) {
54                    PrivacyExplicitOption::Found(matcher)
55                } else if let Some(matcher) = all_allowlist {
56                    PrivacyExplicitOption::Found(matcher.as_ref())
57                } else {
58                    PrivacyExplicitOption::NotFound
59                }
60            }
61            ComponentAllowlistState::AllFilteredOut => PrivacyExplicitOption::NotFound,
62            ComponentAllowlistState::FilteringDisabled => PrivacyExplicitOption::FilteringDisabled,
63        }
64    }
65
66    fn new<'a>(
67        selectors_for_this_component: impl Iterator<Item = Result<&'a Selector, anyhow::Error>>,
68    ) -> Result<Self, Error> {
69        let buckets = bucketize_selectors_by_name(selectors_for_this_component)?;
70        Ok(Self(ComponentAllowlistState::FilteringEnabled {
71            names_allowlist: Arc::new(HashMap::from_iter(
72                buckets
73                    .names
74                    .into_iter()
75                    .map(|(k, v)| Ok((k, HierarchyMatcher::try_from(v)?)))
76                    .collect::<Result<Vec<_>, Error>>()?,
77            )),
78            all_allowlist: buckets.all.map(HierarchyMatcher::try_from).transpose()?.map(Arc::new),
79        }))
80    }
81}
82
83#[cfg(test)]
84impl ComponentAllowlist {
85    pub fn filtering_enabled(&self) -> bool {
86        match self.0 {
87            ComponentAllowlistState::FilteringEnabled { .. }
88            | ComponentAllowlistState::AllFilteredOut => true,
89            ComponentAllowlistState::FilteringDisabled => false,
90        }
91    }
92
93    pub fn new_disabled() -> Self {
94        ComponentAllowlist(ComponentAllowlistState::FilteringDisabled)
95    }
96}
97
98struct BucketedSelectors<'a> {
99    all: Option<Vec<&'a Selector>>,
100    names: HashMap<String, Vec<&'a Selector>>,
101}
102
103fn bucketize_selectors_by_name<'a>(
104    selectors: impl Iterator<Item = Result<&'a Selector, anyhow::Error>>,
105) -> Result<BucketedSelectors<'a>, Error> {
106    let mut names_to_selectors: HashMap<_, Vec<_>> = HashMap::new();
107    let mut selectors_against_all = vec![];
108    // de-duplicating selectors would minimize this interim map that is returned, but
109    // during construction the final HierarchyMatcher will de-duplicate, so it seems not
110    // worth it here
111    for s in selectors {
112        let s = s.map_err(Error::Selectors)?;
113        match s.tree_names {
114            Some(TreeNames::Some(ref tree_names)) => {
115                for name in tree_names {
116                    if let Some(mapped_selectors) = names_to_selectors.get_mut(name) {
117                        mapped_selectors.push(s);
118                    } else {
119                        names_to_selectors.insert(name.to_string(), vec![s]);
120                    }
121                }
122            }
123            None => {
124                if let Some(mapped_selectors) = names_to_selectors.get_mut(DEFAULT_TREE_NAME) {
125                    mapped_selectors.push(s);
126                } else {
127                    names_to_selectors.insert(DEFAULT_TREE_NAME.to_string(), vec![s]);
128                }
129            }
130            Some(TreeNames::All(_)) => {
131                selectors_against_all.push(s);
132            }
133            Some(TreeNames::__SourceBreaking { unknown_ordinal }) => {
134                return Err(Error::Selectors(anyhow!(
135                    "unknown TreeNames variant {unknown_ordinal} in {s:?}"
136                )));
137            }
138        }
139    }
140
141    if selectors_against_all.is_empty() {
142        return Ok(BucketedSelectors { all: None, names: names_to_selectors });
143    }
144
145    for names in names_to_selectors.values_mut() {
146        names.extend(selectors_against_all.iter());
147    }
148
149    Ok(BucketedSelectors { all: Some(selectors_against_all), names: names_to_selectors })
150}
151
152#[derive(Clone)]
153enum StaticHierarchyAllowlistState {
154    // Mapping of components to exfiltration allowlists for the component.
155    FilteringEnabled {
156        component_allowlists: HashMap<ExtendedMoniker, ComponentAllowlist>,
157        all_selectors: Vec<Selector>,
158    },
159
160    // The privacy pipeline is disabled.
161    FilteringDisabled,
162}
163
164#[derive(Clone)]
165pub struct StaticHierarchyAllowlist(StaticHierarchyAllowlistState);
166
167impl StaticHierarchyAllowlist {
168    /// Get the allowlist for the given moniker.
169    ///
170    /// The component must be added via `Self::add_component` before attempting
171    /// to access it via this method.
172    pub fn get(&self, moniker: &ExtendedMoniker) -> ComponentAllowlist {
173        match &self.0 {
174            StaticHierarchyAllowlistState::FilteringEnabled { component_allowlists, .. } => {
175                component_allowlists
176                    .get(moniker)
177                    .cloned()
178                    .unwrap_or(ComponentAllowlist(ComponentAllowlistState::AllFilteredOut))
179            }
180            StaticHierarchyAllowlistState::FilteringDisabled => {
181                ComponentAllowlist(ComponentAllowlistState::FilteringDisabled)
182            }
183        }
184    }
185
186    pub fn new(all_selectors: Option<Vec<Selector>>) -> Self {
187        if let Some(all_selectors) = all_selectors {
188            return Self(StaticHierarchyAllowlistState::FilteringEnabled {
189                // lazily generate the allowlists as components are added
190                component_allowlists: HashMap::new(),
191                all_selectors,
192            });
193        }
194
195        Self(StaticHierarchyAllowlistState::FilteringDisabled)
196    }
197
198    pub fn remove_component(&mut self, moniker: &ExtendedMoniker) {
199        match &mut self.0 {
200            StaticHierarchyAllowlistState::FilteringEnabled { component_allowlists, .. } => {
201                component_allowlists.remove(moniker);
202            }
203            StaticHierarchyAllowlistState::FilteringDisabled => {}
204        }
205    }
206
207    /// Populate the allowlist for the component referred to by `moniker`.
208    pub fn add_component(&mut self, moniker: ExtendedMoniker) -> Result<(), Error> {
209        match &mut self.0 {
210            StaticHierarchyAllowlistState::FilteringEnabled {
211                all_selectors,
212                component_allowlists,
213            } => {
214                let mut matched_selectors =
215                    moniker.match_against_selectors(all_selectors.iter()).peekable();
216                if matched_selectors.peek().is_none() {
217                    drop(matched_selectors);
218                    component_allowlists.insert(
219                        moniker,
220                        ComponentAllowlist(ComponentAllowlistState::AllFilteredOut),
221                    );
222                } else {
223                    let allowlist = ComponentAllowlist::new(matched_selectors)?;
224                    component_allowlists.insert(moniker, allowlist);
225                }
226            }
227            StaticHierarchyAllowlistState::FilteringDisabled => {}
228        }
229
230        Ok(())
231    }
232}
233
234#[cfg(test)]
235impl StaticHierarchyAllowlist {
236    pub fn filtering_enabled(&self) -> bool {
237        match self.0 {
238            StaticHierarchyAllowlistState::FilteringEnabled { .. } => true,
239            StaticHierarchyAllowlistState::FilteringDisabled => false,
240        }
241    }
242
243    pub fn component_was_added(&self, moniker: &ExtendedMoniker) -> bool {
244        match &self.0 {
245            StaticHierarchyAllowlistState::FilteringEnabled { component_allowlists, .. } => {
246                component_allowlists.get(moniker).is_some()
247            }
248            StaticHierarchyAllowlistState::FilteringDisabled => false,
249        }
250    }
251
252    pub fn new_disabled() -> Self {
253        Self(StaticHierarchyAllowlistState::FilteringDisabled)
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use selectors::{parse_selector, VerboseError};
261
262    fn make_selectors(s: &[Selector]) -> impl Iterator<Item = Result<&Selector, anyhow::Error>> {
263        s.iter().map(Ok)
264    }
265
266    fn matcher_found(m: PrivacyExplicitOption<&HierarchyMatcher>) -> bool {
267        matches!(m, PrivacyExplicitOption::Found(_))
268    }
269
270    fn matcher_not_found(m: PrivacyExplicitOption<&HierarchyMatcher>) -> bool {
271        matches!(m, PrivacyExplicitOption::NotFound)
272    }
273
274    #[fuchsia::test]
275    fn component_allowlist() {
276        let selectors = &[
277            parse_selector::<VerboseError>(r#"*:[name=foo]root:hello"#).unwrap(),
278            parse_selector::<VerboseError>(r#"*:[name=bar]root:hello"#).unwrap(),
279            parse_selector::<VerboseError>(r#"*:[...]root:good"#).unwrap(),
280        ];
281
282        let selectors = make_selectors(selectors);
283
284        let list = ComponentAllowlist::new(selectors).unwrap();
285        assert!(list.filtering_enabled());
286
287        assert!(matcher_found(list.matcher("foo")));
288        assert!(matcher_found(list.matcher("bar")));
289        assert!(matcher_found(list.matcher("should match all")));
290    }
291
292    #[fuchsia::test]
293    fn test_bucketize_selectors() {
294        let orig_selectors = &[
295            parse_selector::<VerboseError>(r#"*:[name=foo]root:hello"#).unwrap(),
296            parse_selector::<VerboseError>(r#"*:[name=bar]root:hello"#).unwrap(),
297            parse_selector::<VerboseError>(r#"*:[...]root:something"#).unwrap(),
298            parse_selector::<VerboseError>(r#"*:[name=foo]root:goodbye"#).unwrap(),
299        ];
300
301        let selectors = make_selectors(orig_selectors);
302
303        let buckets = bucketize_selectors_by_name(selectors).unwrap();
304
305        let named_expected = HashMap::from([
306            ("foo".into(), vec![&orig_selectors[0], &orig_selectors[3], &orig_selectors[2]]),
307            ("bar".into(), vec![&orig_selectors[1], &orig_selectors[2]]),
308        ]);
309
310        let all_expected = Some(vec![&orig_selectors[2]]);
311
312        assert_eq!(buckets.names, named_expected);
313        assert_eq!(buckets.all, all_expected);
314    }
315
316    #[fuchsia::test]
317    fn static_hierarchy_allowlist() {
318        let selectors = vec![
319            parse_selector::<VerboseError>(r#"component1:[name=foo]root:foo_one"#).unwrap(),
320            parse_selector::<VerboseError>(r#"component1:[...]root:all_one"#).unwrap(),
321            parse_selector::<VerboseError>(r#"*:[name=foo, name=bar]root"#).unwrap(),
322            parse_selector::<VerboseError>(r#"component2:[name=bar]root:bar_two"#).unwrap(),
323            parse_selector::<VerboseError>(r#"component2:[name=qux]root:bar_two"#).unwrap(),
324        ];
325
326        let mut allowlist = StaticHierarchyAllowlist::new(Some(selectors));
327
328        assert!(allowlist.filtering_enabled());
329
330        let component_moniker = ExtendedMoniker::try_from("component1").unwrap();
331        allowlist.add_component(component_moniker.clone()).unwrap();
332
333        let list = allowlist.get(&component_moniker);
334
335        assert!(list.filtering_enabled());
336
337        assert!(matcher_found(list.matcher("foo")));
338        assert!(matcher_found(list.matcher("bar")));
339        assert!(matcher_found(list.matcher("should match all")));
340
341        let component_moniker = ExtendedMoniker::try_from("component2").unwrap();
342        allowlist.add_component(component_moniker.clone()).unwrap();
343
344        let list = allowlist.get(&component_moniker);
345        assert!(matcher_found(list.matcher("foo")));
346        assert!(matcher_found(list.matcher("bar")));
347        assert!(matcher_found(list.matcher("qux")));
348        assert!(matcher_not_found(list.matcher("no 'all' for 2")));
349    }
350}