| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use { |
| argh::FromArgs, |
| cm_types::{symmetrical_enums, Url}, |
| cml::{ |
| error::{Error, Location}, |
| translate::CompileOptions, |
| }, |
| fidl::persist, |
| fidl_fuchsia_component_decl as fdecl, fidl_fuchsia_component_internal as component_internal, |
| itertools::Itertools, |
| serde::Deserialize, |
| std::{ |
| collections::HashSet, |
| fs::{self, File}, |
| io::Write, |
| path::PathBuf, |
| }, |
| }; |
| |
| fn remove_duplicates<T: Ord>(mut v: Vec<T>) -> Vec<T> { |
| v.sort(); |
| v.dedup(); |
| v |
| } |
| |
| fn merge_option<T, F>(left: Option<T>, right: Option<T>, merge_fn: F) -> Result<Option<T>, Error> |
| where |
| F: FnOnce(T, T) -> Result<T, Error>, |
| { |
| match (left, right) { |
| (left @ Some(_), None) => Ok(left), |
| (None, right @ Some(_)) => Ok(right), |
| (Some(l), Some(r)) => Ok(Some(merge_fn(l, r)?)), |
| (None, None) => Ok(None), |
| } |
| } |
| |
| macro_rules! deep_merge_field { |
| ($target:ident, $other:expr, $field:ident) => { |
| merge_option($target.$field, $other.$field, |l, r| l.merge(r))? |
| }; |
| } |
| |
| macro_rules! merge_field { |
| ($target:ident, $other:expr, $field:ident) => { |
| merge_option($target.$field, $other.$field, |_, _| { |
| Err(Error::parse( |
| format!("Conflicting field found: {:?}", stringify!($field)), |
| None, |
| None, |
| )) |
| })? |
| }; |
| } |
| |
| macro_rules! merge_vec { |
| ($target:ident, $other:expr, $field:ident) => { |
| merge_option($target.$field, $other.$field, |mut l, mut r| { |
| l.append(&mut r); |
| Ok(remove_duplicates(l)) |
| })? |
| }; |
| } |
| |
| const SESSION_MONIKER: &str = "/core/session-manager/session:session"; |
| |
| /// We use types for the platform capabilities so that the product owners get a nice error |
| /// indicating their options if they choose an invalid capability. |
| #[derive(Deserialize, Debug, Eq, PartialEq, Hash, Clone)] |
| enum PlatformCapability { |
| #[serde(rename = "fuchsia.lowpan.device.DeviceExtraConnector")] |
| Lowpan, |
| #[serde(rename = "fuchsia.kernel.VmexResource")] |
| VmexResource, |
| } |
| |
| impl PlatformCapability { |
| /// Convert the capability back to a string. |
| fn source_name(&self) -> &str { |
| match self { |
| &PlatformCapability::Lowpan => "fuchsia.lowpan.device.DeviceExtraConnector", |
| &PlatformCapability::VmexResource => "fuchsia.kernel.VmexResource", |
| } |
| } |
| |
| /// Map the capability to the moniker of the source. |
| fn source_moniker(&self) -> &str { |
| match self { |
| &PlatformCapability::Lowpan => "/core/lowpanservice", |
| &PlatformCapability::VmexResource => "<component_manager>", |
| } |
| } |
| } |
| |
| #[derive(Deserialize, Debug, Default)] |
| #[serde(deny_unknown_fields)] |
| struct Config { |
| debug: Option<bool>, |
| list_children_batch_size: Option<u32>, |
| security_policy: Option<SecurityPolicy>, |
| namespace_capabilities: Option<Vec<cml::Capability>>, |
| builtin_capabilities: Option<Vec<cml::Capability>>, |
| use_builtin_process_launcher: Option<bool>, |
| maintain_utc_clock: Option<bool>, |
| num_threads: Option<u32>, |
| root_component_url: Option<Url>, |
| component_id_index_path: Option<String>, |
| log_destination: Option<LogDestination>, |
| log_all_events: Option<bool>, |
| builtin_boot_resolver: Option<BuiltinBootResolver>, |
| realm_builder_resolver_and_runner: Option<RealmBuilderResolverAndRunner>, |
| enable_introspection: Option<bool>, |
| abi_revision_policy: Option<AbiRevisionPolicy>, |
| vmex_source: Option<VmexSource>, |
| } |
| |
| #[derive(Deserialize, Debug)] |
| #[serde(rename_all = "snake_case")] |
| enum LogDestination { |
| Syslog, |
| Klog, |
| } |
| |
| symmetrical_enums!(LogDestination, component_internal::LogDestination, Syslog, Klog); |
| |
| #[derive(Deserialize, Debug)] |
| #[serde(rename_all = "snake_case")] |
| enum BuiltinBootResolver { |
| None, |
| Boot, |
| } |
| |
| symmetrical_enums!(BuiltinBootResolver, component_internal::BuiltinBootResolver, None, Boot); |
| |
| #[derive(Deserialize, Debug)] |
| #[serde(rename_all = "snake_case")] |
| enum RealmBuilderResolverAndRunner { |
| None, |
| Namespace, |
| } |
| |
| symmetrical_enums!( |
| RealmBuilderResolverAndRunner, |
| component_internal::RealmBuilderResolverAndRunner, |
| None, |
| Namespace |
| ); |
| |
| #[derive(Deserialize, Debug)] |
| #[serde(rename_all = "snake_case")] |
| pub enum AbiRevisionPolicy { |
| AllowAll, |
| EnforcePresenceOnly, |
| EnforcePresenceAndCompatibility, |
| } |
| |
| symmetrical_enums!( |
| AbiRevisionPolicy, |
| component_internal::AbiRevisionPolicy, |
| AllowAll, |
| EnforcePresenceOnly, |
| EnforcePresenceAndCompatibility |
| ); |
| |
| #[derive(Deserialize, Debug)] |
| #[serde(rename_all = "snake_case")] |
| pub enum VmexSource { |
| SystemResource, |
| Namespace, |
| } |
| |
| symmetrical_enums!(VmexSource, component_internal::VmexSource, SystemResource, Namespace); |
| |
| #[derive(Deserialize, Debug, Default)] |
| #[serde(deny_unknown_fields)] |
| pub struct SecurityPolicy { |
| job_policy: Option<JobPolicyAllowlists>, |
| capability_policy: Option<Vec<CapabilityAllowlistEntry>>, |
| debug_registration_policy: Option<Vec<DebugRegistrationAllowlistEntry>>, |
| child_policy: Option<ChildPolicyAllowlists>, |
| } |
| |
| impl SecurityPolicy { |
| fn merge(self, another: SecurityPolicy) -> Result<Self, Error> { |
| return Ok(SecurityPolicy { |
| job_policy: deep_merge_field!(self, another, job_policy), |
| capability_policy: merge_option( |
| self.capability_policy, |
| another.capability_policy, |
| CapabilityAllowlistEntry::merge_vecs, |
| )?, |
| debug_registration_policy: merge_field!(self, another, debug_registration_policy), |
| child_policy: deep_merge_field!(self, another, child_policy), |
| }); |
| } |
| } |
| |
| #[derive(Deserialize, Debug, Default)] |
| #[serde(deny_unknown_fields)] |
| pub struct JobPolicyAllowlists { |
| ambient_mark_vmo_exec: Option<Vec<String>>, |
| main_process_critical: Option<Vec<String>>, |
| create_raw_processes: Option<Vec<String>>, |
| } |
| |
| impl JobPolicyAllowlists { |
| fn merge(self, another: JobPolicyAllowlists) -> Result<Self, Error> { |
| return Ok(JobPolicyAllowlists { |
| ambient_mark_vmo_exec: merge_vec!(self, another, ambient_mark_vmo_exec), |
| main_process_critical: merge_vec!(self, another, main_process_critical), |
| create_raw_processes: merge_vec!(self, another, create_raw_processes), |
| }); |
| } |
| } |
| |
| #[derive(Deserialize, Debug, Default)] |
| #[serde(deny_unknown_fields)] |
| pub struct ChildPolicyAllowlists { |
| reboot_on_terminate: Option<Vec<String>>, |
| } |
| |
| impl ChildPolicyAllowlists { |
| fn merge(self, another: ChildPolicyAllowlists) -> Result<Self, Error> { |
| return Ok(ChildPolicyAllowlists { |
| reboot_on_terminate: merge_vec!(self, another, reboot_on_terminate), |
| }); |
| } |
| } |
| |
| #[derive(Deserialize, Debug, Clone, PartialEq, Hash, Eq, Ord, PartialOrd)] |
| #[serde(rename_all = "lowercase")] |
| pub enum CapabilityTypeName { |
| Directory, |
| Protocol, |
| Service, |
| Storage, |
| Runner, |
| Resolver, |
| } |
| |
| impl std::fmt::Display for CapabilityTypeName { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| let display_name = match &self { |
| CapabilityTypeName::Directory => "directory", |
| CapabilityTypeName::Protocol => "protocol", |
| CapabilityTypeName::Service => "service", |
| CapabilityTypeName::Storage => "storage", |
| CapabilityTypeName::Runner => "runner", |
| CapabilityTypeName::Resolver => "resolver", |
| }; |
| write!(f, "{}", display_name) |
| } |
| } |
| |
| impl Into<component_internal::AllowlistedCapability> for CapabilityTypeName { |
| fn into(self) -> component_internal::AllowlistedCapability { |
| match &self { |
| CapabilityTypeName::Directory => component_internal::AllowlistedCapability::Directory( |
| component_internal::AllowlistedDirectory::default(), |
| ), |
| CapabilityTypeName::Protocol => component_internal::AllowlistedCapability::Protocol( |
| component_internal::AllowlistedProtocol::default(), |
| ), |
| CapabilityTypeName::Service => component_internal::AllowlistedCapability::Service( |
| component_internal::AllowlistedService::default(), |
| ), |
| CapabilityTypeName::Storage => component_internal::AllowlistedCapability::Storage( |
| component_internal::AllowlistedStorage::default(), |
| ), |
| CapabilityTypeName::Runner => component_internal::AllowlistedCapability::Runner( |
| component_internal::AllowlistedRunner::default(), |
| ), |
| CapabilityTypeName::Resolver => component_internal::AllowlistedCapability::Resolver( |
| component_internal::AllowlistedResolver::default(), |
| ), |
| } |
| } |
| } |
| |
| #[derive(Deserialize, Debug, Clone)] |
| #[serde(rename_all = "lowercase")] |
| pub enum DebugRegistrationTypeName { |
| Protocol, |
| } |
| |
| impl Into<component_internal::AllowlistedDebugRegistration> for DebugRegistrationTypeName { |
| fn into(self) -> component_internal::AllowlistedDebugRegistration { |
| match &self { |
| DebugRegistrationTypeName::Protocol => { |
| component_internal::AllowlistedDebugRegistration::Protocol( |
| component_internal::AllowlistedProtocol::default(), |
| ) |
| } |
| } |
| } |
| } |
| |
| #[derive(Deserialize, Debug, Clone, PartialEq, Hash, Eq, Ord, PartialOrd)] |
| #[serde(rename_all = "lowercase")] |
| pub enum CapabilityFrom { |
| Capability, |
| Component, |
| Framework, |
| } |
| |
| impl Into<fdecl::Ref> for CapabilityFrom { |
| fn into(self) -> fdecl::Ref { |
| match &self { |
| CapabilityFrom::Capability => { |
| fdecl::Ref::Capability(fdecl::CapabilityRef { name: "".into() }) |
| } |
| CapabilityFrom::Component => fdecl::Ref::Self_(fdecl::SelfRef {}), |
| CapabilityFrom::Framework => fdecl::Ref::Framework(fdecl::FrameworkRef {}), |
| } |
| } |
| } |
| |
| #[derive(Deserialize, Debug, Default, PartialEq, Clone, Ord, PartialOrd, Eq)] |
| #[serde(deny_unknown_fields)] |
| pub struct CapabilityAllowlistEntry { |
| source_moniker: Option<String>, |
| source_name: Option<String>, |
| source: Option<CapabilityFrom>, |
| capability: Option<CapabilityTypeName>, |
| target_monikers: Option<Vec<String>>, |
| } |
| |
| impl CapabilityAllowlistEntry { |
| fn merge_vecs( |
| some: Vec<CapabilityAllowlistEntry>, |
| another: Vec<CapabilityAllowlistEntry>, |
| ) -> Result<Vec<CapabilityAllowlistEntry>, Error> { |
| #[derive(Hash, Eq, PartialEq)] |
| struct Source { |
| source_moniker: Option<String>, |
| source_name: Option<String>, |
| source: Option<CapabilityFrom>, |
| capability: Option<CapabilityTypeName>, |
| } |
| let mut combined: Vec<_> = some |
| .into_iter() |
| .chain(another.into_iter()) |
| .map(|mut entry| { |
| ( |
| Source { |
| source_moniker: entry.source_moniker.take(), |
| source_name: entry.source_name.take(), |
| source: entry.source.take(), |
| capability: entry.capability.take(), |
| }, |
| entry.target_monikers, |
| ) |
| }) |
| .into_grouping_map() |
| .fold_first(|accumulated, _key, item| { |
| { |
| merge_option(accumulated, item, |mut l, mut r| { |
| l.append(&mut r); |
| Ok(l) |
| }) |
| } |
| .unwrap() |
| }) |
| .into_iter() |
| .map(|(mut key, values)| CapabilityAllowlistEntry { |
| source_moniker: key.source_moniker.take(), |
| source_name: key.source_name.take(), |
| source: key.source.take(), |
| capability: key.capability.take(), |
| target_monikers: match values { |
| Some(v) => Some(remove_duplicates(v)), |
| None => None, |
| }, |
| }) |
| .collect(); |
| // Unstable sorts are faster, and we shouldn't have items that have equal values anyway. |
| combined.sort_unstable(); |
| Ok(combined) |
| } |
| } |
| |
| #[derive(Deserialize, Debug, Default)] |
| #[serde(deny_unknown_fields)] |
| pub struct DebugRegistrationAllowlistEntry { |
| name: Option<String>, |
| debug: Option<DebugRegistrationTypeName>, |
| moniker: Option<String>, |
| environment_name: Option<String>, |
| } |
| |
| impl TryFrom<Config> for component_internal::Config { |
| type Error = Error; |
| |
| fn try_from(config: Config) -> Result<Self, Error> { |
| // Validate "namespace_capabilities". |
| if let Some(capabilities) = config.namespace_capabilities.as_ref() { |
| let mut used_ids = HashSet::new(); |
| for capability in capabilities { |
| Config::validate_namespace_capability(capability, &mut used_ids)?; |
| } |
| } |
| // Validate "builtin_capabilities". |
| if let Some(capabilities) = config.builtin_capabilities.as_ref() { |
| let mut used_ids = HashSet::new(); |
| for capability in capabilities { |
| Config::validate_builtin_capability(capability, &mut used_ids)?; |
| } |
| } |
| |
| Ok(Self { |
| debug: config.debug, |
| enable_introspection: config.enable_introspection, |
| use_builtin_process_launcher: config.use_builtin_process_launcher, |
| maintain_utc_clock: config.maintain_utc_clock, |
| list_children_batch_size: config.list_children_batch_size, |
| security_policy: Some(translate_security_policy(config.security_policy)), |
| namespace_capabilities: config |
| .namespace_capabilities |
| .as_ref() |
| .map(|c| { |
| cml::translate::translate_capabilities(&CompileOptions::default(), c, false) |
| }) |
| .transpose()?, |
| builtin_capabilities: config |
| .builtin_capabilities |
| .as_ref() |
| .map(|c| { |
| cml::translate::translate_capabilities(&CompileOptions::default(), c, true) |
| }) |
| .transpose()?, |
| num_threads: config.num_threads, |
| root_component_url: match config.root_component_url { |
| Some(root_component_url) => Some(root_component_url.as_str().to_string()), |
| None => None, |
| }, |
| component_id_index_path: config.component_id_index_path, |
| log_destination: config.log_destination.map(|d| d.into()), |
| log_all_events: config.log_all_events, |
| builtin_boot_resolver: config.builtin_boot_resolver.map(Into::into), |
| realm_builder_resolver_and_runner: config |
| .realm_builder_resolver_and_runner |
| .map(Into::into), |
| abi_revision_policy: config.abi_revision_policy.map(Into::into), |
| vmex_source: config.vmex_source.map(Into::into), |
| ..Default::default() |
| }) |
| } |
| } |
| |
| // TODO: Instead of returning a "default" security_policy when it's not specified in JSON, |
| // could we return None instead? |
| fn translate_security_policy( |
| security_policy: Option<SecurityPolicy>, |
| ) -> component_internal::SecurityPolicy { |
| let SecurityPolicy { job_policy, capability_policy, debug_registration_policy, child_policy } = |
| security_policy.unwrap_or_default(); |
| component_internal::SecurityPolicy { |
| job_policy: job_policy.map(translate_job_policy), |
| capability_policy: capability_policy.map(translate_capability_policy), |
| debug_registration_policy: debug_registration_policy |
| .map(translate_debug_registration_policy), |
| child_policy: child_policy.map(translate_child_policy), |
| ..Default::default() |
| } |
| } |
| |
| fn translate_job_policy( |
| job_policy: JobPolicyAllowlists, |
| ) -> component_internal::JobPolicyAllowlists { |
| component_internal::JobPolicyAllowlists { |
| ambient_mark_vmo_exec: job_policy.ambient_mark_vmo_exec, |
| main_process_critical: job_policy.main_process_critical, |
| create_raw_processes: job_policy.create_raw_processes, |
| ..Default::default() |
| } |
| } |
| |
| fn translate_child_policy( |
| child_policy: ChildPolicyAllowlists, |
| ) -> component_internal::ChildPolicyAllowlists { |
| component_internal::ChildPolicyAllowlists { |
| reboot_on_terminate: child_policy.reboot_on_terminate, |
| ..Default::default() |
| } |
| } |
| |
| fn translate_capability_policy( |
| capability_policy: Vec<CapabilityAllowlistEntry>, |
| ) -> component_internal::CapabilityPolicyAllowlists { |
| let allowlist = capability_policy |
| .iter() |
| .map(|e| component_internal::CapabilityAllowlistEntry { |
| source_moniker: e.source_moniker.clone(), |
| source_name: e.source_name.clone(), |
| source: e.source.clone().map_or_else(|| None, |t| Some(t.into())), |
| capability: e.capability.clone().map_or_else(|| None, |t| Some(t.into())), |
| target_monikers: e.target_monikers.clone(), |
| ..Default::default() |
| }) |
| .collect::<Vec<_>>(); |
| component_internal::CapabilityPolicyAllowlists { |
| allowlist: Some(allowlist), |
| ..Default::default() |
| } |
| } |
| |
| fn translate_debug_registration_policy( |
| debug_registration_policy: Vec<DebugRegistrationAllowlistEntry>, |
| ) -> component_internal::DebugRegistrationPolicyAllowlists { |
| let allowlist = debug_registration_policy |
| .iter() |
| .map(|e| component_internal::DebugRegistrationAllowlistEntry { |
| name: e.name.clone(), |
| debug: e.debug.clone().map_or_else(|| None, |t| Some(t.into())), |
| moniker: e.moniker.clone(), |
| environment_name: e.environment_name.clone(), |
| ..Default::default() |
| }) |
| .collect::<Vec<_>>(); |
| component_internal::DebugRegistrationPolicyAllowlists { |
| allowlist: Some(allowlist), |
| ..Default::default() |
| } |
| } |
| |
| impl Config { |
| fn from_json_file(path: &PathBuf) -> Result<Self, Error> { |
| let data = fs::read_to_string(path)?; |
| serde_json5::from_str(&data).map_err(|e| { |
| let serde_json5::Error::Message { location, msg } = e; |
| let location = location.map(|l| Location { line: l.line, column: l.column }); |
| Error::parse(msg, location, Some(path)) |
| }) |
| } |
| |
| fn merge(self, another: Config) -> Result<Self, Error> { |
| Ok(Config { |
| debug: merge_field!(self, another, debug), |
| enable_introspection: merge_field!(self, another, enable_introspection), |
| use_builtin_process_launcher: merge_field!(self, another, use_builtin_process_launcher), |
| maintain_utc_clock: merge_field!(self, another, maintain_utc_clock), |
| list_children_batch_size: merge_field!(self, another, list_children_batch_size), |
| security_policy: deep_merge_field!(self, another, security_policy), |
| namespace_capabilities: merge_field!(self, another, namespace_capabilities), |
| builtin_capabilities: merge_field!(self, another, builtin_capabilities), |
| num_threads: merge_field!(self, another, num_threads), |
| root_component_url: merge_field!(self, another, root_component_url), |
| component_id_index_path: merge_field!(self, another, component_id_index_path), |
| log_destination: merge_field!(self, another, log_destination), |
| log_all_events: merge_field!(self, another, log_all_events), |
| builtin_boot_resolver: merge_field!(self, another, builtin_boot_resolver), |
| realm_builder_resolver_and_runner: merge_field!( |
| self, |
| another, |
| realm_builder_resolver_and_runner |
| ), |
| abi_revision_policy: merge_field!(self, another, abi_revision_policy), |
| vmex_source: merge_field!(self, another, vmex_source), |
| }) |
| } |
| |
| fn validate_namespace_capability( |
| capability: &cml::Capability, |
| used_ids: &mut HashSet<String>, |
| ) -> Result<(), Error> { |
| if capability.directory.is_some() && capability.path.is_none() { |
| return Err(Error::validate("\"path\" should be present with \"directory\"")); |
| } |
| if capability.directory.is_some() && capability.rights.is_none() { |
| return Err(Error::validate("\"rights\" should be present with \"directory\"")); |
| } |
| if capability.storage.is_some() { |
| return Err(Error::validate("\"storage\" is not supported for namespace capabilities")); |
| } |
| if capability.runner.is_some() { |
| return Err(Error::validate("\"runner\" is not supported for namespace capabilities")); |
| } |
| if capability.resolver.is_some() { |
| return Err(Error::validate( |
| "\"resolver\" is not supported for namespace capabilities", |
| )); |
| } |
| |
| // Disallow multiple capability ids of the same name. |
| let capability_ids = cml::CapabilityId::from_capability(capability)?; |
| for capability_id in capability_ids { |
| if !used_ids.insert(capability_id.to_string()) { |
| return Err(Error::validate(format!( |
| "\"{}\" is a duplicate \"capability\" name", |
| capability_id, |
| ))); |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| fn validate_builtin_capability( |
| capability: &cml::Capability, |
| used_ids: &mut HashSet<String>, |
| ) -> Result<(), Error> { |
| if capability.storage.is_some() { |
| return Err(Error::validate("\"storage\" is not supported for built-in capabilities")); |
| } |
| if capability.directory.is_some() && capability.rights.is_none() { |
| return Err(Error::validate("\"rights\" should be present with \"directory\"")); |
| } |
| if capability.path.is_some() { |
| return Err(Error::validate( |
| "\"path\" should not be present for built-in capabilities", |
| )); |
| } |
| |
| // Disallow multiple capability ids of the same name. |
| let capability_ids = cml::CapabilityId::from_capability(capability)?; |
| for capability_id in capability_ids { |
| if !used_ids.insert(capability_id.to_string()) { |
| return Err(Error::validate(format!( |
| "\"{}\" is a duplicate \"capability\" name", |
| capability_id, |
| ))); |
| } |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| #[derive(Deserialize, Debug, Default)] |
| #[serde(deny_unknown_fields)] |
| struct ProductConfig { |
| #[serde(default)] |
| ambient_mark_vmo_exec: AmbientMarkVmoExec, |
| #[serde(default)] |
| platform_capabilities: PlatformCapabilities, |
| #[serde(default)] |
| product_capabilities: ProductCapabilities, |
| } |
| |
| #[derive(Deserialize, Debug, Default)] |
| #[serde(deny_unknown_fields)] |
| struct AmbientMarkVmoExec { |
| session: Vec<String>, |
| } |
| |
| #[derive(Deserialize, Debug, Default)] |
| #[serde(deny_unknown_fields)] |
| struct PlatformCapabilities { |
| session: Vec<PlatformCapabilityEntry>, |
| } |
| |
| #[derive(Deserialize, Debug, Default)] |
| #[serde(deny_unknown_fields)] |
| struct ProductCapabilities { |
| session: Vec<ProductCapabilityEntry>, |
| } |
| |
| #[derive(Deserialize, Debug)] |
| #[serde(deny_unknown_fields)] |
| struct PlatformCapabilityEntry { |
| capability: PlatformCapability, |
| target_monikers: Vec<String>, |
| } |
| |
| #[derive(Deserialize, Debug)] |
| #[serde(deny_unknown_fields)] |
| struct ProductCapabilityEntry { |
| source_moniker: String, |
| capability: String, |
| target_monikers: Vec<String>, |
| } |
| |
| impl ProductConfig { |
| fn from_json_file(path: &PathBuf) -> Result<Self, Error> { |
| let data = fs::read_to_string(path)?; |
| serde_json5::from_str(&data).map_err(|e| { |
| let serde_json5::Error::Message { location, msg } = e; |
| let location = location.map(|l| Location { line: l.line, column: l.column }); |
| Error::parse(msg, location, Some(path)) |
| }) |
| } |
| } |
| |
| impl TryInto<Config> for ProductConfig { |
| type Error = Error; |
| |
| fn try_into(self) -> Result<Config, Error> { |
| let Self { ambient_mark_vmo_exec, platform_capabilities, product_capabilities } = self; |
| |
| fn rebase_string(s: String) -> String { |
| format!("{SESSION_MONIKER}/{s}") |
| } |
| |
| fn rebase_vec_string(vec: Vec<String>) -> Vec<String> { |
| vec.into_iter().map(|s| rebase_string(s)).collect() |
| } |
| |
| let vmo_exec = |
| ambient_mark_vmo_exec.session.into_iter().map(|s| rebase_string(s)).collect(); |
| let job_policy = |
| JobPolicyAllowlists { ambient_mark_vmo_exec: Some(vmo_exec), ..Default::default() }; |
| |
| let platform_capabilities = |
| platform_capabilities.session.into_iter().map(|entry| CapabilityAllowlistEntry { |
| source: Some(CapabilityFrom::Component), |
| capability: Some(CapabilityTypeName::Protocol), |
| source_moniker: Some(entry.capability.source_moniker().to_string()), |
| source_name: Some(entry.capability.source_name().to_string()), |
| target_monikers: Some(rebase_vec_string(entry.target_monikers)), |
| }); |
| |
| let capability_policy = product_capabilities |
| .session |
| .into_iter() |
| .map(|entry| CapabilityAllowlistEntry { |
| source: Some(CapabilityFrom::Component), |
| capability: Some(CapabilityTypeName::Protocol), |
| source_moniker: Some(rebase_string(entry.source_moniker)), |
| source_name: Some(entry.capability), |
| target_monikers: Some(rebase_vec_string(entry.target_monikers)), |
| }) |
| .chain(platform_capabilities) |
| .collect(); |
| |
| Ok(Config { |
| security_policy: Some(SecurityPolicy { |
| job_policy: Some(job_policy), |
| capability_policy: Some(capability_policy), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }) |
| } |
| } |
| |
| /// Create a binary config and populate it with data from .json file. |
| #[derive(Debug, Default, FromArgs)] |
| pub struct Args { |
| /// path to a JSON configuration file |
| #[argh(option)] |
| pub input: Vec<PathBuf>, |
| |
| /// path to a product-specific JSON configuration file |
| #[argh(option)] |
| pub product: Vec<PathBuf>, |
| |
| /// path to the output binary config file |
| #[argh(option)] |
| pub output: PathBuf, |
| } |
| |
| /// Compile multiple platform configs with an optional product config, and generate a file that can |
| /// be added to BootFS for component_manager to use at runtime. |
| pub fn compile(args: Args) -> Result<(), Error> { |
| let configs = |
| args.input.iter().map(Config::from_json_file).collect::<Result<Vec<Config>, _>>()?; |
| let mut config_json = |
| configs.into_iter().try_fold(Config::default(), |acc, next| acc.merge(next))?; |
| |
| for product in args.product.into_iter() { |
| let product = ProductConfig::from_json_file(&product)?; |
| config_json = config_json.merge(product.try_into()?)?; |
| } |
| |
| let config_fidl: component_internal::Config = config_json.try_into()?; |
| let bytes = persist(&config_fidl).map_err(|e| Error::FidlEncoding(e))?; |
| let mut file = File::create(args.output).map_err(|e| Error::Io(e))?; |
| file.write_all(&bytes).map_err(|e| Error::Io(e))?; |
| Ok(()) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use { |
| assert_matches::assert_matches, fidl::unpersist, fidl_fuchsia_component_decl as fdecl, |
| fidl_fuchsia_io as fio, std::io::Read, tempfile::TempDir, |
| }; |
| |
| fn compile_str(input: &str) -> Result<component_internal::Config, Error> { |
| let tmp_dir = TempDir::new().unwrap(); |
| let input_path = tmp_dir.path().join("config.json"); |
| let output_path = tmp_dir.path().join("config.fidl"); |
| File::create(&input_path).unwrap().write_all(input.as_bytes()).unwrap(); |
| let args = Args { output: output_path.clone(), input: vec![input_path], product: vec![] }; |
| compile(args)?; |
| let mut bytes = Vec::new(); |
| File::open(output_path)?.read_to_end(&mut bytes)?; |
| let config: component_internal::Config = unpersist(&bytes)?; |
| Ok(config) |
| } |
| |
| #[test] |
| fn test_compile() { |
| let input = r#"{ |
| debug: true, |
| enable_introspection: true, |
| list_children_batch_size: 123, |
| maintain_utc_clock: false, |
| use_builtin_process_launcher: true, |
| security_policy: { |
| job_policy: { |
| main_process_critical: [ "/", "/bar" ], |
| ambient_mark_vmo_exec: ["/foo"], |
| create_raw_processes: ["/baz"], |
| }, |
| capability_policy: [ |
| { |
| source_moniker: "<component_manager>", |
| source: "component", |
| source_name: "fuchsia.boot.RootResource", |
| capability: "protocol", |
| target_monikers: ["/root", "/root/bootstrap", "/root/core"], |
| }, |
| ], |
| debug_registration_policy: [ |
| { |
| name: "fuchsia.boot.RootResource", |
| debug: "protocol", |
| moniker: "/foo", |
| environment_name: "my_env", |
| }, |
| ], |
| child_policy: { |
| reboot_on_terminate: [ "/buz" ], |
| }, |
| }, |
| namespace_capabilities: [ |
| { |
| protocol: "foo_svc", |
| }, |
| { |
| directory: "bar_dir", |
| path: "/bar", |
| rights: [ "connect" ], |
| }, |
| ], |
| builtin_capabilities: [ |
| { |
| protocol: "foo_protocol", |
| }, |
| { |
| directory: "foo_dir", |
| rights: [ "connect" ], |
| }, |
| { |
| service: "foo_svc", |
| }, |
| { |
| runner: "foo_runner", |
| }, |
| { |
| resolver: "foo_resolver", |
| }, |
| { |
| event_stream: "foo_event_stream", |
| } |
| ], |
| num_threads: 321, |
| root_component_url: "fuchsia-pkg://fuchsia.com/foo#meta/foo.cm", |
| component_id_index_path: "/this/is/an/absolute/path", |
| log_destination: "klog", |
| log_all_events: true, |
| builtin_boot_resolver: "boot", |
| realm_builder_resolver_and_runner: "namespace", |
| vmex_source: "namespace", |
| }"#; |
| let config = compile_str(input).expect("failed to compile"); |
| assert_eq!( |
| config, |
| component_internal::Config { |
| debug: Some(true), |
| enable_introspection: Some(true), |
| maintain_utc_clock: Some(false), |
| use_builtin_process_launcher: Some(true), |
| list_children_batch_size: Some(123), |
| security_policy: Some(component_internal::SecurityPolicy { |
| job_policy: Some(component_internal::JobPolicyAllowlists { |
| main_process_critical: Some(vec!["/".to_string(), "/bar".to_string()]), |
| ambient_mark_vmo_exec: Some(vec!["/foo".to_string()]), |
| create_raw_processes: Some(vec!["/baz".to_string()]), |
| ..Default::default() |
| }), |
| capability_policy: Some(component_internal::CapabilityPolicyAllowlists { |
| allowlist: Some(vec![component_internal::CapabilityAllowlistEntry { |
| source_moniker: Some("<component_manager>".to_string()), |
| source_name: Some("fuchsia.boot.RootResource".to_string()), |
| source: Some(fdecl::Ref::Self_(fdecl::SelfRef {})), |
| capability: Some(component_internal::AllowlistedCapability::Protocol( |
| component_internal::AllowlistedProtocol::default() |
| )), |
| target_monikers: Some(vec![ |
| "/root".to_string(), |
| "/root/bootstrap".to_string(), |
| "/root/core".to_string() |
| ]), |
| ..Default::default() |
| },]), |
| ..Default::default() |
| }), |
| debug_registration_policy: Some( |
| component_internal::DebugRegistrationPolicyAllowlists { |
| allowlist: Some(vec![ |
| component_internal::DebugRegistrationAllowlistEntry { |
| name: Some("fuchsia.boot.RootResource".to_string()), |
| debug: Some( |
| component_internal::AllowlistedDebugRegistration::Protocol( |
| component_internal::AllowlistedProtocol::default() |
| ) |
| ), |
| moniker: Some("/foo".to_string()), |
| environment_name: Some("my_env".to_string()), |
| ..Default::default() |
| } |
| ]), |
| ..Default::default() |
| } |
| ), |
| child_policy: Some(component_internal::ChildPolicyAllowlists { |
| reboot_on_terminate: Some(vec!["/buz".to_string()]), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }), |
| namespace_capabilities: Some(vec![ |
| fdecl::Capability::Protocol(fdecl::Protocol { |
| name: Some("foo_svc".into()), |
| source_path: Some("/svc/foo_svc".into()), |
| ..Default::default() |
| }), |
| fdecl::Capability::Directory(fdecl::Directory { |
| name: Some("bar_dir".into()), |
| source_path: Some("/bar".into()), |
| rights: Some(fio::Operations::CONNECT), |
| ..Default::default() |
| }), |
| ]), |
| builtin_capabilities: Some(vec![ |
| fdecl::Capability::Protocol(fdecl::Protocol { |
| name: Some("foo_protocol".into()), |
| source_path: None, |
| ..Default::default() |
| }), |
| fdecl::Capability::Directory(fdecl::Directory { |
| name: Some("foo_dir".into()), |
| source_path: None, |
| rights: Some(fio::Operations::CONNECT), |
| ..Default::default() |
| }), |
| fdecl::Capability::Service(fdecl::Service { |
| name: Some("foo_svc".into()), |
| source_path: None, |
| ..Default::default() |
| }), |
| fdecl::Capability::Runner(fdecl::Runner { |
| name: Some("foo_runner".into()), |
| source_path: None, |
| ..Default::default() |
| }), |
| fdecl::Capability::Resolver(fdecl::Resolver { |
| name: Some("foo_resolver".into()), |
| source_path: None, |
| ..Default::default() |
| }), |
| fdecl::Capability::EventStream(fdecl::EventStream { |
| name: Some("foo_event_stream".into()), |
| ..Default::default() |
| }), |
| ]), |
| num_threads: Some(321), |
| root_component_url: Some("fuchsia-pkg://fuchsia.com/foo#meta/foo.cm".to_string()), |
| component_id_index_path: Some("/this/is/an/absolute/path".to_string()), |
| log_destination: Some(component_internal::LogDestination::Klog), |
| log_all_events: Some(true), |
| builtin_boot_resolver: Some(component_internal::BuiltinBootResolver::Boot), |
| realm_builder_resolver_and_runner: Some( |
| component_internal::RealmBuilderResolverAndRunner::Namespace |
| ), |
| vmex_source: Some(component_internal::VmexSource::Namespace), |
| ..Default::default() |
| } |
| ); |
| } |
| |
| #[test] |
| fn test_validate_namespace_capabilities() { |
| { |
| let input = r#"{ |
| namespace_capabilities: [ |
| { |
| protocol: "foo", |
| }, |
| { |
| directory: "foo", |
| path: "/foo", |
| rights: [ "connect" ], |
| }, |
| ], |
| }"#; |
| assert_matches!( |
| compile_str(input), |
| Err(Error::Validate { err, .. } ) |
| if &err == "\"foo\" is a duplicate \"capability\" name" |
| ); |
| } |
| { |
| let input = r#"{ |
| namespace_capabilities: [ |
| { |
| directory: "foo", |
| path: "/foo", |
| }, |
| ], |
| }"#; |
| assert_matches!( |
| compile_str(input), |
| Err(Error::Validate { err, .. } ) |
| if &err == "\"rights\" should be present with \"directory\"" |
| ); |
| } |
| { |
| let input = r#"{ |
| namespace_capabilities: [ |
| { |
| directory: "foo", |
| rights: [ "connect" ], |
| }, |
| ], |
| }"#; |
| assert_matches!( |
| compile_str(input), |
| Err(Error::Validate { err, .. } ) |
| if &err == "\"path\" should be present with \"directory\"" |
| ); |
| } |
| } |
| |
| #[test] |
| fn test_validate_builtin_capabilities() { |
| { |
| let input = r#"{ |
| builtin_capabilities: [ |
| { |
| protocol: "foo", |
| }, |
| { |
| directory: "foo", |
| rights: [ "connect" ], |
| }, |
| ], |
| }"#; |
| assert_matches!( |
| compile_str(input), |
| Err(Error::Validate { err, .. } ) |
| if &err == "\"foo\" is a duplicate \"capability\" name" |
| ); |
| } |
| { |
| let input = r#"{ |
| builtin_capabilities: [ |
| { |
| directory: "foo", |
| path: "/foo", |
| }, |
| ], |
| }"#; |
| assert_matches!( |
| compile_str(input), |
| Err(Error::Validate { err, .. } ) |
| if &err == "\"rights\" should be present with \"directory\"" |
| ); |
| } |
| { |
| let input = r#"{ |
| builtin_capabilities: [ |
| { |
| storage: "foo", |
| }, |
| ], |
| }"#; |
| assert_matches!( |
| compile_str(input), |
| Err(Error::Validate { err, .. } ) |
| if &err == "\"storage\" is not supported for built-in capabilities" |
| ); |
| } |
| { |
| let input = r#"{ |
| builtin_capabilities: [ |
| { |
| runner: "foo", |
| path: "/foo", |
| }, |
| ], |
| }"#; |
| assert_matches!( |
| compile_str(input), |
| Err(Error::Validate { err, .. } ) |
| if &err == "\"path\" should not be present for built-in capabilities" |
| ); |
| } |
| } |
| |
| #[test] |
| fn test_compile_conflict() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let output_path = tmp_dir.path().join("config"); |
| |
| let input_path = tmp_dir.path().join("foo.json"); |
| let input = "{\"debug\": true,}"; |
| File::create(&input_path).unwrap().write_all(input.as_bytes()).unwrap(); |
| |
| let another_input_path = tmp_dir.path().join("bar.json"); |
| let another_input = "{\"debug\": false,}"; |
| File::create(&another_input_path).unwrap().write_all(another_input.as_bytes()).unwrap(); |
| |
| let args = Args { |
| output: output_path.clone(), |
| input: vec![input_path, another_input_path], |
| product: vec![], |
| }; |
| assert_matches!(compile(args), Err(Error::Parse { .. })); |
| } |
| |
| #[test] |
| fn test_merge() -> Result<(), Error> { |
| let tmp_dir = TempDir::new().unwrap(); |
| let output_path = tmp_dir.path().join("config"); |
| |
| let input_path = tmp_dir.path().join("foo.json"); |
| let input = "{\"debug\": true,}"; |
| File::create(&input_path).unwrap().write_all(input.as_bytes()).unwrap(); |
| |
| let another_input_path = tmp_dir.path().join("bar.json"); |
| let another_input = "{\"list_children_batch_size\": 42}"; |
| File::create(&another_input_path).unwrap().write_all(another_input.as_bytes()).unwrap(); |
| |
| let args = Args { |
| output: output_path.clone(), |
| input: vec![input_path, another_input_path], |
| product: vec![], |
| }; |
| compile(args)?; |
| |
| let mut bytes = Vec::new(); |
| File::open(output_path)?.read_to_end(&mut bytes)?; |
| let config: component_internal::Config = unpersist(&bytes)?; |
| assert_eq!(config.debug, Some(true)); |
| assert_eq!(config.list_children_batch_size, Some(42)); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_deep_merge() -> Result<(), Error> { |
| let tmp_dir = TempDir::new().unwrap(); |
| let output_path = tmp_dir.path().join("config"); |
| |
| let input_path = tmp_dir.path().join("foo.json"); |
| let input = r#"{ |
| security_policy: { |
| job_policy: { |
| ambient_mark_vmo_exec: ["/foo1"], |
| create_raw_processes: ["/foo1"], |
| }, |
| }, |
| }"#; |
| File::create(&input_path).unwrap().write_all(input.as_bytes()).unwrap(); |
| |
| let another_input_path = tmp_dir.path().join("bar.json"); |
| let another_input = r#"{ |
| security_policy: { |
| job_policy: { |
| ambient_mark_vmo_exec: ["/foo2"], |
| create_raw_processes: ["/foo2"], |
| }, |
| }, |
| }"#; |
| File::create(&another_input_path).unwrap().write_all(another_input.as_bytes()).unwrap(); |
| |
| let args = Args { |
| output: output_path.clone(), |
| input: vec![input_path, another_input_path], |
| product: vec![], |
| }; |
| compile(args)?; |
| |
| let mut bytes = Vec::new(); |
| File::open(output_path)?.read_to_end(&mut bytes)?; |
| let config: component_internal::Config = unpersist(&bytes)?; |
| assert_eq!( |
| config.security_policy, |
| Some(component_internal::SecurityPolicy { |
| job_policy: Some(component_internal::JobPolicyAllowlists { |
| ambient_mark_vmo_exec: Some(vec!["/foo1".to_string(), "/foo2".to_string()]), |
| create_raw_processes: Some(vec!["/foo1".to_string(), "/foo2".to_string()]), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }) |
| ); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_invalid_component_url() { |
| let input = r#"{ |
| root_component_url: "not quite a valid Url", |
| }"#; |
| assert_matches!(compile_str(input), Err(Error::Parse { .. })); |
| } |
| |
| #[test] |
| fn test_capability_allowlist_merging() { |
| let left = vec![ |
| CapabilityAllowlistEntry { |
| source_moniker: Some("moniker1".into()), |
| source_name: Some("name1".into()), |
| source: Some(CapabilityFrom::Capability), |
| capability: Some(CapabilityTypeName::Protocol), |
| target_monikers: Some(vec!["target1".into()]), |
| }, |
| CapabilityAllowlistEntry { |
| source_moniker: Some("moniker2".into()), |
| source_name: Some("name2".into()), |
| source: Some(CapabilityFrom::Capability), |
| capability: Some(CapabilityTypeName::Protocol), |
| target_monikers: Some(vec!["target2".into()]), |
| }, |
| ]; |
| |
| let right = vec![ |
| CapabilityAllowlistEntry { |
| source_moniker: Some("moniker2".into()), |
| source_name: Some("name2".into()), |
| source: Some(CapabilityFrom::Capability), |
| capability: Some(CapabilityTypeName::Protocol), |
| target_monikers: Some(vec!["target3".into()]), |
| }, |
| CapabilityAllowlistEntry { |
| source_moniker: Some("moniker1".into()), |
| source_name: Some("name1".into()), |
| source: Some(CapabilityFrom::Capability), |
| capability: Some(CapabilityTypeName::Protocol), |
| target_monikers: Some(vec!["target4".into()]), |
| }, |
| ]; |
| |
| let expected_combine = vec![ |
| CapabilityAllowlistEntry { |
| source_moniker: Some("moniker1".into()), |
| source_name: Some("name1".into()), |
| source: Some(CapabilityFrom::Capability), |
| capability: Some(CapabilityTypeName::Protocol), |
| target_monikers: Some(vec!["target1".into(), "target4".into()]), |
| }, |
| CapabilityAllowlistEntry { |
| source_moniker: Some("moniker2".into()), |
| source_name: Some("name2".into()), |
| source: Some(CapabilityFrom::Capability), |
| capability: Some(CapabilityTypeName::Protocol), |
| target_monikers: Some(vec!["target2".into(), "target3".into()]), |
| }, |
| ]; |
| |
| let combined = CapabilityAllowlistEntry::merge_vecs(left, right).unwrap(); |
| assert_eq!(combined, expected_combine); |
| } |
| |
| #[test] |
| fn test_capability_allowlist_merging_no_dups() { |
| let left = vec![ |
| CapabilityAllowlistEntry { |
| source_moniker: Some("moniker1".into()), |
| source_name: Some("name1".into()), |
| source: Some(CapabilityFrom::Capability), |
| capability: Some(CapabilityTypeName::Protocol), |
| target_monikers: Some(vec!["target1".into()]), |
| }, |
| CapabilityAllowlistEntry { |
| source_moniker: Some("moniker2".into()), |
| source_name: Some("name2".into()), |
| source: Some(CapabilityFrom::Capability), |
| capability: Some(CapabilityTypeName::Protocol), |
| target_monikers: Some(vec!["target2".into()]), |
| }, |
| ]; |
| |
| let right = left.clone(); |
| |
| let expected_combine = left.clone(); |
| |
| let combined = CapabilityAllowlistEntry::merge_vecs(left, right).unwrap(); |
| assert_eq!(combined, expected_combine); |
| } |
| |
| #[test] |
| fn test_product_and_platform() -> Result<(), Error> { |
| let tmp_dir = TempDir::new().unwrap(); |
| let output_path = tmp_dir.path().join("config"); |
| |
| let input_path = tmp_dir.path().join("foo.json"); |
| let input = r#"{ |
| security_policy: { |
| job_policy: { |
| ambient_mark_vmo_exec: ["/foo1"], |
| create_raw_processes: ["/foo1"], |
| }, |
| capability_policy: [ |
| { |
| source_moniker: "<component_manager>", |
| source: "component", |
| source_name: "fuchsia.boot.RootResource", |
| capability: "protocol", |
| target_monikers: ["/root", "/root/bootstrap", "/root/core"], |
| }, |
| ], |
| }, |
| }"#; |
| File::create(&input_path).unwrap().write_all(input.as_bytes()).unwrap(); |
| |
| let another_input_path = tmp_dir.path().join("bar.json"); |
| let another_input = r#"{ |
| ambient_mark_vmo_exec: { |
| session: ["foo2"], |
| }, |
| platform_capabilities: { |
| session: [ |
| { |
| capability: "fuchsia.lowpan.device.DeviceExtraConnector", |
| target_monikers: ["session_comp1"], |
| }, |
| ], |
| }, |
| product_capabilities: { |
| session: [ |
| { |
| source_moniker: "session_comp1", |
| capability: "product.some.Resource", |
| target_monikers: ["session_comp2", "session_comp3"], |
| }, |
| ], |
| }, |
| }"#; |
| File::create(&another_input_path).unwrap().write_all(another_input.as_bytes()).unwrap(); |
| |
| let args = Args { |
| output: output_path.clone(), |
| input: vec![input_path], |
| product: vec![another_input_path], |
| }; |
| compile(args)?; |
| |
| let mut bytes = Vec::new(); |
| File::open(output_path)?.read_to_end(&mut bytes)?; |
| let config: component_internal::Config = unpersist(&bytes)?; |
| assert_eq!( |
| config.security_policy, |
| Some(component_internal::SecurityPolicy { |
| job_policy: Some(component_internal::JobPolicyAllowlists { |
| ambient_mark_vmo_exec: Some(vec![ |
| "/core/session-manager/session:session/foo2".to_string(), |
| "/foo1".to_string() |
| ]), |
| create_raw_processes: Some(vec!["/foo1".to_string()]), |
| ..Default::default() |
| }), |
| capability_policy: Some(component_internal::CapabilityPolicyAllowlists { |
| allowlist: Some(vec![ |
| component_internal::CapabilityAllowlistEntry { |
| source_moniker: Some("/core/lowpanservice".to_string()), |
| source_name: Some( |
| "fuchsia.lowpan.device.DeviceExtraConnector".to_string() |
| ), |
| source: Some(fdecl::Ref::Self_(fdecl::SelfRef {})), |
| capability: Some(component_internal::AllowlistedCapability::Protocol( |
| component_internal::AllowlistedProtocol::default() |
| )), |
| target_monikers: Some(vec![ |
| "/core/session-manager/session:session/session_comp1".to_string(), |
| ]), |
| ..Default::default() |
| }, |
| component_internal::CapabilityAllowlistEntry { |
| source_moniker: Some( |
| "/core/session-manager/session:session/session_comp1".to_string() |
| ), |
| source_name: Some("product.some.Resource".to_string()), |
| source: Some(fdecl::Ref::Self_(fdecl::SelfRef {})), |
| capability: Some(component_internal::AllowlistedCapability::Protocol( |
| component_internal::AllowlistedProtocol::default() |
| )), |
| target_monikers: Some(vec![ |
| "/core/session-manager/session:session/session_comp2".to_string(), |
| "/core/session-manager/session:session/session_comp3".to_string(), |
| ]), |
| ..Default::default() |
| }, |
| component_internal::CapabilityAllowlistEntry { |
| source_moniker: Some("<component_manager>".to_string()), |
| source_name: Some("fuchsia.boot.RootResource".to_string()), |
| source: Some(fdecl::Ref::Self_(fdecl::SelfRef {})), |
| capability: Some(component_internal::AllowlistedCapability::Protocol( |
| component_internal::AllowlistedProtocol::default() |
| )), |
| target_monikers: Some(vec![ |
| "/root".to_string(), |
| "/root/bootstrap".to_string(), |
| "/root/core".to_string() |
| ]), |
| ..Default::default() |
| }, |
| ]), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }) |
| ); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_product_only() -> Result<(), Error> { |
| let tmp_dir = TempDir::new().unwrap(); |
| let output_path = tmp_dir.path().join("config"); |
| |
| let another_input_path = tmp_dir.path().join("bar.json"); |
| let another_input = r#"{ |
| ambient_mark_vmo_exec: { |
| session: ["foo2"], |
| }, |
| platform_capabilities: { |
| session: [ |
| { |
| capability: "fuchsia.lowpan.device.DeviceExtraConnector", |
| target_monikers: ["session_comp1"], |
| }, |
| ], |
| }, |
| product_capabilities: { |
| session: [ |
| { |
| source_moniker: "session_comp1", |
| capability: "product.some.Resource", |
| target_monikers: ["session_comp2", "session_comp3"], |
| }, |
| ], |
| }, |
| }"#; |
| File::create(&another_input_path).unwrap().write_all(another_input.as_bytes()).unwrap(); |
| |
| let args = |
| Args { output: output_path.clone(), input: vec![], product: vec![another_input_path] }; |
| compile(args)?; |
| |
| let mut bytes = Vec::new(); |
| File::open(output_path)?.read_to_end(&mut bytes)?; |
| let config: component_internal::Config = unpersist(&bytes)?; |
| assert_eq!( |
| config.security_policy, |
| Some(component_internal::SecurityPolicy { |
| job_policy: Some(component_internal::JobPolicyAllowlists { |
| ambient_mark_vmo_exec: Some(vec![ |
| "/core/session-manager/session:session/foo2".to_string(), |
| ]), |
| ..Default::default() |
| }), |
| capability_policy: Some(component_internal::CapabilityPolicyAllowlists { |
| allowlist: Some(vec![ |
| component_internal::CapabilityAllowlistEntry { |
| source_moniker: Some( |
| "/core/session-manager/session:session/session_comp1".to_string() |
| ), |
| source_name: Some("product.some.Resource".to_string()), |
| source: Some(fdecl::Ref::Self_(fdecl::SelfRef {})), |
| capability: Some(component_internal::AllowlistedCapability::Protocol( |
| component_internal::AllowlistedProtocol::default() |
| )), |
| target_monikers: Some(vec![ |
| "/core/session-manager/session:session/session_comp2".to_string(), |
| "/core/session-manager/session:session/session_comp3".to_string(), |
| ]), |
| ..Default::default() |
| }, |
| component_internal::CapabilityAllowlistEntry { |
| source_moniker: Some("/core/lowpanservice".to_string()), |
| source_name: Some( |
| "fuchsia.lowpan.device.DeviceExtraConnector".to_string() |
| ), |
| source: Some(fdecl::Ref::Self_(fdecl::SelfRef {})), |
| capability: Some(component_internal::AllowlistedCapability::Protocol( |
| component_internal::AllowlistedProtocol::default() |
| )), |
| target_monikers: Some(vec![ |
| "/core/session-manager/session:session/session_comp1".to_string(), |
| ]), |
| ..Default::default() |
| }, |
| ]), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }) |
| ); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_product_invalid_platform_capability() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let output_path = tmp_dir.path().join("config"); |
| |
| let another_input_path = tmp_dir.path().join("bar.json"); |
| let another_input = r#"{ |
| platform_capabilities: { |
| session: [ |
| { |
| capability: "fuchsia.nonexistent.Resource", |
| target_monikers: ["session_comp1"], |
| }, |
| ], |
| }, |
| }"#; |
| File::create(&another_input_path).unwrap().write_all(another_input.as_bytes()).unwrap(); |
| |
| let args = |
| Args { output: output_path.clone(), input: vec![], product: vec![another_input_path] }; |
| assert_matches!(compile(args), Err(Error::Parse { .. })); |
| } |
| } |