routing/
policy.rs

1// Copyright 2020 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::capability_source::{
6    AnonymizedAggregateSource, BuiltinSource, CapabilitySource, CapabilityToCapabilitySource,
7    ComponentSource, EnvironmentSource, FilteredAggregateProviderSource, FilteredProviderSource,
8    FrameworkSource, NamespaceSource, VoidSource,
9};
10use cm_config::{
11    AllowlistEntry, AllowlistMatcher, CapabilityAllowlistKey, CapabilityAllowlistSource,
12    DebugCapabilityKey, SecurityPolicy,
13};
14use log::{error, warn};
15use moniker::{ExtendedMoniker, Moniker};
16use std::sync::Arc;
17use thiserror::Error;
18use zx_status as zx;
19
20use cm_rust::CapabilityTypeName;
21#[cfg(feature = "serde")]
22use serde::{Deserialize, Serialize};
23
24/// Errors returned by the PolicyChecker and the ScopedPolicyChecker.
25#[cfg_attr(feature = "serde", derive(Deserialize, Serialize), serde(rename_all = "snake_case"))]
26#[derive(Debug, Clone, Error, PartialEq)]
27pub enum PolicyError {
28    #[error("security policy disallows \"{policy}\" job policy for \"{moniker}\"")]
29    JobPolicyDisallowed { policy: String, moniker: Moniker },
30
31    #[error("security policy disallows \"{policy}\" child policy for \"{moniker}\"")]
32    ChildPolicyDisallowed { policy: String, moniker: Moniker },
33
34    #[error("security policy was unable to extract the source from the routed capability at component \"{moniker}\"")]
35    InvalidCapabilitySource { moniker: ExtendedMoniker },
36
37    #[error("security policy disallows \"{cap}\" from \"{source_moniker}\" being used at \"{target_moniker}\"")]
38    CapabilityUseDisallowed {
39        cap: String,
40        source_moniker: ExtendedMoniker,
41        target_moniker: Moniker,
42    },
43
44    #[error(
45        "debug security policy disallows \"{cap}\" from being registered in \
46        environment \"{env_name}\" at \"{env_moniker}\""
47    )]
48    DebugCapabilityUseDisallowed { cap: String, env_moniker: Moniker, env_name: String },
49}
50
51impl PolicyError {
52    /// Convert this error into its approximate `zx::Status` equivalent.
53    pub fn as_zx_status(&self) -> zx::Status {
54        zx::Status::ACCESS_DENIED
55    }
56}
57
58impl From<PolicyError> for ExtendedMoniker {
59    fn from(err: PolicyError) -> ExtendedMoniker {
60        match err {
61            PolicyError::ChildPolicyDisallowed { moniker, .. }
62            | PolicyError::DebugCapabilityUseDisallowed { env_moniker: moniker, .. }
63            | PolicyError::JobPolicyDisallowed { moniker, .. } => moniker.into(),
64
65            PolicyError::CapabilityUseDisallowed { source_moniker: moniker, .. }
66            | PolicyError::InvalidCapabilitySource { moniker } => moniker,
67        }
68    }
69}
70
71/// Evaluates security policy globally across the entire Model and all components.
72/// This is used to enforce runtime capability routing restrictions across all
73/// components to prevent high privilleged capabilities from being routed to
74/// components outside of the list defined in the runtime security policy.
75#[derive(Clone, Debug, Default)]
76pub struct GlobalPolicyChecker {
77    /// The security policy to apply.
78    policy: Arc<SecurityPolicy>,
79}
80
81impl GlobalPolicyChecker {
82    /// Constructs a new PolicyChecker object configured by the SecurityPolicy.
83    pub fn new(policy: Arc<SecurityPolicy>) -> Self {
84        Self { policy }
85    }
86
87    fn get_policy_key(
88        capability_source: &CapabilitySource,
89    ) -> Result<CapabilityAllowlistKey, PolicyError> {
90        Ok(match &capability_source {
91            CapabilitySource::Namespace(NamespaceSource { capability, .. }) => {
92                CapabilityAllowlistKey {
93                    source_moniker: ExtendedMoniker::ComponentManager,
94                    source_name: capability
95                        .source_name()
96                        .ok_or(PolicyError::InvalidCapabilitySource {
97                            moniker: capability_source.source_moniker(),
98                        })?
99                        .clone(),
100                    source: CapabilityAllowlistSource::Self_,
101                    capability: capability.type_name(),
102                }
103            }
104            CapabilitySource::Component(ComponentSource { capability, moniker }) => {
105                CapabilityAllowlistKey {
106                    source_moniker: ExtendedMoniker::ComponentInstance(moniker.clone()),
107                    source_name: capability
108                        .source_name()
109                        .ok_or(PolicyError::InvalidCapabilitySource {
110                            moniker: capability_source.source_moniker(),
111                        })?
112                        .clone(),
113                    source: CapabilityAllowlistSource::Self_,
114                    capability: capability.type_name(),
115                }
116            }
117            CapabilitySource::Builtin(BuiltinSource { capability, .. }) => CapabilityAllowlistKey {
118                source_moniker: ExtendedMoniker::ComponentManager,
119                source_name: capability.source_name().clone(),
120                source: CapabilityAllowlistSource::Self_,
121                capability: capability.type_name(),
122            },
123            CapabilitySource::Framework(FrameworkSource { capability, moniker }) => {
124                CapabilityAllowlistKey {
125                    source_moniker: ExtendedMoniker::ComponentInstance(moniker.clone()),
126                    source_name: capability.source_name().clone(),
127                    source: CapabilityAllowlistSource::Framework,
128                    capability: capability.type_name(),
129                }
130            }
131            CapabilitySource::Void(VoidSource { capability, moniker }) => CapabilityAllowlistKey {
132                source_moniker: ExtendedMoniker::ComponentInstance(moniker.clone()),
133                source_name: capability.source_name().clone(),
134                source: CapabilityAllowlistSource::Void,
135                capability: capability.type_name(),
136            },
137            CapabilitySource::Capability(CapabilityToCapabilitySource {
138                source_capability,
139                moniker,
140            }) => CapabilityAllowlistKey {
141                source_moniker: ExtendedMoniker::ComponentInstance(moniker.clone()),
142                source_name: source_capability
143                    .source_name()
144                    .ok_or(PolicyError::InvalidCapabilitySource {
145                        moniker: capability_source.source_moniker(),
146                    })?
147                    .clone(),
148                source: CapabilityAllowlistSource::Capability,
149                capability: source_capability.type_name(),
150            },
151            CapabilitySource::AnonymizedAggregate(AnonymizedAggregateSource {
152                capability,
153                moniker,
154                ..
155            })
156            | CapabilitySource::FilteredProvider(FilteredProviderSource {
157                capability,
158                moniker,
159                ..
160            })
161            | CapabilitySource::FilteredAggregateProvider(FilteredAggregateProviderSource {
162                capability,
163                moniker,
164                ..
165            }) => CapabilityAllowlistKey {
166                source_moniker: ExtendedMoniker::ComponentInstance(moniker.clone()),
167                source_name: capability.source_name().clone(),
168                source: CapabilityAllowlistSource::Self_,
169                capability: capability.type_name(),
170            },
171            CapabilitySource::Environment(EnvironmentSource { capability, .. }) => {
172                CapabilityAllowlistKey {
173                    source_moniker: ExtendedMoniker::ComponentManager,
174                    source_name: capability
175                        .source_name()
176                        .ok_or(PolicyError::InvalidCapabilitySource {
177                            moniker: capability_source.source_moniker(),
178                        })?
179                        .clone(),
180                    source: CapabilityAllowlistSource::Environment,
181                    capability: capability.type_name(),
182                }
183            }
184        })
185    }
186
187    /// Returns Ok(()) if the provided capability source can be routed to the
188    /// given target_moniker, else a descriptive PolicyError.
189    pub fn can_route_capability<'a>(
190        &self,
191        capability_source: &'a CapabilitySource,
192        target_moniker: &'a Moniker,
193    ) -> Result<(), PolicyError> {
194        let policy_key = Self::get_policy_key(capability_source).map_err(|e| {
195            error!("Security policy could not generate a policy key for `{}`", capability_source);
196            e
197        })?;
198
199        match self.policy.capability_policy.get(&policy_key) {
200            Some(entries) => {
201                let parts = target_moniker
202                    .path()
203                    .iter()
204                    .map(|c| AllowlistMatcher::Exact(c.clone()))
205                    .collect();
206                let entry = AllowlistEntry { matchers: parts };
207
208                // Use the HashSet to find any exact matches quickly.
209                if entries.contains(&entry) {
210                    return Ok(());
211                }
212
213                // Otherwise linear search for any non-exact matches.
214                if entries.iter().any(|entry| entry.matches(&target_moniker)) {
215                    Ok(())
216                } else {
217                    warn!(
218                        "Security policy prevented `{}` from `{}` being routed to `{}`.",
219                        policy_key.source_name, policy_key.source_moniker, target_moniker
220                    );
221                    Err(PolicyError::CapabilityUseDisallowed {
222                        cap: policy_key.source_name.to_string(),
223                        source_moniker: policy_key.source_moniker.to_owned(),
224                        target_moniker: target_moniker.to_owned(),
225                    })
226                }
227            }
228            None => Ok(()),
229        }
230    }
231
232    /// Returns Ok(()) if the provided debug capability source is allowed to be routed from given
233    /// environment.
234    pub fn can_register_debug_capability<'a>(
235        &self,
236        capability_type: CapabilityTypeName,
237        name: &'a cm_types::Name,
238        env_moniker: &'a Moniker,
239        env_name: &'a cm_types::Name,
240    ) -> Result<(), PolicyError> {
241        let debug_key = DebugCapabilityKey {
242            name: name.clone(),
243            source: CapabilityAllowlistSource::Self_,
244            capability: capability_type,
245            env_name: env_name.clone(),
246        };
247        let route_allowed = match self.policy.debug_capability_policy.get(&debug_key) {
248            None => false,
249            Some(allowlist_set) => allowlist_set.iter().any(|entry| entry.matches(env_moniker)),
250        };
251        if route_allowed {
252            return Ok(());
253        }
254
255        warn!(
256            "Debug security policy prevented `{}` from being registered to environment `{}` in `{}`.",
257            debug_key.name, env_name, env_moniker,
258        );
259        Err(PolicyError::DebugCapabilityUseDisallowed {
260            cap: debug_key.name.to_string(),
261            env_moniker: env_moniker.to_owned(),
262            env_name: env_name.to_string(),
263        })
264    }
265
266    /// Returns Ok(()) if `target_moniker` is allowed to have `on_terminate=REBOOT` set.
267    pub fn reboot_on_terminate_allowed(&self, target_moniker: &Moniker) -> Result<(), PolicyError> {
268        self.policy
269            .child_policy
270            .reboot_on_terminate
271            .iter()
272            .any(|entry| entry.matches(&target_moniker))
273            .then_some(())
274            .ok_or_else(|| PolicyError::ChildPolicyDisallowed {
275                policy: "reboot_on_terminate".to_owned(),
276                moniker: target_moniker.to_owned(),
277            })
278    }
279}
280
281/// Evaluates security policy relative to a specific Component (based on that Component's
282/// Moniker).
283#[derive(Clone)]
284pub struct ScopedPolicyChecker {
285    /// The security policy to apply.
286    policy: Arc<SecurityPolicy>,
287
288    /// The moniker of the component that policy will be evaluated for.
289    pub scope: Moniker,
290}
291
292impl ScopedPolicyChecker {
293    pub fn new(policy: Arc<SecurityPolicy>, scope: Moniker) -> Self {
294        ScopedPolicyChecker { policy, scope }
295    }
296
297    // This interface is super simple for now since there's only three allowlists. In the future
298    // we'll probably want a different interface than an individual function per policy item.
299
300    pub fn ambient_mark_vmo_exec_allowed(&self) -> Result<(), PolicyError> {
301        self.policy
302            .job_policy
303            .ambient_mark_vmo_exec
304            .iter()
305            .any(|entry| entry.matches(&self.scope))
306            .then_some(())
307            .ok_or_else(|| PolicyError::JobPolicyDisallowed {
308                policy: "ambient_mark_vmo_exec".to_owned(),
309                moniker: self.scope.to_owned(),
310            })
311    }
312
313    pub fn main_process_critical_allowed(&self) -> Result<(), PolicyError> {
314        self.policy
315            .job_policy
316            .main_process_critical
317            .iter()
318            .any(|entry| entry.matches(&self.scope))
319            .then_some(())
320            .ok_or_else(|| PolicyError::JobPolicyDisallowed {
321                policy: "main_process_critical".to_owned(),
322                moniker: self.scope.to_owned(),
323            })
324    }
325
326    pub fn create_raw_processes_allowed(&self) -> Result<(), PolicyError> {
327        self.policy
328            .job_policy
329            .create_raw_processes
330            .iter()
331            .any(|entry| entry.matches(&self.scope))
332            .then_some(())
333            .ok_or_else(|| PolicyError::JobPolicyDisallowed {
334                policy: "create_raw_processes".to_owned(),
335                moniker: self.scope.to_owned(),
336            })
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343    use assert_matches::assert_matches;
344    use cm_config::{AllowlistEntryBuilder, ChildPolicyAllowlists, JobPolicyAllowlists};
345    use moniker::ChildName;
346    use std::collections::HashMap;
347
348    #[test]
349    fn scoped_policy_checker_vmex() {
350        macro_rules! assert_vmex_allowed_matches {
351            ($policy:expr, $moniker:expr, $expected:pat) => {
352                let result = ScopedPolicyChecker::new($policy.clone(), $moniker.clone())
353                    .ambient_mark_vmo_exec_allowed();
354                assert_matches!(result, $expected);
355            };
356        }
357        macro_rules! assert_vmex_disallowed {
358            ($policy:expr, $moniker:expr) => {
359                assert_vmex_allowed_matches!(
360                    $policy,
361                    $moniker,
362                    Err(PolicyError::JobPolicyDisallowed { .. })
363                );
364            };
365        }
366        let policy = Arc::new(SecurityPolicy::default());
367        assert_vmex_disallowed!(policy, Moniker::root());
368        assert_vmex_disallowed!(policy, Moniker::try_from(["foo"]).unwrap());
369
370        let allowed1 = Moniker::try_from(["foo", "bar"]).unwrap();
371        let allowed2 = Moniker::try_from(["baz", "fiz"]).unwrap();
372        let policy = Arc::new(SecurityPolicy {
373            job_policy: JobPolicyAllowlists {
374                ambient_mark_vmo_exec: vec![
375                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
376                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
377                ],
378                main_process_critical: vec![
379                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
380                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
381                ],
382                create_raw_processes: vec![
383                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
384                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
385                ],
386            },
387            capability_policy: HashMap::new(),
388            debug_capability_policy: HashMap::new(),
389            child_policy: ChildPolicyAllowlists {
390                reboot_on_terminate: vec![
391                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
392                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
393                ],
394            },
395        });
396        assert_vmex_allowed_matches!(policy, allowed1, Ok(()));
397        assert_vmex_allowed_matches!(policy, allowed2, Ok(()));
398        assert_vmex_disallowed!(policy, Moniker::root());
399        assert_vmex_disallowed!(policy, allowed1.parent().unwrap());
400        assert_vmex_disallowed!(policy, allowed1.child(ChildName::try_from("baz").unwrap()));
401    }
402
403    #[test]
404    fn scoped_policy_checker_create_raw_processes() {
405        macro_rules! assert_create_raw_processes_allowed_matches {
406            ($policy:expr, $moniker:expr, $expected:pat) => {
407                let result = ScopedPolicyChecker::new($policy.clone(), $moniker.clone())
408                    .create_raw_processes_allowed();
409                assert_matches!(result, $expected);
410            };
411        }
412        macro_rules! assert_create_raw_processes_disallowed {
413            ($policy:expr, $moniker:expr) => {
414                assert_create_raw_processes_allowed_matches!(
415                    $policy,
416                    $moniker,
417                    Err(PolicyError::JobPolicyDisallowed { .. })
418                );
419            };
420        }
421        let policy = Arc::new(SecurityPolicy::default());
422        assert_create_raw_processes_disallowed!(policy, Moniker::root());
423        assert_create_raw_processes_disallowed!(policy, Moniker::try_from(["foo"]).unwrap());
424
425        let allowed1 = Moniker::try_from(["foo", "bar"]).unwrap();
426        let allowed2 = Moniker::try_from(["baz", "fiz"]).unwrap();
427        let policy = Arc::new(SecurityPolicy {
428            job_policy: JobPolicyAllowlists {
429                ambient_mark_vmo_exec: vec![],
430                main_process_critical: vec![],
431                create_raw_processes: vec![
432                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
433                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
434                ],
435            },
436            capability_policy: HashMap::new(),
437            debug_capability_policy: HashMap::new(),
438            child_policy: ChildPolicyAllowlists { reboot_on_terminate: vec![] },
439        });
440        assert_create_raw_processes_allowed_matches!(policy, allowed1, Ok(()));
441        assert_create_raw_processes_allowed_matches!(policy, allowed2, Ok(()));
442        assert_create_raw_processes_disallowed!(policy, Moniker::root());
443        assert_create_raw_processes_disallowed!(policy, allowed1.parent().unwrap());
444        assert_create_raw_processes_disallowed!(
445            policy,
446            allowed1.child(ChildName::try_from("baz").unwrap())
447        );
448    }
449
450    #[test]
451    fn scoped_policy_checker_main_process_critical_allowed() {
452        macro_rules! assert_critical_allowed_matches {
453            ($policy:expr, $moniker:expr, $expected:pat) => {
454                let result = ScopedPolicyChecker::new($policy.clone(), $moniker.clone())
455                    .main_process_critical_allowed();
456                assert_matches!(result, $expected);
457            };
458        }
459        macro_rules! assert_critical_disallowed {
460            ($policy:expr, $moniker:expr) => {
461                assert_critical_allowed_matches!(
462                    $policy,
463                    $moniker,
464                    Err(PolicyError::JobPolicyDisallowed { .. })
465                );
466            };
467        }
468        let policy = Arc::new(SecurityPolicy::default());
469        assert_critical_disallowed!(policy, Moniker::root());
470        assert_critical_disallowed!(policy, Moniker::try_from(["foo"]).unwrap());
471
472        let allowed1 = Moniker::try_from(["foo", "bar"]).unwrap();
473        let allowed2 = Moniker::try_from(["baz", "fiz"]).unwrap();
474        let policy = Arc::new(SecurityPolicy {
475            job_policy: JobPolicyAllowlists {
476                ambient_mark_vmo_exec: vec![
477                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
478                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
479                ],
480                main_process_critical: vec![
481                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
482                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
483                ],
484                create_raw_processes: vec![
485                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
486                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
487                ],
488            },
489            capability_policy: HashMap::new(),
490            debug_capability_policy: HashMap::new(),
491            child_policy: ChildPolicyAllowlists { reboot_on_terminate: vec![] },
492        });
493        assert_critical_allowed_matches!(policy, allowed1, Ok(()));
494        assert_critical_allowed_matches!(policy, allowed2, Ok(()));
495        assert_critical_disallowed!(policy, Moniker::root());
496        assert_critical_disallowed!(policy, allowed1.parent().unwrap());
497        assert_critical_disallowed!(policy, allowed1.child(ChildName::try_from("baz").unwrap()));
498    }
499
500    #[test]
501    fn scoped_policy_checker_reboot_policy_allowed() {
502        macro_rules! assert_reboot_allowed_matches {
503            ($policy:expr, $moniker:expr, $expected:pat) => {
504                let result = GlobalPolicyChecker::new($policy.clone())
505                    .reboot_on_terminate_allowed(&$moniker);
506                assert_matches!(result, $expected);
507            };
508        }
509        macro_rules! assert_reboot_disallowed {
510            ($policy:expr, $moniker:expr) => {
511                assert_reboot_allowed_matches!(
512                    $policy,
513                    $moniker,
514                    Err(PolicyError::ChildPolicyDisallowed { .. })
515                );
516            };
517        }
518
519        // Empty policy and enabled.
520        let policy = Arc::new(SecurityPolicy::default());
521        assert_reboot_disallowed!(policy, Moniker::root());
522        assert_reboot_disallowed!(policy, Moniker::try_from(["foo"]).unwrap());
523
524        // Nonempty policy.
525        let allowed1 = Moniker::try_from(["foo", "bar"]).unwrap();
526        let allowed2 = Moniker::try_from(["baz", "fiz"]).unwrap();
527        let policy = Arc::new(SecurityPolicy {
528            job_policy: JobPolicyAllowlists {
529                ambient_mark_vmo_exec: vec![],
530                main_process_critical: vec![],
531                create_raw_processes: vec![],
532            },
533            capability_policy: HashMap::new(),
534            debug_capability_policy: HashMap::new(),
535            child_policy: ChildPolicyAllowlists {
536                reboot_on_terminate: vec![
537                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed1),
538                    AllowlistEntryBuilder::build_exact_from_moniker(&allowed2),
539                ],
540            },
541        });
542        assert_reboot_allowed_matches!(policy, allowed1, Ok(()));
543        assert_reboot_allowed_matches!(policy, allowed2, Ok(()));
544        assert_reboot_disallowed!(policy, Moniker::root());
545        assert_reboot_disallowed!(policy, allowed1.parent().unwrap());
546        assert_reboot_disallowed!(policy, allowed1.child(ChildName::try_from("baz").unwrap()));
547    }
548}