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