From 62e952559f600e72d7498c9b12f906cb0b1ba150 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Mon, 16 Sep 2024 21:39:37 +0100 Subject: [PATCH] refactor(permissions): split up Descriptor into Allow, Deny, and Query (#25508) This makes the permission system more versatile. --- cli/args/flags.rs | 103 +- cli/args/import_map.rs | 3 +- cli/args/mod.rs | 7 +- cli/cache/mod.rs | 8 +- cli/factory.rs | 53 +- cli/file_fetcher.rs | 166 +- cli/graph_container.rs | 4 +- cli/graph_util.rs | 6 +- cli/jsr.rs | 11 +- cli/lsp/config.rs | 24 +- cli/lsp/jsr.rs | 3 +- cli/lsp/npm.rs | 3 +- cli/lsp/registries.rs | 12 +- cli/lsp/testing/execution.rs | 15 +- cli/module_loader.rs | 4 +- cli/npm/byonm.rs | 2 +- cli/npm/managed/resolvers/common.rs | 3 +- cli/npm/mod.rs | 6 +- cli/ops/bench.rs | 19 +- cli/ops/testing.rs | 19 +- cli/standalone/mod.rs | 40 +- cli/tools/bench/mod.rs | 33 +- cli/tools/jupyter/mod.rs | 4 +- cli/tools/registry/pm/cache_deps.rs | 2 +- cli/tools/repl/mod.rs | 10 +- cli/tools/run/mod.rs | 18 +- cli/tools/serve.rs | 9 +- cli/tools/test/mod.rs | 40 +- cli/worker.rs | 79 +- ext/fetch/lib.rs | 27 +- ext/ffi/call.rs | 4 +- ext/ffi/callback.rs | 2 +- ext/ffi/dlfcn.rs | 18 +- ext/ffi/lib.rs | 23 +- ext/ffi/repr.rs | 42 +- ext/fs/lib.rs | 80 +- ext/fs/ops.rs | 302 +- ext/kv/sqlite.rs | 74 +- ext/napi/lib.rs | 16 +- ext/net/lib.rs | 45 +- ext/net/ops.rs | 21 +- ext/net/ops_tls.rs | 9 +- ext/net/ops_unix.rs | 42 +- ext/node/lib.rs | 35 +- ext/node/ops/fs.rs | 71 +- runtime/examples/extension/main.rs | 7 +- runtime/lib.rs | 1 + runtime/ops/fs_events.rs | 5 +- runtime/ops/permissions.rs | 160 +- runtime/ops/process.rs | 69 +- runtime/ops/worker_host.rs | 15 +- runtime/permissions.rs | 164 + runtime/permissions/lib.rs | 2661 ++++++++++------- runtime/snapshot.rs | 97 +- runtime/web_worker.rs | 7 +- runtime/worker.rs | 15 +- tests/integration/run_tests.rs | 6 - .../__test__.jsonc | 8 + .../deny_run_binary_absolute_path/main.out | 8 + .../deny_run_binary_absolute_path/main.ts | 15 + .../056_make_temp_file_write_perm.out | 4 + .../056_make_temp_file_write_perm.ts | 52 + .../make_temp_write_perm/__test__.jsonc | 15 + .../__test__.jsonc | 7 +- .../allow_run_allowlist_resolution/main.out | 2 +- .../allow_run_allowlist_resolution/main.ts | 37 +- tests/specs/run/ld_preload/__test__.jsonc | 4 +- .../run/ld_preload/set_with_allow_env.ts | 4 +- .../run/056_make_temp_file_write_perm.out | 1 - .../run/056_make_temp_file_write_perm.ts | 9 - .../workers/read_check_granular_worker.js | 18 +- tests/unit/os_test.ts | 4 +- tests/unit/worker_test.ts | 39 +- tools/lint.js | 2 +- 74 files changed, 3062 insertions(+), 1891 deletions(-) create mode 100644 runtime/permissions.rs create mode 100644 tests/specs/permission/deny_run_binary_absolute_path/__test__.jsonc create mode 100644 tests/specs/permission/deny_run_binary_absolute_path/main.out create mode 100644 tests/specs/permission/deny_run_binary_absolute_path/main.ts create mode 100644 tests/specs/permission/make_temp_write_perm/056_make_temp_file_write_perm.out create mode 100644 tests/specs/permission/make_temp_write_perm/056_make_temp_file_write_perm.ts create mode 100644 tests/specs/permission/make_temp_write_perm/__test__.jsonc delete mode 100644 tests/testdata/run/056_make_temp_file_write_perm.out delete mode 100644 tests/testdata/run/056_make_temp_file_write_perm.ts diff --git a/cli/args/flags.rs b/cli/args/flags.rs index f7502b7060..f832c2a623 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -32,7 +32,6 @@ use deno_core::normalize_path; use deno_core::resolve_url_or_path; use deno_core::url::Url; use deno_graph::GraphKind; -use deno_runtime::colors; use deno_runtime::deno_permissions::parse_sys_kind; use deno_runtime::deno_permissions::PermissionsOptions; use log::debug; @@ -661,107 +660,25 @@ impl PermissionFlags { || self.deny_write.is_some() } - pub fn to_options( - &self, - // will be None when `deno compile` can't resolve the cwd - initial_cwd: Option<&Path>, - ) -> Result { - fn convert_option_str_to_path_buf( - flag: &Option>, - initial_cwd: Option<&Path>, - ) -> Result>, AnyError> { - let Some(paths) = &flag else { - return Ok(None); - }; - - let mut new_paths = Vec::with_capacity(paths.len()); - for path in paths { - if let Some(initial_cwd) = initial_cwd { - new_paths.push(initial_cwd.join(path)) - } else { - let path = PathBuf::from(path); - if path.is_absolute() { - new_paths.push(path); - } else { - bail!("Could not resolve relative permission path '{}' when current working directory could not be resolved.", path.display()) - } - } - } - Ok(Some(new_paths)) - } - - fn resolve_allow_run( - allow_run: &[String], - ) -> Result, AnyError> { - let mut new_allow_run = Vec::with_capacity(allow_run.len()); - for command_name in allow_run { - if command_name.is_empty() { - bail!("Empty command name not allowed in --allow-run=...") - } - let command_path_result = which::which(command_name); - match command_path_result { - Ok(command_path) => new_allow_run.push(command_path), - Err(err) => { - log::info!( - "{} Failed to resolve '{}' for allow-run: {}", - colors::gray("Info"), - command_name, - err - ); - } - } - } - Ok(new_allow_run) - } - - let mut deny_write = - convert_option_str_to_path_buf(&self.deny_write, initial_cwd)?; - let allow_run = self - .allow_run - .as_ref() - .and_then(|raw_allow_run| match resolve_allow_run(raw_allow_run) { - Ok(resolved_allow_run) => { - if resolved_allow_run.is_empty() && !raw_allow_run.is_empty() { - None // convert to no permissions if now empty - } else { - Some(Ok(resolved_allow_run)) - } - } - Err(err) => Some(Err(err)), - }) - .transpose()?; - // add the allow_run list to deno_write - if let Some(allow_run_vec) = &allow_run { - if !allow_run_vec.is_empty() { - let deno_write = deny_write.get_or_insert_with(Vec::new); - deno_write.extend(allow_run_vec.iter().cloned()); - } - } - - Ok(PermissionsOptions { + pub fn to_options(&self) -> PermissionsOptions { + PermissionsOptions { allow_all: self.allow_all, allow_env: self.allow_env.clone(), deny_env: self.deny_env.clone(), allow_net: self.allow_net.clone(), deny_net: self.deny_net.clone(), - allow_ffi: convert_option_str_to_path_buf(&self.allow_ffi, initial_cwd)?, - deny_ffi: convert_option_str_to_path_buf(&self.deny_ffi, initial_cwd)?, - allow_read: convert_option_str_to_path_buf( - &self.allow_read, - initial_cwd, - )?, - deny_read: convert_option_str_to_path_buf(&self.deny_read, initial_cwd)?, - allow_run, + allow_ffi: self.allow_ffi.clone(), + deny_ffi: self.deny_ffi.clone(), + allow_read: self.allow_read.clone(), + deny_read: self.deny_read.clone(), + allow_run: self.allow_run.clone(), deny_run: self.deny_run.clone(), allow_sys: self.allow_sys.clone(), deny_sys: self.deny_sys.clone(), - allow_write: convert_option_str_to_path_buf( - &self.allow_write, - initial_cwd, - )?, - deny_write, + allow_write: self.allow_write.clone(), + deny_write: self.deny_write.clone(), prompt: !resolve_no_prompt(self), - }) + } } } diff --git a/cli/args/import_map.rs b/cli/args/import_map.rs index 7a16ab2156..ff2f158715 100644 --- a/cli/args/import_map.rs +++ b/cli/args/import_map.rs @@ -3,7 +3,6 @@ use deno_core::error::AnyError; use deno_core::serde_json; use deno_core::url::Url; -use deno_runtime::deno_permissions::PermissionsContainer; use crate::file_fetcher::FileFetcher; @@ -17,7 +16,7 @@ pub async fn resolve_import_map_value_from_specifier( Ok(serde_json::from_str(&data_url_text)?) } else { let file = file_fetcher - .fetch(specifier, &PermissionsContainer::allow_all()) + .fetch_bypass_permissions(specifier) .await? .into_text_decoded()?; Ok(serde_json::from_str(&file.source)?) diff --git a/cli/args/mod.rs b/cli/args/mod.rs index 7d885295e0..0e4004a530 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -27,7 +27,6 @@ use deno_npm::npm_rc::NpmRc; use deno_npm::npm_rc::ResolvedNpmRc; use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot; use deno_npm::NpmSystemInfo; -use deno_runtime::deno_permissions::PermissionsContainer; use deno_semver::npm::NpmPackageReqReference; use import_map::resolve_import_map_value_from_specifier; @@ -1082,7 +1081,7 @@ impl CliOptions { let specifier = specifier.clone(); async move { let file = file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) + .fetch_bypass_permissions(&specifier) .await? .into_text_decoded()?; Ok(file.source.to_string()) @@ -1501,8 +1500,8 @@ impl CliOptions { &self.flags.permissions } - pub fn permissions_options(&self) -> Result { - self.flags.permissions.to_options(Some(&self.initial_cwd)) + pub fn permissions_options(&self) -> PermissionsOptions { + self.flags.permissions.to_options() } pub fn reload_flag(&self) -> bool { diff --git a/cli/cache/mod.rs b/cli/cache/mod.rs index 3b4e277607..86b65bc53e 100644 --- a/cli/cache/mod.rs +++ b/cli/cache/mod.rs @@ -4,6 +4,7 @@ use crate::args::CacheSetting; use crate::errors::get_error_class_name; use crate::file_fetcher::FetchNoFollowOptions; use crate::file_fetcher::FetchOptions; +use crate::file_fetcher::FetchPermissionsOption; use crate::file_fetcher::FileFetcher; use crate::file_fetcher::FileOrRedirect; use crate::npm::CliNpmResolver; @@ -18,7 +19,6 @@ use deno_graph::source::CacheInfo; use deno_graph::source::LoadFuture; use deno_graph::source::LoadResponse; use deno_graph::source::Loader; -use deno_runtime::deno_permissions::PermissionsContainer; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; @@ -112,7 +112,7 @@ pub struct FetchCacher { global_http_cache: Arc, npm_resolver: Arc, module_info_cache: Arc, - permissions: PermissionsContainer, + permissions: FetchPermissionsOption, cache_info_enabled: bool, } @@ -123,7 +123,7 @@ impl FetchCacher { global_http_cache: Arc, npm_resolver: Arc, module_info_cache: Arc, - permissions: PermissionsContainer, + permissions: FetchPermissionsOption, ) -> Self { Self { file_fetcher, @@ -230,7 +230,7 @@ impl Loader for FetchCacher { .fetch_no_follow_with_options(FetchNoFollowOptions { fetch_options: FetchOptions { specifier: &specifier, - permissions: &permissions, + permissions: permissions.as_ref(), maybe_accept: None, maybe_cache_setting: maybe_cache_setting.as_ref(), }, diff --git a/cli/factory.rs b/cli/factory.rs index 1ec2104edc..fe6d5b92ae 100644 --- a/cli/factory.rs +++ b/cli/factory.rs @@ -65,10 +65,13 @@ use deno_core::FeatureChecker; use deno_runtime::deno_fs; use deno_runtime::deno_node::DenoFsNodeResolverEnv; use deno_runtime::deno_node::NodeResolver; +use deno_runtime::deno_permissions::Permissions; +use deno_runtime::deno_permissions::PermissionsContainer; use deno_runtime::deno_tls::rustls::RootCertStore; use deno_runtime::deno_tls::RootCertStoreProvider; use deno_runtime::deno_web::BlobStore; use deno_runtime::inspector_server::InspectorServer; +use deno_runtime::permissions::RuntimePermissionDescriptorParser; use log::warn; use node_resolver::analyze::NodeCodeTranslator; use once_cell::sync::OnceCell; @@ -181,6 +184,7 @@ struct CliFactoryServices { node_code_translator: Deferred>, node_resolver: Deferred>, npm_resolver: Deferred>, + permission_desc_parser: Deferred>, sloppy_imports_resolver: Deferred>>, text_only_progress_bar: Deferred, type_checker: Deferred>, @@ -708,6 +712,15 @@ impl CliFactory { .await } + pub fn permission_desc_parser( + &self, + ) -> Result<&Arc, AnyError> { + self.services.permission_desc_parser.get_or_try_init(|| { + let fs = self.fs().clone(); + Ok(Arc::new(RuntimePermissionDescriptorParser::new(fs))) + }) + } + pub fn feature_checker(&self) -> Result<&Arc, AnyError> { self.services.feature_checker.get_or_try_init(|| { let cli_options = self.cli_options()?; @@ -739,6 +752,17 @@ impl CliFactory { )) } + pub fn create_permissions_container( + &self, + ) -> Result { + let desc_parser = self.permission_desc_parser()?.clone(); + let permissions = Permissions::from_options( + desc_parser.as_ref(), + &self.cli_options()?.permissions_options(), + )?; + Ok(PermissionsContainer::new(desc_parser, permissions)) + } + pub async fn create_cli_main_worker_factory( &self, ) -> Result { @@ -754,11 +778,17 @@ impl CliFactory { }; Ok(CliMainWorkerFactory::new( - StorageKeyResolver::from_options(cli_options), - cli_options.sub_command().clone(), - npm_resolver.clone(), - node_resolver.clone(), self.blob_store().clone(), + if cli_options.code_cache_enabled() { + Some(self.code_cache()?.clone()) + } else { + None + }, + self.feature_checker()?.clone(), + self.fs().clone(), + maybe_file_watcher_communicator, + self.maybe_inspector_server()?.clone(), + cli_options.maybe_lockfile().cloned(), Box::new(CliModuleLoaderFactory::new( cli_options, if cli_options.code_cache_enabled() { @@ -779,17 +809,12 @@ impl CliFactory { self.parsed_source_cache().clone(), self.resolver().await?.clone(), )), + node_resolver.clone(), + npm_resolver.clone(), + self.permission_desc_parser()?.clone(), self.root_cert_store_provider().clone(), - self.fs().clone(), - maybe_file_watcher_communicator, - self.maybe_inspector_server()?.clone(), - cli_options.maybe_lockfile().cloned(), - self.feature_checker()?.clone(), - if cli_options.code_cache_enabled() { - Some(self.code_cache()?.clone()) - } else { - None - }, + StorageKeyResolver::from_options(cli_options), + cli_options.sub_command().clone(), self.create_cli_main_worker_options()?, )) } diff --git a/cli/file_fetcher.rs b/cli/file_fetcher.rs index ace4d3c7ee..ca8fcbee43 100644 --- a/cli/file_fetcher.rs +++ b/cli/file_fetcher.rs @@ -161,9 +161,38 @@ fn get_validated_scheme( } } +#[derive(Debug, Copy, Clone)] +pub enum FetchPermissionsOptionRef<'a> { + AllowAll, + Container(&'a PermissionsContainer), +} + +#[derive(Debug, Clone)] +pub enum FetchPermissionsOption { + AllowAll, + Container(PermissionsContainer), +} + +impl FetchPermissionsOption { + pub fn as_ref(&self) -> FetchPermissionsOptionRef { + match self { + FetchPermissionsOption::AllowAll => FetchPermissionsOptionRef::AllowAll, + FetchPermissionsOption::Container(container) => { + FetchPermissionsOptionRef::Container(container) + } + } + } +} + +impl From for FetchPermissionsOption { + fn from(value: PermissionsContainer) -> Self { + Self::Container(value) + } +} + pub struct FetchOptions<'a> { pub specifier: &'a ModuleSpecifier, - pub permissions: &'a PermissionsContainer, + pub permissions: FetchPermissionsOptionRef<'a>, pub maybe_accept: Option<&'a str>, pub maybe_cache_setting: Option<&'a CacheSetting>, } @@ -515,11 +544,33 @@ impl FileFetcher { } } + #[inline(always)] + pub async fn fetch_bypass_permissions( + &self, + specifier: &ModuleSpecifier, + ) -> Result { + self + .fetch_inner(specifier, FetchPermissionsOptionRef::AllowAll) + .await + } + /// Fetch a source file and asynchronously return it. + #[allow(dead_code)] // todo(25469): undo when merging + #[inline(always)] pub async fn fetch( &self, specifier: &ModuleSpecifier, permissions: &PermissionsContainer, + ) -> Result { + self + .fetch_inner(specifier, FetchPermissionsOptionRef::Container(permissions)) + .await + } + + async fn fetch_inner( + &self, + specifier: &ModuleSpecifier, + permissions: FetchPermissionsOptionRef<'_>, ) -> Result { self .fetch_with_options(FetchOptions { @@ -583,7 +634,14 @@ impl FileFetcher { specifier ); let scheme = get_validated_scheme(specifier)?; - options.permissions.check_specifier(specifier)?; + match options.permissions { + FetchPermissionsOptionRef::AllowAll => { + // allow + } + FetchPermissionsOptionRef::Container(permissions) => { + permissions.check_specifier(specifier)?; + } + } if let Some(file) = self.memory_files.get(specifier) { Ok(FileOrRedirect::File(file)) } else if scheme == "file" { @@ -684,9 +742,7 @@ mod tests { async fn test_fetch(specifier: &ModuleSpecifier) -> (File, FileFetcher) { let (file_fetcher, _) = setup(CacheSetting::ReloadAll, None); - let result = file_fetcher - .fetch(specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher.fetch_bypass_permissions(specifier).await; assert!(result.is_ok()); (result.unwrap(), file_fetcher) } @@ -700,7 +756,7 @@ mod tests { .fetch_with_options_and_max_redirect( FetchOptions { specifier, - permissions: &PermissionsContainer::allow_all(), + permissions: FetchPermissionsOptionRef::AllowAll, maybe_accept: None, maybe_cache_setting: Some(&file_fetcher.cache_setting), }, @@ -796,9 +852,7 @@ mod tests { }; file_fetcher.insert_memory_files(file.clone()); - let result = file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let result_file = result.unwrap(); assert_eq!(result_file, file); @@ -809,9 +863,7 @@ mod tests { let (file_fetcher, _) = setup(CacheSetting::Use, None); let specifier = resolve_url("data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=").unwrap(); - let result = file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap().into_text_decoded().unwrap(); assert_eq!( @@ -840,9 +892,7 @@ mod tests { None, ); - let result = file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap().into_text_decoded().unwrap(); assert_eq!( @@ -862,9 +912,7 @@ mod tests { let specifier = ModuleSpecifier::parse("http://localhost:4545/subdir/mod2.ts").unwrap(); - let result = file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap().into_text_decoded().unwrap(); assert_eq!( @@ -882,9 +930,7 @@ mod tests { .set(&specifier, headers.clone(), file.source.as_bytes()) .unwrap(); - let result = file_fetcher_01 - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher_01.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap().into_text_decoded().unwrap(); assert_eq!( @@ -908,9 +954,7 @@ mod tests { .set(&specifier, headers.clone(), file.source.as_bytes()) .unwrap(); - let result = file_fetcher_02 - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher_02.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap().into_text_decoded().unwrap(); assert_eq!( @@ -933,9 +977,7 @@ mod tests { Default::default(), None, ); - let result = file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap().into_text_decoded().unwrap(); assert_eq!( @@ -966,9 +1008,7 @@ mod tests { None, ); - let result = file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let cache_key = file_fetcher.http_cache.cache_item_key(&specifier).unwrap(); @@ -1002,9 +1042,7 @@ mod tests { Default::default(), None, ); - let result = file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let cache_key = @@ -1041,9 +1079,7 @@ mod tests { resolve_url("http://localhost:4545/subdir/redirects/redirect1.js") .unwrap(); - let result = file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap(); assert_eq!(file.specifier, redirected_specifier); @@ -1082,9 +1118,7 @@ mod tests { resolve_url("http://localhost:4545/subdir/redirects/redirect1.js") .unwrap(); - let result = file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap(); assert_eq!(file.specifier, redirected_02_specifier); @@ -1142,9 +1176,7 @@ mod tests { None, ); - let result = file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let cache_key = file_fetcher @@ -1182,7 +1214,7 @@ mod tests { None, ); let result = file_fetcher - .fetch(&redirected_specifier, &PermissionsContainer::allow_all()) + .fetch_bypass_permissions(&redirected_specifier) .await; assert!(result.is_ok()); @@ -1223,7 +1255,7 @@ mod tests { .fetch_with_options_and_max_redirect( FetchOptions { specifier: &specifier, - permissions: &PermissionsContainer::allow_all(), + permissions: FetchPermissionsOptionRef::AllowAll, maybe_accept: None, maybe_cache_setting: Some(&file_fetcher.cache_setting), }, @@ -1236,7 +1268,7 @@ mod tests { .fetch_with_options_and_max_redirect( FetchOptions { specifier: &specifier, - permissions: &PermissionsContainer::allow_all(), + permissions: FetchPermissionsOptionRef::AllowAll, maybe_accept: None, maybe_cache_setting: Some(&file_fetcher.cache_setting), }, @@ -1264,9 +1296,7 @@ mod tests { resolve_url("http://localhost:4550/subdir/redirects/redirect1.js") .unwrap(); - let result = file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap(); assert_eq!(file.specifier, redirected_specifier); @@ -1310,9 +1340,7 @@ mod tests { let specifier = resolve_url("http://localhost:4545/run/002_hello.ts").unwrap(); - let result = file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_err()); let err = result.unwrap_err(); assert_eq!(get_custom_error_class(&err), Some("NoRemote")); @@ -1343,22 +1371,16 @@ mod tests { let specifier = resolve_url("http://localhost:4545/run/002_hello.ts").unwrap(); - let result = file_fetcher_01 - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher_01.fetch_bypass_permissions(&specifier).await; assert!(result.is_err()); let err = result.unwrap_err(); assert_eq!(err.to_string(), "Specifier not found in cache: \"http://localhost:4545/run/002_hello.ts\", --cached-only is specified."); assert_eq!(get_custom_error_class(&err), Some("NotCached")); - let result = file_fetcher_02 - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher_02.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); - let result = file_fetcher_01 - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher_01.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); } @@ -1368,17 +1390,13 @@ mod tests { let fixture_path = temp_dir.path().join("mod.ts"); let specifier = ModuleSpecifier::from_file_path(&fixture_path).unwrap(); fs::write(fixture_path.clone(), r#"console.log("hello deno");"#).unwrap(); - let result = file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap().into_text_decoded().unwrap(); assert_eq!(&*file.source, r#"console.log("hello deno");"#); fs::write(fixture_path, r#"console.log("goodbye deno");"#).unwrap(); - let result = file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap().into_text_decoded().unwrap(); assert_eq!(&*file.source, r#"console.log("goodbye deno");"#); @@ -1392,18 +1410,14 @@ mod tests { setup(CacheSetting::RespectHeaders, Some(temp_dir.clone())); let specifier = ModuleSpecifier::parse("http://localhost:4545/dynamic").unwrap(); - let result = file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap(); let first = file.source; let (file_fetcher, _) = setup(CacheSetting::RespectHeaders, Some(temp_dir.clone())); - let result = file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap(); let second = file.source; @@ -1419,18 +1433,14 @@ mod tests { setup(CacheSetting::RespectHeaders, Some(temp_dir.clone())); let specifier = ModuleSpecifier::parse("http://localhost:4545/dynamic_cache").unwrap(); - let result = file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap(); let first = file.source; let (file_fetcher, _) = setup(CacheSetting::RespectHeaders, Some(temp_dir.clone())); - let result = file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await; + let result = file_fetcher.fetch_bypass_permissions(&specifier).await; assert!(result.is_ok()); let file = result.unwrap(); let second = file.source; diff --git a/cli/graph_container.rs b/cli/graph_container.rs index 9f71045c69..9f049946f2 100644 --- a/cli/graph_container.rs +++ b/cli/graph_container.rs @@ -9,9 +9,9 @@ use deno_core::error::AnyError; use deno_core::parking_lot::RwLock; use deno_graph::ModuleGraph; use deno_runtime::colors; -use deno_runtime::deno_permissions::PermissionsContainer; use crate::args::CliOptions; +use crate::file_fetcher::FetchPermissionsOption; use crate::module_loader::ModuleLoadPreparer; use crate::util::fs::collect_specifiers; use crate::util::path::is_script_ext; @@ -75,7 +75,7 @@ impl MainModuleGraphContainer { specifiers, false, self.cli_options.ts_type_lib_window(), - PermissionsContainer::allow_all(), + FetchPermissionsOption::AllowAll, ) .await?; graph_permit.commit(); diff --git a/cli/graph_util.rs b/cli/graph_util.rs index d73733123f..cb2e1bde87 100644 --- a/cli/graph_util.rs +++ b/cli/graph_util.rs @@ -11,6 +11,7 @@ use crate::cache::ModuleInfoCache; use crate::cache::ParsedSourceCache; use crate::colors; use crate::errors::get_error_class_name; +use crate::file_fetcher::FetchPermissionsOption; use crate::file_fetcher::FileFetcher; use crate::npm::CliNpmResolver; use crate::resolver::CliGraphResolver; @@ -41,7 +42,6 @@ use deno_graph::ResolutionError; use deno_graph::SpecifierError; use deno_runtime::deno_fs::FileSystem; use deno_runtime::deno_node; -use deno_runtime::deno_permissions::PermissionsContainer; use deno_semver::jsr::JsrDepPackageReq; use deno_semver::package::PackageNv; use deno_semver::Version; @@ -670,12 +670,12 @@ impl ModuleGraphBuilder { /// Creates the default loader used for creating a graph. pub fn create_graph_loader(&self) -> cache::FetchCacher { - self.create_fetch_cacher(PermissionsContainer::allow_all()) + self.create_fetch_cacher(FetchPermissionsOption::AllowAll) } pub fn create_fetch_cacher( &self, - permissions: PermissionsContainer, + permissions: FetchPermissionsOption, ) -> cache::FetchCacher { cache::FetchCacher::new( self.file_fetcher.clone(), diff --git a/cli/jsr.rs b/cli/jsr.rs index 87a54af22b..767d304d60 100644 --- a/cli/jsr.rs +++ b/cli/jsr.rs @@ -6,7 +6,6 @@ use dashmap::DashMap; use deno_core::serde_json; use deno_graph::packages::JsrPackageInfo; use deno_graph::packages::JsrPackageVersionInfo; -use deno_runtime::deno_permissions::PermissionsContainer; use deno_semver::package::PackageNv; use deno_semver::package::PackageReq; use std::sync::Arc; @@ -68,10 +67,7 @@ impl JsrFetchResolver { let file_fetcher = self.file_fetcher.clone(); // spawn due to the lsp's `Send` requirement let file = deno_core::unsync::spawn(async move { - file_fetcher - .fetch(&meta_url, &PermissionsContainer::allow_all()) - .await - .ok() + file_fetcher.fetch_bypass_permissions(&meta_url).await.ok() }) .await .ok()??; @@ -96,10 +92,7 @@ impl JsrFetchResolver { let file_fetcher = self.file_fetcher.clone(); // spawn due to the lsp's `Send` requirement let file = deno_core::unsync::spawn(async move { - file_fetcher - .fetch(&meta_url, &PermissionsContainer::allow_all()) - .await - .ok() + file_fetcher.fetch_bypass_permissions(&meta_url).await.ok() }) .await .ok()??; diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index c5d0642855..f69cae4359 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -37,7 +37,6 @@ use deno_lint::linter::LintConfig as DenoLintConfig; use deno_npm::npm_rc::ResolvedNpmRc; use deno_package_json::PackageJsonCache; use deno_runtime::deno_node::PackageJson; -use deno_runtime::deno_permissions::PermissionsContainer; use deno_runtime::fs_util::specifier_to_file_path; use indexmap::IndexSet; use lsp_types::ClientCapabilities; @@ -1509,17 +1508,16 @@ impl ConfigData { ConfigWatchedFileType::ImportMap, ); // spawn due to the lsp's `Send` requirement - let fetch_result = deno_core::unsync::spawn({ - let file_fetcher = file_fetcher.cloned().unwrap(); - let import_map_url = import_map_url.clone(); - async move { - file_fetcher - .fetch(&import_map_url, &PermissionsContainer::allow_all()) - .await - } - }) - .await - .unwrap(); + let fetch_result = + deno_core::unsync::spawn({ + let file_fetcher = file_fetcher.cloned().unwrap(); + let import_map_url = import_map_url.clone(); + async move { + file_fetcher.fetch_bypass_permissions(&import_map_url).await + } + }) + .await + .unwrap(); let value_result = fetch_result.and_then(|f| { serde_json::from_slice::(&f.source).map_err(|e| e.into()) @@ -1558,7 +1556,7 @@ impl ConfigData { let file_fetcher = file_fetcher.clone().unwrap(); async move { let file = file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) + .fetch_bypass_permissions(&specifier) .await? .into_text_decoded()?; Ok(file.source.to_string()) diff --git a/cli/lsp/jsr.rs b/cli/lsp/jsr.rs index 6c591637c8..9a738ec287 100644 --- a/cli/lsp/jsr.rs +++ b/cli/lsp/jsr.rs @@ -14,7 +14,6 @@ use deno_graph::packages::JsrPackageInfo; use deno_graph::packages::JsrPackageInfoVersion; use deno_graph::packages::JsrPackageVersionInfo; use deno_graph::ModuleSpecifier; -use deno_runtime::deno_permissions::PermissionsContainer; use deno_semver::jsr::JsrPackageReqReference; use deno_semver::package::PackageNv; use deno_semver::package::PackageReq; @@ -311,7 +310,7 @@ impl PackageSearchApi for CliJsrSearchApi { // spawn due to the lsp's `Send` requirement let file = deno_core::unsync::spawn(async move { file_fetcher - .fetch(&search_url, &PermissionsContainer::allow_all()) + .fetch_bypass_permissions(&search_url) .await? .into_text_decoded() }) diff --git a/cli/lsp/npm.rs b/cli/lsp/npm.rs index e5aa337440..8bdeb7e7d8 100644 --- a/cli/lsp/npm.rs +++ b/cli/lsp/npm.rs @@ -4,7 +4,6 @@ use dashmap::DashMap; use deno_core::anyhow::anyhow; use deno_core::error::AnyError; use deno_core::serde_json; -use deno_runtime::deno_permissions::PermissionsContainer; use deno_semver::package::PackageNv; use deno_semver::Version; use serde::Deserialize; @@ -55,7 +54,7 @@ impl PackageSearchApi for CliNpmSearchApi { let file_fetcher = self.file_fetcher.clone(); let file = deno_core::unsync::spawn(async move { file_fetcher - .fetch(&search_url, &PermissionsContainer::allow_all()) + .fetch_bypass_permissions(&search_url) .await? .into_text_decoded() }) diff --git a/cli/lsp/registries.rs b/cli/lsp/registries.rs index c709831d42..5f7ce00823 100644 --- a/cli/lsp/registries.rs +++ b/cli/lsp/registries.rs @@ -16,6 +16,7 @@ use crate::args::CacheSetting; use crate::cache::GlobalHttpCache; use crate::cache::HttpCache; use crate::file_fetcher::FetchOptions; +use crate::file_fetcher::FetchPermissionsOptionRef; use crate::file_fetcher::FileFetcher; use crate::http_util::HttpClientProvider; @@ -30,7 +31,6 @@ use deno_core::url::Position; use deno_core::url::Url; use deno_core::ModuleSpecifier; use deno_graph::Dependency; -use deno_runtime::deno_permissions::PermissionsContainer; use log::error; use once_cell::sync::Lazy; use std::borrow::Cow; @@ -481,7 +481,7 @@ impl ModuleRegistry { file_fetcher .fetch_with_options(FetchOptions { specifier: &specifier, - permissions: &PermissionsContainer::allow_all(), + permissions: FetchPermissionsOptionRef::AllowAll, maybe_accept: Some("application/vnd.deno.reg.v2+json, application/vnd.deno.reg.v1+json;q=0.9, application/json;q=0.8"), maybe_cache_setting: None, }) @@ -584,7 +584,7 @@ impl ModuleRegistry { let file = deno_core::unsync::spawn({ async move { file_fetcher - .fetch(&endpoint, &PermissionsContainer::allow_all()) + .fetch_bypass_permissions(&endpoint) .await .ok()? .into_text_decoded() @@ -983,7 +983,7 @@ impl ModuleRegistry { // spawn due to the lsp's `Send` requirement let file = deno_core::unsync::spawn(async move { file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) + .fetch_bypass_permissions(&specifier) .await .ok()? .into_text_decoded() @@ -1049,7 +1049,7 @@ impl ModuleRegistry { let specifier = specifier.clone(); async move { file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) + .fetch_bypass_permissions(&specifier) .await .map_err(|err| { error!( @@ -1095,7 +1095,7 @@ impl ModuleRegistry { let specifier = specifier.clone(); async move { file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) + .fetch_bypass_permissions(&specifier) .await .map_err(|err| { error!( diff --git a/cli/lsp/testing/execution.rs b/cli/lsp/testing/execution.rs index c2398d4ff2..a8cea8dd44 100644 --- a/cli/lsp/testing/execution.rs +++ b/cli/lsp/testing/execution.rs @@ -31,6 +31,7 @@ use deno_core::unsync::spawn; use deno_core::unsync::spawn_blocking; use deno_core::ModuleSpecifier; use deno_runtime::deno_permissions::Permissions; +use deno_runtime::deno_permissions::PermissionsContainer; use deno_runtime::tokio_util::create_and_run_current_thread; use indexmap::IndexMap; use std::borrow::Cow; @@ -227,8 +228,11 @@ impl TestRun { // Various test files should not share the same permissions in terms of // `PermissionsContainer` - otherwise granting/revoking permissions in one // file would have impact on other files, which is undesirable. - let permissions = - Permissions::from_options(&cli_options.permissions_options()?)?; + let permission_desc_parser = factory.permission_desc_parser()?.clone(); + let permissions = Permissions::from_options( + permission_desc_parser.as_ref(), + &cli_options.permissions_options(), + )?; let main_graph_container = factory.main_module_graph_container().await?; test::check_specifiers( factory.file_fetcher()?, @@ -276,7 +280,10 @@ impl TestRun { let join_handles = queue.into_iter().map(move |specifier| { let specifier = specifier.clone(); let worker_factory = worker_factory.clone(); - let permissions = permissions.clone(); + let permissions_container = PermissionsContainer::new( + permission_desc_parser.clone(), + permissions.clone(), + ); let worker_sender = test_event_sender_factory.worker(); let fail_fast_tracker = fail_fast_tracker.clone(); let lsp_filter = self.filters.get(&specifier); @@ -305,7 +312,7 @@ impl TestRun { // channel. create_and_run_current_thread(test::test_specifier( worker_factory, - permissions, + permissions_container, specifier, worker_sender, fail_fast_tracker, diff --git a/cli/module_loader.rs b/cli/module_loader.rs index a35e60090c..86824cd701 100644 --- a/cli/module_loader.rs +++ b/cli/module_loader.rs @@ -104,7 +104,7 @@ impl ModuleLoadPreparer { roots: &[ModuleSpecifier], is_dynamic: bool, lib: TsTypeLib, - permissions: PermissionsContainer, + permissions: crate::file_fetcher::FetchPermissionsOption, ) -> Result<(), AnyError> { log::debug!("Preparing module load."); let _pb_clear_guard = self.progress_bar.clear_guard(); @@ -762,7 +762,7 @@ impl ModuleLoader &[specifier], is_dynamic, lib, - root_permissions, + root_permissions.into(), ) .await?; update_permit.commit(); diff --git a/cli/npm/byonm.rs b/cli/npm/byonm.rs index 3249b2ed19..24645e4164 100644 --- a/cli/npm/byonm.rs +++ b/cli/npm/byonm.rs @@ -280,7 +280,7 @@ impl NodeRequireResolver for ByonmCliNpmResolver { .components() .any(|c| c.as_os_str().to_ascii_lowercase() == "node_modules") { - permissions.check_read(path)?; + _ = permissions.check_read_path(path)?; } Ok(()) } diff --git a/cli/npm/managed/resolvers/common.rs b/cli/npm/managed/resolvers/common.rs index 170dc2ae6b..1893aa56ae 100644 --- a/cli/npm/managed/resolvers/common.rs +++ b/cli/npm/managed/resolvers/common.rs @@ -127,7 +127,8 @@ impl RegistryReadPermissionChecker { } } - permissions.check_read(path) + _ = permissions.check_read_path(path)?; + Ok(()) } } diff --git a/cli/npm/mod.rs b/cli/npm/mod.rs index c8f87e6aff..bedde64551 100644 --- a/cli/npm/mod.rs +++ b/cli/npm/mod.rs @@ -15,7 +15,6 @@ use deno_core::serde_json; use deno_npm::registry::NpmPackageInfo; use deno_runtime::deno_node::NodeRequireResolver; use deno_runtime::deno_node::NpmProcessStateProvider; -use deno_runtime::deno_permissions::PermissionsContainer; use deno_semver::package::PackageNv; use deno_semver::package::PackageReq; use node_resolver::NpmResolver; @@ -152,10 +151,7 @@ impl NpmFetchResolver { let file_fetcher = self.file_fetcher.clone(); // spawn due to the lsp's `Send` requirement let file = deno_core::unsync::spawn(async move { - file_fetcher - .fetch(&info_url, &PermissionsContainer::allow_all()) - .await - .ok() + file_fetcher.fetch_bypass_permissions(&info_url).await.ok() }) .await .ok()??; diff --git a/cli/ops/bench.rs b/cli/ops/bench.rs index 5521253ffd..edd8c118ca 100644 --- a/cli/ops/bench.rs +++ b/cli/ops/bench.rs @@ -2,6 +2,7 @@ use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; +use std::sync::Arc; use std::time; use deno_core::error::generic_error; @@ -13,6 +14,7 @@ use deno_core::ModuleSpecifier; use deno_core::OpState; use deno_runtime::deno_permissions::create_child_permissions; use deno_runtime::deno_permissions::ChildPermissionsArg; +use deno_runtime::deno_permissions::PermissionDescriptorParser; use deno_runtime::deno_permissions::PermissionsContainer; use tokio::sync::mpsc::UnboundedSender; use uuid::Uuid; @@ -59,11 +61,18 @@ pub fn op_pledge_test_permissions( #[serde] args: ChildPermissionsArg, ) -> Result { let token = Uuid::new_v4(); + let permission_desc_parser = state + .borrow::>() + .clone(); let parent_permissions = state.borrow_mut::(); let worker_permissions = { - let mut parent_permissions = parent_permissions.0.lock(); - let perms = create_child_permissions(&mut parent_permissions, args)?; - PermissionsContainer::new(perms) + let mut parent_permissions = parent_permissions.inner.lock(); + let perms = create_child_permissions( + permission_desc_parser.as_ref(), + &mut parent_permissions, + args, + )?; + PermissionsContainer::new(permission_desc_parser, perms) }; let parent_permissions = parent_permissions.clone(); @@ -74,7 +83,7 @@ pub fn op_pledge_test_permissions( state.put::(PermissionsHolder(token, parent_permissions)); // NOTE: This call overrides current permission set for the worker - state.put(worker_permissions.0.clone()); + state.put(worker_permissions.inner.clone()); state.put::(worker_permissions); Ok(token) @@ -91,7 +100,7 @@ pub fn op_restore_test_permissions( } let permissions = permissions_holder.1; - state.put(permissions.0.clone()); + state.put(permissions.inner.clone()); state.put::(permissions); Ok(()) } else { diff --git a/cli/ops/testing.rs b/cli/ops/testing.rs index b8839a6f8b..6a8d310062 100644 --- a/cli/ops/testing.rs +++ b/cli/ops/testing.rs @@ -18,9 +18,11 @@ use deno_core::ModuleSpecifier; use deno_core::OpState; use deno_runtime::deno_permissions::create_child_permissions; use deno_runtime::deno_permissions::ChildPermissionsArg; +use deno_runtime::deno_permissions::PermissionDescriptorParser; use deno_runtime::deno_permissions::PermissionsContainer; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; +use std::sync::Arc; use uuid::Uuid; deno_core::extension!(deno_test, @@ -54,11 +56,18 @@ pub fn op_pledge_test_permissions( #[serde] args: ChildPermissionsArg, ) -> Result { let token = Uuid::new_v4(); + let permission_desc_parser = state + .borrow::>() + .clone(); let parent_permissions = state.borrow_mut::(); let worker_permissions = { - let mut parent_permissions = parent_permissions.0.lock(); - let perms = create_child_permissions(&mut parent_permissions, args)?; - PermissionsContainer::new(perms) + let mut parent_permissions = parent_permissions.inner.lock(); + let perms = create_child_permissions( + permission_desc_parser.as_ref(), + &mut parent_permissions, + args, + )?; + PermissionsContainer::new(permission_desc_parser, perms) }; let parent_permissions = parent_permissions.clone(); @@ -68,7 +77,7 @@ pub fn op_pledge_test_permissions( state.put::(PermissionsHolder(token, parent_permissions)); // NOTE: This call overrides current permission set for the worker - state.put(worker_permissions.0.clone()); + state.put(worker_permissions.inner.clone()); state.put::(worker_permissions); Ok(token) @@ -85,7 +94,7 @@ pub fn op_restore_test_permissions( } let permissions = permissions_holder.1; - state.put(permissions.0.clone()); + state.put(permissions.inner.clone()); state.put::(permissions); Ok(()) } else { diff --git a/cli/standalone/mod.rs b/cli/standalone/mod.rs index 0a08296d96..40968a8c44 100644 --- a/cli/standalone/mod.rs +++ b/cli/standalone/mod.rs @@ -32,6 +32,8 @@ use deno_runtime::deno_permissions::Permissions; use deno_runtime::deno_permissions::PermissionsContainer; use deno_runtime::deno_tls::rustls::RootCertStore; use deno_runtime::deno_tls::RootCertStoreProvider; +use deno_runtime::deno_web::BlobStore; +use deno_runtime::permissions::RuntimePermissionDescriptorParser; use deno_runtime::WorkerExecutionMode; use deno_runtime::WorkerLogLevel; use deno_semver::npm::NpmPackageReqReference; @@ -449,7 +451,6 @@ pub async fn run( let current_exe_path = std::env::current_exe().unwrap(); let current_exe_name = current_exe_path.file_name().unwrap().to_string_lossy(); - let maybe_cwd = std::env::current_dir().ok(); let deno_dir_provider = Arc::new(DenoDirProvider::new(None)); let root_cert_store_provider = Arc::new(StandaloneRootCertStoreProvider { ca_stores: metadata.ca_stores, @@ -660,8 +661,7 @@ pub async fn run( }; let permissions = { - let mut permissions = - metadata.permissions.to_options(maybe_cwd.as_deref())?; + let mut permissions = metadata.permissions.to_options(); // if running with an npm vfs, grant read access to it if let Some(vfs_root) = maybe_vfs_root { match &mut permissions.allow_read { @@ -669,15 +669,20 @@ pub async fn run( // do nothing, already granted } Some(vec) => { - vec.push(vfs_root); + vec.push(vfs_root.to_string_lossy().to_string()); } None => { - permissions.allow_read = Some(vec![vfs_root]); + permissions.allow_read = + Some(vec![vfs_root.to_string_lossy().to_string()]); } } } - PermissionsContainer::new(Permissions::from_options(&permissions)?) + let desc_parser = + Arc::new(RuntimePermissionDescriptorParser::new(fs.clone())); + let permissions = + Permissions::from_options(desc_parser.as_ref(), &permissions)?; + PermissionsContainer::new(desc_parser, permissions) }; let feature_checker = Arc::new({ let mut checker = FeatureChecker::default(); @@ -689,21 +694,24 @@ pub async fn run( } checker }); + let permission_desc_parser = + Arc::new(RuntimePermissionDescriptorParser::new(fs.clone())); let worker_factory = CliMainWorkerFactory::new( - StorageKeyResolver::empty(), - crate::args::DenoSubcommand::Run(Default::default()), - npm_resolver, - node_resolver, - Default::default(), - Box::new(module_loader_factory), - root_cert_store_provider, + Arc::new(BlobStore::default()), + // Code cache is not supported for standalone binary yet. + None, + feature_checker, fs, None, None, None, - feature_checker, - // Code cache is not supported for standalone binary yet. - None, + Box::new(module_loader_factory), + node_resolver, + npm_resolver, + permission_desc_parser, + root_cert_store_provider, + StorageKeyResolver::empty(), + crate::args::DenoSubcommand::Run(Default::default()), CliMainWorkerOptions { argv: metadata.argv, log_level: WorkerLogLevel::Info, diff --git a/cli/tools/bench/mod.rs b/cli/tools/bench/mod.rs index 44ae8321d2..f133759c9f 100644 --- a/cli/tools/bench/mod.rs +++ b/cli/tools/bench/mod.rs @@ -30,6 +30,7 @@ use deno_core::ModuleSpecifier; use deno_core::PollEventLoopOptions; use deno_runtime::deno_permissions::Permissions; use deno_runtime::deno_permissions::PermissionsContainer; +use deno_runtime::permissions::RuntimePermissionDescriptorParser; use deno_runtime::tokio_util::create_and_run_current_thread; use deno_runtime::WorkerExecutionMode; use indexmap::IndexMap; @@ -144,14 +145,14 @@ fn create_reporter( /// Run a single specifier as an executable bench module. async fn bench_specifier( worker_factory: Arc, - permissions: Permissions, + permissions_container: PermissionsContainer, specifier: ModuleSpecifier, sender: UnboundedSender, filter: TestFilter, ) -> Result<(), AnyError> { match bench_specifier_inner( worker_factory, - permissions, + permissions_container, specifier.clone(), &sender, filter, @@ -176,7 +177,7 @@ async fn bench_specifier( /// Run a single specifier as an executable bench module. async fn bench_specifier_inner( worker_factory: Arc, - permissions: Permissions, + permissions_container: PermissionsContainer, specifier: ModuleSpecifier, sender: &UnboundedSender, filter: TestFilter, @@ -185,7 +186,7 @@ async fn bench_specifier_inner( .create_custom_worker( WorkerExecutionMode::Bench, specifier.clone(), - PermissionsContainer::new(permissions), + permissions_container, vec![ops::bench::deno_bench::init_ops(sender.clone())], Default::default(), ) @@ -264,6 +265,7 @@ async fn bench_specifier_inner( async fn bench_specifiers( worker_factory: Arc, permissions: &Permissions, + permissions_desc_parser: &Arc, specifiers: Vec, options: BenchSpecifierOptions, ) -> Result<(), AnyError> { @@ -273,13 +275,16 @@ async fn bench_specifiers( let join_handles = specifiers.into_iter().map(move |specifier| { let worker_factory = worker_factory.clone(); - let permissions = permissions.clone(); + let permissions_container = PermissionsContainer::new( + permissions_desc_parser.clone(), + permissions.clone(), + ); let sender = sender.clone(); let options = option_for_handles.clone(); spawn_blocking(move || { let future = bench_specifier( worker_factory, - permissions, + permissions_container, specifier, sender, options.filter, @@ -410,8 +415,11 @@ pub async fn run_benchmarks( // Various bench files should not share the same permissions in terms of // `PermissionsContainer` - otherwise granting/revoking permissions in one // file would have impact on other files, which is undesirable. - let permissions = - Permissions::from_options(&cli_options.permissions_options()?)?; + let permission_desc_parser = factory.permission_desc_parser()?.clone(); + let permissions = Permissions::from_options( + permission_desc_parser.as_ref(), + &cli_options.permissions_options(), + )?; let members_with_bench_options = cli_options.resolve_bench_options_for_members(&bench_flags)?; @@ -446,6 +454,7 @@ pub async fn run_benchmarks( bench_specifiers( worker_factory, &permissions, + &permission_desc_parser, specifiers, BenchSpecifierOptions { filter: TestFilter::from_flag(&workspace_bench_options.filter), @@ -519,8 +528,11 @@ pub async fn run_benchmarks_with_watch( // Various bench files should not share the same permissions in terms of // `PermissionsContainer` - otherwise granting/revoking permissions in one // file would have impact on other files, which is undesirable. - let permissions = - Permissions::from_options(&cli_options.permissions_options()?)?; + let permission_desc_parser = factory.permission_desc_parser()?.clone(); + let permissions = Permissions::from_options( + permission_desc_parser.as_ref(), + &cli_options.permissions_options(), + )?; let graph = module_graph_creator .create_graph(graph_kind, collected_bench_modules.clone()) @@ -568,6 +580,7 @@ pub async fn run_benchmarks_with_watch( bench_specifiers( worker_factory, &permissions, + &permission_desc_parser, specifiers, BenchSpecifierOptions { filter: TestFilter::from_flag(&workspace_bench_options.filter), diff --git a/cli/tools/jupyter/mod.rs b/cli/tools/jupyter/mod.rs index 14fcbd72c4..71e947dddb 100644 --- a/cli/tools/jupyter/mod.rs +++ b/cli/tools/jupyter/mod.rs @@ -25,7 +25,6 @@ use deno_core::serde_json::json; use deno_core::url::Url; use deno_runtime::deno_io::Stdio; use deno_runtime::deno_io::StdioPipe; -use deno_runtime::deno_permissions::Permissions; use deno_runtime::deno_permissions::PermissionsContainer; use deno_runtime::WorkerExecutionMode; use deno_terminal::colors; @@ -65,7 +64,8 @@ pub async fn kernel( resolve_url_or_path("./$deno$jupyter.ts", cli_options.initial_cwd()) .unwrap(); // TODO(bartlomieju): should we run with all permissions? - let permissions = PermissionsContainer::new(Permissions::allow_all()); + let permissions = + PermissionsContainer::allow_all(factory.permission_desc_parser()?.clone()); let npm_resolver = factory.npm_resolver().await?.clone(); let resolver = factory.resolver().await?.clone(); let worker_factory = factory.create_cli_main_worker_factory().await?; diff --git a/cli/tools/registry/pm/cache_deps.rs b/cli/tools/registry/pm/cache_deps.rs index d292c32f58..a03c30df85 100644 --- a/cli/tools/registry/pm/cache_deps.rs +++ b/cli/tools/registry/pm/cache_deps.rs @@ -106,7 +106,7 @@ pub async fn cache_top_level_deps( &roots, false, deno_config::deno_json::TsTypeLib::DenoWorker, - deno_runtime::deno_permissions::PermissionsContainer::allow_all(), + crate::file_fetcher::FetchPermissionsOption::AllowAll, ) .await?; } diff --git a/cli/tools/repl/mod.rs b/cli/tools/repl/mod.rs index ed3d94c846..24bc8e30a5 100644 --- a/cli/tools/repl/mod.rs +++ b/cli/tools/repl/mod.rs @@ -16,8 +16,6 @@ use deno_core::error::AnyError; use deno_core::futures::StreamExt; use deno_core::serde_json; use deno_core::unsync::spawn_blocking; -use deno_runtime::deno_permissions::Permissions; -use deno_runtime::deno_permissions::PermissionsContainer; use deno_runtime::WorkerExecutionMode; use rustyline::error::ReadlineError; @@ -151,9 +149,7 @@ async fn read_eval_file( let specifier = deno_core::resolve_url_or_path(eval_file, cli_options.initial_cwd())?; - let file = file_fetcher - .fetch(&specifier, &PermissionsContainer::allow_all()) - .await?; + let file = file_fetcher.fetch_bypass_permissions(&specifier).await?; Ok(file.into_text_decoded()?.source) } @@ -166,9 +162,7 @@ pub async fn run( let factory = CliFactory::from_flags(flags); let cli_options = factory.cli_options()?; let main_module = cli_options.resolve_main_module()?; - let permissions = PermissionsContainer::new(Permissions::from_options( - &cli_options.permissions_options()?, - )?); + let permissions = factory.create_permissions_container()?; let npm_resolver = factory.npm_resolver().await?.clone(); let resolver = factory.resolver().await?.clone(); let file_fetcher = factory.file_fetcher()?; diff --git a/cli/tools/run/mod.rs b/cli/tools/run/mod.rs index bdafdae88a..2006444904 100644 --- a/cli/tools/run/mod.rs +++ b/cli/tools/run/mod.rs @@ -5,8 +5,6 @@ use std::sync::Arc; use deno_config::deno_json::NodeModulesDirMode; use deno_core::error::AnyError; -use deno_runtime::deno_permissions::Permissions; -use deno_runtime::deno_permissions::PermissionsContainer; use deno_runtime::WorkerExecutionMode; use crate::args::EvalFlags; @@ -62,9 +60,7 @@ pub async fn run_script( maybe_npm_install(&factory).await?; - let permissions = PermissionsContainer::new(Permissions::from_options( - &cli_options.permissions_options()?, - )?); + let permissions = factory.create_permissions_container()?; let worker_factory = factory.create_cli_main_worker_factory().await?; let mut worker = worker_factory .create_main_worker(mode, main_module, permissions) @@ -83,9 +79,7 @@ pub async fn run_from_stdin(flags: Arc) -> Result { let file_fetcher = factory.file_fetcher()?; let worker_factory = factory.create_cli_main_worker_factory().await?; - let permissions = PermissionsContainer::new(Permissions::from_options( - &cli_options.permissions_options()?, - )?); + let permissions = factory.create_permissions_container()?; let mut source = Vec::new(); std::io::stdin().read_to_end(&mut source)?; // Save a fake file into file fetcher cache @@ -131,9 +125,7 @@ async fn run_with_watch( let _ = watcher_communicator.watch_paths(cli_options.watch_paths()); - let permissions = PermissionsContainer::new(Permissions::from_options( - &cli_options.permissions_options()?, - )?); + let permissions = factory.create_permissions_container()?; let mut worker = factory .create_cli_main_worker_factory() .await? @@ -181,9 +173,7 @@ pub async fn eval_command( source: source_code.into_bytes().into(), }); - let permissions = PermissionsContainer::new(Permissions::from_options( - &cli_options.permissions_options()?, - )?); + let permissions = factory.create_permissions_container()?; let worker_factory = factory.create_cli_main_worker_factory().await?; let mut worker = worker_factory .create_main_worker(WorkerExecutionMode::Eval, main_module, permissions) diff --git a/cli/tools/serve.rs b/cli/tools/serve.rs index 24666b8f64..2f553cf1ed 100644 --- a/cli/tools/serve.rs +++ b/cli/tools/serve.rs @@ -5,7 +5,6 @@ use std::sync::Arc; use deno_core::error::AnyError; use deno_core::futures::TryFutureExt; use deno_core::ModuleSpecifier; -use deno_runtime::deno_permissions::Permissions; use deno_runtime::deno_permissions::PermissionsContainer; use super::run::check_permission_before_script; @@ -45,9 +44,7 @@ pub async fn serve( maybe_npm_install(&factory).await?; - let permissions = PermissionsContainer::new(Permissions::from_options( - &cli_options.permissions_options()?, - )?); + let permissions = factory.create_permissions_container()?; let worker_factory = factory.create_cli_main_worker_factory().await?; do_serve( @@ -175,9 +172,7 @@ async fn serve_with_watch( let _ = watcher_communicator.watch_paths(cli_options.watch_paths()); - let permissions = PermissionsContainer::new(Permissions::from_options( - &cli_options.permissions_options()?, - )?); + let permissions = factory.create_permissions_container()?; let worker_factory = factory.create_cli_main_worker_factory().await?; do_serve(worker_factory, main_module, permissions, worker_count, hmr) diff --git a/cli/tools/test/mod.rs b/cli/tools/test/mod.rs index 7b172cf879..63382ffc67 100644 --- a/cli/tools/test/mod.rs +++ b/cli/tools/test/mod.rs @@ -56,6 +56,7 @@ use deno_runtime::deno_io::StdioPipe; use deno_runtime::deno_permissions::Permissions; use deno_runtime::deno_permissions::PermissionsContainer; use deno_runtime::fmt_errors::format_js_error; +use deno_runtime::permissions::RuntimePermissionDescriptorParser; use deno_runtime::tokio_util::create_and_run_current_thread; use deno_runtime::worker::MainWorker; use deno_runtime::WorkerExecutionMode; @@ -595,7 +596,7 @@ fn get_test_reporter(options: &TestSpecifiersOptions) -> Box { async fn configure_main_worker( worker_factory: Arc, specifier: &Url, - permissions: Permissions, + permissions_container: PermissionsContainer, worker_sender: TestEventWorkerSender, options: &TestSpecifierOptions, ) -> Result<(Option>, MainWorker), anyhow::Error> { @@ -603,7 +604,7 @@ async fn configure_main_worker( .create_custom_worker( WorkerExecutionMode::Test, specifier.clone(), - PermissionsContainer::new(permissions), + permissions_container, vec![ops::testing::deno_test::init_ops(worker_sender.sender)], Stdio { stdin: StdioPipe::inherit(), @@ -646,7 +647,7 @@ async fn configure_main_worker( /// both. pub async fn test_specifier( worker_factory: Arc, - permissions: Permissions, + permissions_container: PermissionsContainer, specifier: ModuleSpecifier, worker_sender: TestEventWorkerSender, fail_fast_tracker: FailFastTracker, @@ -658,7 +659,7 @@ pub async fn test_specifier( let (coverage_collector, mut worker) = configure_main_worker( worker_factory, &specifier, - permissions, + permissions_container, worker_sender, &options, ) @@ -1327,9 +1328,8 @@ async fn fetch_inline_files( ) -> Result, AnyError> { let mut files = Vec::new(); for specifier in specifiers { - let fetch_permissions = PermissionsContainer::allow_all(); let file = file_fetcher - .fetch(&specifier, &fetch_permissions) + .fetch_bypass_permissions(&specifier) .await? .into_text_decoded()?; @@ -1407,6 +1407,7 @@ static HAS_TEST_RUN_SIGINT_HANDLER: AtomicBool = AtomicBool::new(false); async fn test_specifiers( worker_factory: Arc, permissions: &Permissions, + permission_desc_parser: &Arc, specifiers: Vec, options: TestSpecifiersOptions, ) -> Result<(), AnyError> { @@ -1434,14 +1435,17 @@ async fn test_specifiers( let join_handles = specifiers.into_iter().map(move |specifier| { let worker_factory = worker_factory.clone(); - let permissions = permissions.clone(); + let permissions_container = PermissionsContainer::new( + permission_desc_parser.clone(), + permissions.clone(), + ); let worker_sender = test_event_sender_factory.worker(); let fail_fast_tracker = fail_fast_tracker.clone(); let specifier_options = options.specifier.clone(); spawn_blocking(move || { create_and_run_current_thread(test_specifier( worker_factory, - permissions, + permissions_container, specifier, worker_sender, fail_fast_tracker, @@ -1739,9 +1743,7 @@ async fn fetch_specifiers_with_test_mode( .collect::>(); for (specifier, mode) in &mut specifiers_with_mode { - let file = file_fetcher - .fetch(specifier, &PermissionsContainer::allow_all()) - .await?; + let file = file_fetcher.fetch_bypass_permissions(specifier).await?; let (media_type, _) = file.resolve_media_type_and_charset(); if matches!(media_type, MediaType::Unknown | MediaType::Dts) { @@ -1764,8 +1766,11 @@ pub async fn run_tests( // Various test files should not share the same permissions in terms of // `PermissionsContainer` - otherwise granting/revoking permissions in one // file would have impact on other files, which is undesirable. - let permissions = - Permissions::from_options(&cli_options.permissions_options()?)?; + let permission_desc_parser = factory.permission_desc_parser()?; + let permissions = Permissions::from_options( + permission_desc_parser.as_ref(), + &cli_options.permissions_options(), + )?; let log_level = cli_options.log_level(); let members_with_test_options = @@ -1802,6 +1807,7 @@ pub async fn run_tests( test_specifiers( worker_factory, &permissions, + permission_desc_parser, specifiers_with_mode .into_iter() .filter_map(|(s, m)| match m { @@ -1914,8 +1920,11 @@ pub async fn run_tests_with_watch( .flatten() .collect::>(); - let permissions = - Permissions::from_options(&cli_options.permissions_options()?)?; + let permission_desc_parser = factory.permission_desc_parser()?; + let permissions = Permissions::from_options( + permission_desc_parser.as_ref(), + &cli_options.permissions_options(), + )?; let graph = module_graph_creator .create_graph(graph_kind, test_modules) .await?; @@ -1969,6 +1978,7 @@ pub async fn run_tests_with_watch( test_specifiers( worker_factory, &permissions, + permission_desc_parser, specifiers_with_mode .into_iter() .filter_map(|(s, m)| match m { diff --git a/cli/worker.rs b/cli/worker.rs index 94884ff825..78753bf223 100644 --- a/cli/worker.rs +++ b/cli/worker.rs @@ -30,6 +30,7 @@ use deno_runtime::deno_web::BlobStore; use deno_runtime::fmt_errors::format_js_error; use deno_runtime::inspector_server::InspectorServer; use deno_runtime::ops::worker_host::CreateWebWorkerCb; +use deno_runtime::permissions::RuntimePermissionDescriptorParser; use deno_runtime::web_worker::WebWorker; use deno_runtime::web_worker::WebWorkerOptions; use deno_runtime::worker::MainWorker; @@ -121,23 +122,24 @@ pub struct CliMainWorkerOptions { } struct SharedWorkerState { - options: CliMainWorkerOptions, - subcommand: DenoSubcommand, - storage_key_resolver: StorageKeyResolver, - npm_resolver: Arc, - node_resolver: Arc, blob_store: Arc, broadcast_channel: InMemoryBroadcastChannel, - shared_array_buffer_store: SharedArrayBufferStore, + code_cache: Option>, compiled_wasm_module_store: CompiledWasmModuleStore, - module_loader_factory: Box, - root_cert_store_provider: Arc, + feature_checker: Arc, fs: Arc, maybe_file_watcher_communicator: Option>, maybe_inspector_server: Option>, maybe_lockfile: Option>, - feature_checker: Arc, - code_cache: Option>, + module_loader_factory: Box, + node_resolver: Arc, + npm_resolver: Arc, + permission_desc_parser: Arc, + root_cert_store_provider: Arc, + shared_array_buffer_store: SharedArrayBufferStore, + storage_key_resolver: StorageKeyResolver, + options: CliMainWorkerOptions, + subcommand: DenoSubcommand, } impl SharedWorkerState { @@ -418,40 +420,42 @@ pub struct CliMainWorkerFactory { impl CliMainWorkerFactory { #[allow(clippy::too_many_arguments)] pub fn new( - storage_key_resolver: StorageKeyResolver, - subcommand: DenoSubcommand, - npm_resolver: Arc, - node_resolver: Arc, blob_store: Arc, - module_loader_factory: Box, - root_cert_store_provider: Arc, + code_cache: Option>, + feature_checker: Arc, fs: Arc, maybe_file_watcher_communicator: Option>, maybe_inspector_server: Option>, maybe_lockfile: Option>, - feature_checker: Arc, - code_cache: Option>, + module_loader_factory: Box, + node_resolver: Arc, + npm_resolver: Arc, + permission_parser: Arc, + root_cert_store_provider: Arc, + storage_key_resolver: StorageKeyResolver, + subcommand: DenoSubcommand, options: CliMainWorkerOptions, ) -> Self { Self { shared: Arc::new(SharedWorkerState { - options, - subcommand, - storage_key_resolver, - npm_resolver, - node_resolver, blob_store, broadcast_channel: Default::default(), - shared_array_buffer_store: Default::default(), + code_cache, compiled_wasm_module_store: Default::default(), - module_loader_factory, - root_cert_store_provider, + feature_checker, fs, maybe_file_watcher_communicator, maybe_inspector_server, maybe_lockfile, - feature_checker, - code_cache, + module_loader_factory, + node_resolver, + npm_resolver, + permission_desc_parser: permission_parser, + root_cert_store_provider, + shared_array_buffer_store: Default::default(), + storage_key_resolver, + options, + subcommand, }), } } @@ -525,9 +529,13 @@ impl CliMainWorkerFactory { (main_module, false) }; - let ModuleLoaderAndSourceMapGetter { module_loader } = shared - .module_loader_factory - .create_for_main(PermissionsContainer::allow_all(), permissions.clone()); + let ModuleLoaderAndSourceMapGetter { module_loader } = + shared.module_loader_factory.create_for_main( + PermissionsContainer::allow_all( + self.shared.permission_desc_parser.clone(), + ), + permissions.clone(), + ); let maybe_inspector_server = shared.maybe_inspector_server.clone(); let create_web_worker_cb = @@ -619,6 +627,7 @@ impl CliMainWorkerFactory { ), stdio, feature_checker, + permission_desc_parser: shared.permission_desc_parser.clone(), skip_op_registration: shared.options.skip_op_registration, v8_code_cache: shared.code_cache.clone(), }; @@ -809,6 +818,7 @@ fn create_web_worker_callback( stdio: stdio.clone(), cache_storage_dir, feature_checker, + permission_desc_parser: shared.permission_desc_parser.clone(), strace_ops: shared.options.strace_ops.clone(), close_on_idle: args.close_on_idle, maybe_worker_metadata: args.maybe_worker_metadata, @@ -830,13 +840,16 @@ fn create_web_worker_callback( mod tests { use super::*; use deno_core::resolve_path; + use deno_fs::RealFs; use deno_runtime::deno_permissions::Permissions; fn create_test_worker() -> MainWorker { let main_module = resolve_path("./hello.js", &std::env::current_dir().unwrap()).unwrap(); - let permissions = - PermissionsContainer::new(Permissions::none_without_prompt()); + let permissions = PermissionsContainer::new( + Arc::new(RuntimePermissionDescriptorParser::new(Arc::new(RealFs))), + Permissions::none_without_prompt(), + ); let options = WorkerOptions { startup_snapshot: crate::js::deno_isolate_init(), diff --git a/ext/fetch/lib.rs b/ext/fetch/lib.rs index 79659771e6..88f3038528 100644 --- a/ext/fetch/lib.rs +++ b/ext/fetch/lib.rs @@ -299,10 +299,15 @@ impl Drop for ResourceToBodyAdapter { pub trait FetchPermissions { fn check_net_url( &mut self, - _url: &Url, + url: &Url, api_name: &str, ) -> Result<(), AnyError>; - fn check_read(&mut self, _p: &Path, api_name: &str) -> Result<(), AnyError>; + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] + fn check_read<'a>( + &mut self, + p: &'a Path, + api_name: &str, + ) -> Result, AnyError>; } impl FetchPermissions for deno_permissions::PermissionsContainer { @@ -316,12 +321,16 @@ impl FetchPermissions for deno_permissions::PermissionsContainer { } #[inline(always)] - fn check_read( + fn check_read<'a>( &mut self, - path: &Path, + path: &'a Path, api_name: &str, - ) -> Result<(), AnyError> { - deno_permissions::PermissionsContainer::check_read(self, path, api_name) + ) -> Result, AnyError> { + deno_permissions::PermissionsContainer::check_read_path( + self, + path, + Some(api_name), + ) } } @@ -359,7 +368,11 @@ where type_error("NetworkError when attempting to fetch resource") })?; let permissions = state.borrow_mut::(); - permissions.check_read(&path, "fetch()")?; + let path = permissions.check_read(&path, "fetch()")?; + let url = match path { + Cow::Owned(path) => Url::from_file_path(path).unwrap(), + Cow::Borrowed(_) => url, + }; if method != Method::GET { return Err(type_error(format!( diff --git a/ext/ffi/call.rs b/ext/ffi/call.rs index 380fc03a13..3572b9e813 100644 --- a/ext/ffi/call.rs +++ b/ext/ffi/call.rs @@ -287,7 +287,7 @@ where { let mut state = state.borrow_mut(); let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; }; let symbol = PtrSymbol::new(pointer, &def)?; @@ -384,7 +384,7 @@ where { let mut state = state.borrow_mut(); let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; }; let symbol = PtrSymbol::new(pointer, &def)?; diff --git a/ext/ffi/callback.rs b/ext/ffi/callback.rs index 7d0114131a..6fa166f52b 100644 --- a/ext/ffi/callback.rs +++ b/ext/ffi/callback.rs @@ -557,7 +557,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; let thread_id: u32 = LOCAL_THREAD_ID.with(|s| { let value = *s.borrow(); diff --git a/ext/ffi/dlfcn.rs b/ext/ffi/dlfcn.rs index 2bae5d2239..10199bf858 100644 --- a/ext/ffi/dlfcn.rs +++ b/ext/ffi/dlfcn.rs @@ -19,7 +19,6 @@ use serde_value::ValueDeserializer; use std::borrow::Cow; use std::collections::HashMap; use std::ffi::c_void; -use std::path::PathBuf; use std::rc::Rc; pub struct DynamicLibraryResource { @@ -121,15 +120,13 @@ pub fn op_ffi_load<'scope, FP>( where FP: FfiPermissions + 'static, { - let path = args.path; - let permissions = state.borrow_mut::(); - permissions.check_partial(Some(&PathBuf::from(&path)))?; + let path = permissions.check_partial_with_path(&args.path)?; let lib = Library::open(&path).map_err(|e| { dlopen2::Error::OpeningLibraryError(std::io::Error::new( std::io::ErrorKind::Other, - format_error(e, path), + format_error(e, &path), )) })?; let mut resource = DynamicLibraryResource { @@ -290,7 +287,10 @@ fn sync_fn_impl<'s>( // `path` is only used on Windows. #[allow(unused_variables)] -pub(crate) fn format_error(e: dlopen2::Error, path: String) -> String { +pub(crate) fn format_error( + e: dlopen2::Error, + path: &std::path::Path, +) -> String { match e { #[cfg(target_os = "windows")] // This calls FormatMessageW with library path @@ -300,7 +300,6 @@ pub(crate) fn format_error(e: dlopen2::Error, path: String) -> String { // // https://github.com/denoland/deno/issues/11632 dlopen2::Error::OpeningLibraryError(e) => { - use std::ffi::OsStr; use std::os::windows::ffi::OsStrExt; use winapi::shared::minwindef::DWORD; use winapi::shared::winerror::ERROR_INSUFFICIENT_BUFFER; @@ -324,7 +323,8 @@ pub(crate) fn format_error(e: dlopen2::Error, path: String) -> String { let mut buf = vec![0; 500]; - let path = OsStr::new(&path) + let path = path + .as_os_str() .encode_wide() .chain(Some(0)) .collect::>(); @@ -384,7 +384,7 @@ mod tests { std::io::Error::from_raw_os_error(0x000000C1), ); assert_eq!( - format_error(err, "foo.dll".to_string()), + format_error(err, &std::path::PathBuf::from("foo.dll")), "foo.dll is not a valid Win32 application.\r\n".to_string(), ); } diff --git a/ext/ffi/lib.rs b/ext/ffi/lib.rs index 59d241c5a2..77ec3c85e3 100644 --- a/ext/ffi/lib.rs +++ b/ext/ffi/lib.rs @@ -5,7 +5,7 @@ use deno_core::error::AnyError; use std::mem::size_of; use std::os::raw::c_char; use std::os::raw::c_short; -use std::path::Path; +use std::path::PathBuf; mod call; mod callback; @@ -41,13 +41,28 @@ const _: () = { pub const UNSTABLE_FEATURE_NAME: &str = "ffi"; pub trait FfiPermissions { - fn check_partial(&mut self, path: Option<&Path>) -> Result<(), AnyError>; + fn check_partial_no_path(&mut self) -> Result<(), AnyError>; + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] + fn check_partial_with_path( + &mut self, + path: &str, + ) -> Result; } impl FfiPermissions for deno_permissions::PermissionsContainer { #[inline(always)] - fn check_partial(&mut self, path: Option<&Path>) -> Result<(), AnyError> { - deno_permissions::PermissionsContainer::check_ffi_partial(self, path) + fn check_partial_no_path(&mut self) -> Result<(), AnyError> { + deno_permissions::PermissionsContainer::check_ffi_partial_no_path(self) + } + + #[inline(always)] + fn check_partial_with_path( + &mut self, + path: &str, + ) -> Result { + deno_permissions::PermissionsContainer::check_ffi_partial_with_path( + self, path, + ) } } diff --git a/ext/ffi/repr.rs b/ext/ffi/repr.rs index f565374755..315e6d53bc 100644 --- a/ext/ffi/repr.rs +++ b/ext/ffi/repr.rs @@ -21,7 +21,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; Ok(ptr_number as *mut c_void) } @@ -36,7 +36,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; Ok(a == b) } @@ -50,7 +50,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; Ok(buf as *mut c_void) } @@ -64,7 +64,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; let Some(buf) = buf.get_backing_store() else { return Ok(0 as _); @@ -85,7 +85,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; if ptr.is_null() { return Err(type_error("Invalid pointer to offset, pointer is null")); @@ -115,7 +115,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; Ok(ptr as usize) } @@ -132,7 +132,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; if ptr.is_null() { return Err(type_error("Invalid ArrayBuffer pointer, pointer is null")); @@ -164,7 +164,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; if src.is_null() { Err(type_error("Invalid ArrayBuffer pointer, pointer is null")) @@ -195,7 +195,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; if ptr.is_null() { return Err(type_error("Invalid CString pointer, pointer is null")); @@ -221,7 +221,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; if ptr.is_null() { return Err(type_error("Invalid bool pointer, pointer is null")); @@ -241,7 +241,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; if ptr.is_null() { return Err(type_error("Invalid u8 pointer, pointer is null")); @@ -263,7 +263,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; if ptr.is_null() { return Err(type_error("Invalid i8 pointer, pointer is null")); @@ -285,7 +285,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; if ptr.is_null() { return Err(type_error("Invalid u16 pointer, pointer is null")); @@ -307,7 +307,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; if ptr.is_null() { return Err(type_error("Invalid i16 pointer, pointer is null")); @@ -329,7 +329,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; if ptr.is_null() { return Err(type_error("Invalid u32 pointer, pointer is null")); @@ -349,7 +349,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; if ptr.is_null() { return Err(type_error("Invalid i32 pointer, pointer is null")); @@ -372,7 +372,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; if ptr.is_null() { return Err(type_error("Invalid u64 pointer, pointer is null")); @@ -398,7 +398,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; if ptr.is_null() { return Err(type_error("Invalid i64 pointer, pointer is null")); @@ -421,7 +421,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; if ptr.is_null() { return Err(type_error("Invalid f32 pointer, pointer is null")); @@ -441,7 +441,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; if ptr.is_null() { return Err(type_error("Invalid f64 pointer, pointer is null")); @@ -461,7 +461,7 @@ where FP: FfiPermissions + 'static, { let permissions = state.borrow_mut::(); - permissions.check_partial(None)?; + permissions.check_partial_no_path()?; if ptr.is_null() { return Err(type_error("Invalid pointer pointer, pointer is null")); diff --git a/ext/fs/lib.rs b/ext/fs/lib.rs index a6f273323a..bd49078b2e 100644 --- a/ext/fs/lib.rs +++ b/ext/fs/lib.rs @@ -24,6 +24,7 @@ use deno_core::error::AnyError; use deno_io::fs::FsError; use std::borrow::Cow; use std::path::Path; +use std::path::PathBuf; pub trait FsPermissions { fn check_open<'a>( @@ -34,8 +35,18 @@ pub trait FsPermissions { path: &'a Path, api_name: &str, ) -> Result, FsError>; - fn check_read(&mut self, path: &Path, api_name: &str) - -> Result<(), AnyError>; + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] + fn check_read( + &mut self, + path: &str, + api_name: &str, + ) -> Result; + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] + fn check_read_path<'a>( + &mut self, + path: &'a Path, + api_name: &str, + ) -> Result, AnyError>; fn check_read_all(&mut self, api_name: &str) -> Result<(), AnyError>; fn check_read_blind( &mut self, @@ -43,16 +54,24 @@ pub trait FsPermissions { display: &str, api_name: &str, ) -> Result<(), AnyError>; + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] fn check_write( &mut self, - path: &Path, + path: &str, api_name: &str, - ) -> Result<(), AnyError>; + ) -> Result; + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] + fn check_write_path<'a>( + &mut self, + path: &'a Path, + api_name: &str, + ) -> Result, AnyError>; + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] fn check_write_partial( &mut self, - path: &Path, + path: &str, api_name: &str, - ) -> Result<(), AnyError>; + ) -> Result; fn check_write_all(&mut self, api_name: &str) -> Result<(), AnyError>; fn check_write_blind( &mut self, @@ -96,25 +115,44 @@ impl FsPermissions for deno_permissions::PermissionsContainer { // If somehow read or write aren't specified, use read let read = read || !write; + let mut path: Cow<'a, Path> = Cow::Borrowed(path); if read { - FsPermissions::check_read(self, path, api_name) + let resolved_path = FsPermissions::check_read_path(self, &path, api_name) .map_err(|_| FsError::NotCapable("read"))?; + if let Cow::Owned(resolved_path) = resolved_path { + path = Cow::Owned(resolved_path); + } } if write { - FsPermissions::check_write(self, path, api_name) - .map_err(|_| FsError::NotCapable("write"))?; + let resolved_path = + FsPermissions::check_write_path(self, &path, api_name) + .map_err(|_| FsError::NotCapable("write"))?; + if let Cow::Owned(resolved_path) = resolved_path { + path = Cow::Owned(resolved_path); + } } - Ok(Cow::Borrowed(path)) + Ok(path) } fn check_read( &mut self, - path: &Path, + path: &str, api_name: &str, - ) -> Result<(), AnyError> { + ) -> Result { deno_permissions::PermissionsContainer::check_read(self, path, api_name) } + fn check_read_path<'a>( + &mut self, + path: &'a Path, + api_name: &str, + ) -> Result, AnyError> { + deno_permissions::PermissionsContainer::check_read_path( + self, + path, + Some(api_name), + ) + } fn check_read_blind( &mut self, path: &Path, @@ -128,17 +166,27 @@ impl FsPermissions for deno_permissions::PermissionsContainer { fn check_write( &mut self, - path: &Path, + path: &str, api_name: &str, - ) -> Result<(), AnyError> { + ) -> Result { deno_permissions::PermissionsContainer::check_write(self, path, api_name) } + fn check_write_path<'a>( + &mut self, + path: &'a Path, + api_name: &str, + ) -> Result, AnyError> { + deno_permissions::PermissionsContainer::check_write_path( + self, path, api_name, + ) + } + fn check_write_partial( &mut self, - path: &Path, + path: &str, api_name: &str, - ) -> Result<(), AnyError> { + ) -> Result { deno_permissions::PermissionsContainer::check_write_partial( self, path, api_name, ) diff --git a/ext/fs/ops.rs b/ext/fs/ops.rs index f25cd944de..150d3b9555 100644 --- a/ext/fs/ops.rs +++ b/ext/fs/ops.rs @@ -5,6 +5,7 @@ use std::io; use std::io::SeekFrom; use std::path::Path; use std::path::PathBuf; +use std::path::StripPrefixError; use std::rc::Rc; use deno_core::anyhow::bail; @@ -105,8 +106,9 @@ pub fn op_fs_chdir

( where P: FsPermissions + 'static, { - let d = PathBuf::from(&directory); - state.borrow_mut::

().check_read(&d, "Deno.chdir()")?; + let d = state + .borrow_mut::

() + .check_read(directory, "Deno.chdir()")?; state .borrow::() .chdir(&d) @@ -188,11 +190,9 @@ pub fn op_fs_mkdir_sync

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - let mode = mode.unwrap_or(0o777) & 0o777; - state + let path = state .borrow_mut::

() .check_write(&path, "Deno.mkdirSync()")?; @@ -213,14 +213,12 @@ pub async fn op_fs_mkdir_async

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - let mode = mode.unwrap_or(0o777) & 0o777; - let fs = { + let (fs, path) = { let mut state = state.borrow_mut(); - state.borrow_mut::

().check_write(&path, "Deno.mkdir()")?; - state.borrow::().clone() + let path = state.borrow_mut::

().check_write(&path, "Deno.mkdir()")?; + (state.borrow::().clone(), path) }; fs.mkdir_async(path.clone(), recursive, mode) @@ -239,8 +237,7 @@ pub fn op_fs_chmod_sync

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - state + let path = state .borrow_mut::

() .check_write(&path, "Deno.chmodSync()")?; let fs = state.borrow::(); @@ -257,11 +254,10 @@ pub async fn op_fs_chmod_async

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - let fs = { + let (fs, path) = { let mut state = state.borrow_mut(); - state.borrow_mut::

().check_write(&path, "Deno.chmod()")?; - state.borrow::().clone() + let path = state.borrow_mut::

().check_write(&path, "Deno.chmod()")?; + (state.borrow::().clone(), path) }; fs.chmod_async(path.clone(), mode) .await @@ -279,8 +275,7 @@ pub fn op_fs_chown_sync

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - state + let path = state .borrow_mut::

() .check_write(&path, "Deno.chownSync()")?; let fs = state.borrow::(); @@ -299,11 +294,10 @@ pub async fn op_fs_chown_async

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - let fs = { + let (fs, path) = { let mut state = state.borrow_mut(); - state.borrow_mut::

().check_write(&path, "Deno.chown()")?; - state.borrow::().clone() + let path = state.borrow_mut::

().check_write(&path, "Deno.chown()")?; + (state.borrow::().clone(), path) }; fs.chown_async(path.clone(), uid, gid) .await @@ -320,11 +314,9 @@ pub fn op_fs_remove_sync

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - - state + let path = state .borrow_mut::

() - .check_write(&path, "Deno.removeSync()")?; + .check_write(path, "Deno.removeSync()")?; let fs = state.borrow::(); fs.remove_sync(&path, recursive) @@ -342,21 +334,19 @@ pub async fn op_fs_remove_async

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - - let fs = { + let (fs, path) = { let mut state = state.borrow_mut(); - if recursive { + let path = if recursive { state .borrow_mut::

() - .check_write(&path, "Deno.remove()")?; + .check_write(&path, "Deno.remove()")? } else { state .borrow_mut::

() - .check_write_partial(&path, "Deno.remove()")?; - } + .check_write_partial(&path, "Deno.remove()")? + }; - state.borrow::().clone() + (state.borrow::().clone(), path) }; fs.remove_async(path.clone(), recursive) @@ -375,12 +365,9 @@ pub fn op_fs_copy_file_sync

( where P: FsPermissions + 'static, { - let from = PathBuf::from(from); - let to = PathBuf::from(to); - let permissions = state.borrow_mut::

(); - permissions.check_read(&from, "Deno.copyFileSync()")?; - permissions.check_write(&to, "Deno.copyFileSync()")?; + let from = permissions.check_read(from, "Deno.copyFileSync()")?; + let to = permissions.check_write(to, "Deno.copyFileSync()")?; let fs = state.borrow::(); fs.copy_file_sync(&from, &to) @@ -398,15 +385,12 @@ pub async fn op_fs_copy_file_async

( where P: FsPermissions + 'static, { - let from = PathBuf::from(from); - let to = PathBuf::from(to); - - let fs = { + let (fs, from, to) = { let mut state = state.borrow_mut(); let permissions = state.borrow_mut::

(); - permissions.check_read(&from, "Deno.copyFile()")?; - permissions.check_write(&to, "Deno.copyFile()")?; - state.borrow::().clone() + let from = permissions.check_read(&from, "Deno.copyFile()")?; + let to = permissions.check_write(&to, "Deno.copyFile()")?; + (state.borrow::().clone(), from, to) }; fs.copy_file_async(from.clone(), to.clone()) @@ -425,8 +409,7 @@ pub fn op_fs_stat_sync

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - state + let path = state .borrow_mut::

() .check_read(&path, "Deno.statSync()")?; let fs = state.borrow::(); @@ -445,12 +428,11 @@ pub async fn op_fs_stat_async

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - let fs = { + let (fs, path) = { let mut state = state.borrow_mut(); let permissions = state.borrow_mut::

(); - permissions.check_read(&path, "Deno.stat()")?; - state.borrow::().clone() + let path = permissions.check_read(&path, "Deno.stat()")?; + (state.borrow::().clone(), path) }; let stat = fs .stat_async(path.clone()) @@ -468,8 +450,7 @@ pub fn op_fs_lstat_sync

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - state + let path = state .borrow_mut::

() .check_read(&path, "Deno.lstatSync()")?; let fs = state.borrow::(); @@ -488,12 +469,11 @@ pub async fn op_fs_lstat_async

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - let fs = { + let (fs, path) = { let mut state = state.borrow_mut(); let permissions = state.borrow_mut::

(); - permissions.check_read(&path, "Deno.lstat()")?; - state.borrow::().clone() + let path = permissions.check_read(&path, "Deno.lstat()")?; + (state.borrow::().clone(), path) }; let stat = fs .lstat_async(path.clone()) @@ -511,11 +491,9 @@ pub fn op_fs_realpath_sync

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - let fs = state.borrow::().clone(); let permissions = state.borrow_mut::

(); - permissions.check_read(&path, "Deno.realPathSync()")?; + let path = permissions.check_read(&path, "Deno.realPathSync()")?; if path.is_relative() { permissions.check_read_blind(&fs.cwd()?, "CWD", "Deno.realPathSync()")?; } @@ -536,18 +514,16 @@ pub async fn op_fs_realpath_async

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - - let fs; - { + let (fs, path) = { let mut state = state.borrow_mut(); - fs = state.borrow::().clone(); + let fs = state.borrow::().clone(); let permissions = state.borrow_mut::

(); - permissions.check_read(&path, "Deno.realPath()")?; + let path = permissions.check_read(&path, "Deno.realPath()")?; if path.is_relative() { permissions.check_read_blind(&fs.cwd()?, "CWD", "Deno.realPath()")?; } - } + (fs, path) + }; let resolved_path = fs .realpath_async(path.clone()) .await @@ -566,9 +542,7 @@ pub fn op_fs_read_dir_sync

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - - state + let path = state .borrow_mut::

() .check_read(&path, "Deno.readDirSync()")?; @@ -587,14 +561,12 @@ pub async fn op_fs_read_dir_async

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - - let fs = { + let (fs, path) = { let mut state = state.borrow_mut(); - state + let path = state .borrow_mut::

() .check_read(&path, "Deno.readDir()")?; - state.borrow::().clone() + (state.borrow::().clone(), path) }; let entries = fs @@ -614,13 +586,10 @@ pub fn op_fs_rename_sync

( where P: FsPermissions + 'static, { - let oldpath = PathBuf::from(oldpath); - let newpath = PathBuf::from(newpath); - let permissions = state.borrow_mut::

(); - permissions.check_read(&oldpath, "Deno.renameSync()")?; - permissions.check_write(&oldpath, "Deno.renameSync()")?; - permissions.check_write(&newpath, "Deno.renameSync()")?; + let _ = permissions.check_read(&oldpath, "Deno.renameSync()")?; + let oldpath = permissions.check_write(&oldpath, "Deno.renameSync()")?; + let newpath = permissions.check_write(&newpath, "Deno.renameSync()")?; let fs = state.borrow::(); fs.rename_sync(&oldpath, &newpath) @@ -638,16 +607,13 @@ pub async fn op_fs_rename_async

( where P: FsPermissions + 'static, { - let oldpath = PathBuf::from(oldpath); - let newpath = PathBuf::from(newpath); - - let fs = { + let (fs, oldpath, newpath) = { let mut state = state.borrow_mut(); let permissions = state.borrow_mut::

(); - permissions.check_read(&oldpath, "Deno.rename()")?; - permissions.check_write(&oldpath, "Deno.rename()")?; - permissions.check_write(&newpath, "Deno.rename()")?; - state.borrow::().clone() + _ = permissions.check_read(&oldpath, "Deno.rename()")?; + let oldpath = permissions.check_write(&oldpath, "Deno.rename()")?; + let newpath = permissions.check_write(&newpath, "Deno.rename()")?; + (state.borrow::().clone(), oldpath, newpath) }; fs.rename_async(oldpath.clone(), newpath.clone()) @@ -666,14 +632,11 @@ pub fn op_fs_link_sync

( where P: FsPermissions + 'static, { - let oldpath = PathBuf::from(oldpath); - let newpath = PathBuf::from(newpath); - let permissions = state.borrow_mut::

(); - permissions.check_read(&oldpath, "Deno.linkSync()")?; - permissions.check_write(&oldpath, "Deno.linkSync()")?; - permissions.check_read(&newpath, "Deno.linkSync()")?; - permissions.check_write(&newpath, "Deno.linkSync()")?; + _ = permissions.check_read(oldpath, "Deno.linkSync()")?; + let oldpath = permissions.check_write(oldpath, "Deno.linkSync()")?; + _ = permissions.check_read(newpath, "Deno.linkSync()")?; + let newpath = permissions.check_write(newpath, "Deno.linkSync()")?; let fs = state.borrow::(); fs.link_sync(&oldpath, &newpath) @@ -691,17 +654,14 @@ pub async fn op_fs_link_async

( where P: FsPermissions + 'static, { - let oldpath = PathBuf::from(&oldpath); - let newpath = PathBuf::from(&newpath); - - let fs = { + let (fs, oldpath, newpath) = { let mut state = state.borrow_mut(); let permissions = state.borrow_mut::

(); - permissions.check_read(&oldpath, "Deno.link()")?; - permissions.check_write(&oldpath, "Deno.link()")?; - permissions.check_read(&newpath, "Deno.link()")?; - permissions.check_write(&newpath, "Deno.link()")?; - state.borrow::().clone() + _ = permissions.check_read(&oldpath, "Deno.link()")?; + let oldpath = permissions.check_write(&oldpath, "Deno.link()")?; + _ = permissions.check_read(&newpath, "Deno.link()")?; + let newpath = permissions.check_write(&newpath, "Deno.link()")?; + (state.borrow::().clone(), oldpath, newpath) }; fs.link_async(oldpath.clone(), newpath.clone()) @@ -772,9 +732,7 @@ pub fn op_fs_read_link_sync

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - - state + let path = state .borrow_mut::

() .check_read(&path, "Deno.readLink()")?; @@ -794,14 +752,12 @@ pub async fn op_fs_read_link_async

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - - let fs = { + let (fs, path) = { let mut state = state.borrow_mut(); - state + let path = state .borrow_mut::

() .check_read(&path, "Deno.readLink()")?; - state.borrow::().clone() + (state.borrow::().clone(), path) }; let target = fs @@ -821,11 +777,9 @@ pub fn op_fs_truncate_sync

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - - state + let path = state .borrow_mut::

() - .check_write(&path, "Deno.truncateSync()")?; + .check_write(path, "Deno.truncateSync()")?; let fs = state.borrow::(); fs.truncate_sync(&path, len) @@ -843,14 +797,12 @@ pub async fn op_fs_truncate_async

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - - let fs = { + let (fs, path) = { let mut state = state.borrow_mut(); - state + let path = state .borrow_mut::

() .check_write(&path, "Deno.truncate()")?; - state.borrow::().clone() + (state.borrow::().clone(), path) }; fs.truncate_async(path.clone(), len) @@ -872,9 +824,7 @@ pub fn op_fs_utime_sync

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - - state.borrow_mut::

().check_write(&path, "Deno.utime()")?; + let path = state.borrow_mut::

().check_write(path, "Deno.utime()")?; let fs = state.borrow::(); fs.utime_sync(&path, atime_secs, atime_nanos, mtime_secs, mtime_nanos) @@ -895,12 +845,10 @@ pub async fn op_fs_utime_async

( where P: FsPermissions + 'static, { - let path = PathBuf::from(path); - - let fs = { + let (fs, path) = { let mut state = state.borrow_mut(); - state.borrow_mut::

().check_write(&path, "Deno.utime()")?; - state.borrow::().clone() + let path = state.borrow_mut::

().check_write(&path, "Deno.utime()")?; + (state.borrow::().clone(), path) }; fs.utime_async( @@ -920,15 +868,18 @@ where #[string] pub fn op_fs_make_temp_dir_sync

( state: &mut OpState, - #[string] dir: Option, + #[string] dir_arg: Option, #[string] prefix: Option, #[string] suffix: Option, ) -> Result where P: FsPermissions + 'static, { - let (dir, fs) = - make_temp_check_sync::

(state, dir, "Deno.makeTempDirSync()")?; + let (dir, fs) = make_temp_check_sync::

( + state, + dir_arg.as_deref(), + "Deno.makeTempDirSync()", + )?; let mut rng = thread_rng(); @@ -936,7 +887,11 @@ where for _ in 0..MAX_TRIES { let path = tmp_name(&mut rng, &dir, prefix.as_deref(), suffix.as_deref())?; match fs.mkdir_sync(&path, false, 0o700) { - Ok(_) => return path_into_string(path.into_os_string()), + Ok(_) => { + // PERMISSIONS: ensure the absolute path is not leaked + let path = strip_dir_prefix(&dir, dir_arg.as_deref(), path)?; + return path_into_string(path.into_os_string()); + } Err(FsError::Io(ref e)) if e.kind() == io::ErrorKind::AlreadyExists => { continue; } @@ -955,14 +910,18 @@ where #[string] pub async fn op_fs_make_temp_dir_async

( state: Rc>, - #[string] dir: Option, + #[string] dir_arg: Option, #[string] prefix: Option, #[string] suffix: Option, ) -> Result where P: FsPermissions + 'static, { - let (dir, fs) = make_temp_check_async::

(state, dir, "Deno.makeTempDir()")?; + let (dir, fs) = make_temp_check_async::

( + state, + dir_arg.as_deref(), + "Deno.makeTempDir()", + )?; let mut rng = thread_rng(); @@ -970,7 +929,11 @@ where for _ in 0..MAX_TRIES { let path = tmp_name(&mut rng, &dir, prefix.as_deref(), suffix.as_deref())?; match fs.clone().mkdir_async(path.clone(), false, 0o700).await { - Ok(_) => return path_into_string(path.into_os_string()), + Ok(_) => { + // PERMISSIONS: ensure the absolute path is not leaked + let path = strip_dir_prefix(&dir, dir_arg.as_deref(), path)?; + return path_into_string(path.into_os_string()); + } Err(FsError::Io(ref e)) if e.kind() == io::ErrorKind::AlreadyExists => { continue; } @@ -989,15 +952,18 @@ where #[string] pub fn op_fs_make_temp_file_sync

( state: &mut OpState, - #[string] dir: Option, + #[string] dir_arg: Option, #[string] prefix: Option, #[string] suffix: Option, ) -> Result where P: FsPermissions + 'static, { - let (dir, fs) = - make_temp_check_sync::

(state, dir, "Deno.makeTempFileSync()")?; + let (dir, fs) = make_temp_check_sync::

( + state, + dir_arg.as_deref(), + "Deno.makeTempFileSync()", + )?; let open_opts = OpenOptions { write: true, @@ -1011,7 +977,11 @@ where for _ in 0..MAX_TRIES { let path = tmp_name(&mut rng, &dir, prefix.as_deref(), suffix.as_deref())?; match fs.open_sync(&path, open_opts, None) { - Ok(_) => return path_into_string(path.into_os_string()), + Ok(_) => { + // PERMISSIONS: ensure the absolute path is not leaked + let path = strip_dir_prefix(&dir, dir_arg.as_deref(), path)?; + return path_into_string(path.into_os_string()); + } Err(FsError::Io(ref e)) if e.kind() == io::ErrorKind::AlreadyExists => { continue; } @@ -1030,15 +1000,18 @@ where #[string] pub async fn op_fs_make_temp_file_async

( state: Rc>, - #[string] dir: Option, + #[string] dir_arg: Option, #[string] prefix: Option, #[string] suffix: Option, ) -> Result where P: FsPermissions + 'static, { - let (dir, fs) = - make_temp_check_async::

(state, dir, "Deno.makeTempFile()")?; + let (dir, fs) = make_temp_check_async::

( + state, + dir_arg.as_deref(), + "Deno.makeTempFile()", + )?; let open_opts = OpenOptions { write: true, @@ -1053,7 +1026,11 @@ where for _ in 0..MAX_TRIES { let path = tmp_name(&mut rng, &dir, prefix.as_deref(), suffix.as_deref())?; match fs.clone().open_async(path.clone(), open_opts, None).await { - Ok(_) => return path_into_string(path.into_os_string()), + Ok(_) => { + // PERMISSIONS: ensure the absolute path is not leaked + let path = strip_dir_prefix(&dir, dir_arg.as_deref(), path)?; + return path_into_string(path.into_os_string()); + } Err(FsError::Io(ref e)) if e.kind() == io::ErrorKind::AlreadyExists => { continue; } @@ -1067,9 +1044,26 @@ where .context("tmpfile") } +fn strip_dir_prefix( + resolved_dir: &Path, + dir_arg: Option<&str>, + result_path: PathBuf, +) -> Result { + if resolved_dir.is_absolute() { + match &dir_arg { + Some(dir_arg) => { + Ok(Path::new(dir_arg).join(result_path.strip_prefix(resolved_dir)?)) + } + None => Ok(result_path), + } + } else { + Ok(result_path) + } +} + fn make_temp_check_sync

( state: &mut OpState, - dir: Option, + dir: Option<&str>, api_name: &str, ) -> Result<(PathBuf, FileSystemRc), AnyError> where @@ -1077,11 +1071,7 @@ where { let fs = state.borrow::().clone(); let dir = match dir { - Some(dir) => { - let dir = PathBuf::from(dir); - state.borrow_mut::

().check_write(&dir, api_name)?; - dir - } + Some(dir) => state.borrow_mut::

().check_write(dir, api_name)?, None => { let dir = fs.tmp_dir().context("tmpdir")?; state @@ -1095,7 +1085,7 @@ where fn make_temp_check_async

( state: Rc>, - dir: Option, + dir: Option<&str>, api_name: &str, ) -> Result<(PathBuf, FileSystemRc), AnyError> where @@ -1104,11 +1094,7 @@ where let mut state = state.borrow_mut(); let fs = state.borrow::().clone(); let dir = match dir { - Some(dir) => { - let dir = PathBuf::from(dir); - state.borrow_mut::

().check_write(&dir, api_name)?; - dir - } + Some(dir) => state.borrow_mut::

().check_write(dir, api_name)?, None => { let dir = fs.tmp_dir().context("tmpdir")?; state diff --git a/ext/kv/sqlite.rs b/ext/kv/sqlite.rs index b31fd1736b..8027ff03d4 100644 --- a/ext/kv/sqlite.rs +++ b/ext/kv/sqlite.rs @@ -1,5 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use std::borrow::Cow; use std::cell::RefCell; use std::collections::HashMap; use std::env::current_dir; @@ -36,19 +37,37 @@ pub struct SqliteDbHandler { } pub trait SqliteDbHandlerPermissions { - fn check_read(&mut self, p: &Path, api_name: &str) -> Result<(), AnyError>; - fn check_write(&mut self, p: &Path, api_name: &str) -> Result<(), AnyError>; + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] + fn check_read( + &mut self, + p: &str, + api_name: &str, + ) -> Result; + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] + fn check_write<'a>( + &mut self, + p: &'a Path, + api_name: &str, + ) -> Result, AnyError>; } impl SqliteDbHandlerPermissions for deno_permissions::PermissionsContainer { #[inline(always)] - fn check_read(&mut self, p: &Path, api_name: &str) -> Result<(), AnyError> { + fn check_read( + &mut self, + p: &str, + api_name: &str, + ) -> Result { deno_permissions::PermissionsContainer::check_read(self, p, api_name) } #[inline(always)] - fn check_write(&mut self, p: &Path, api_name: &str) -> Result<(), AnyError> { - deno_permissions::PermissionsContainer::check_write(self, p, api_name) + fn check_write<'a>( + &mut self, + p: &'a Path, + api_name: &str, + ) -> Result, AnyError> { + deno_permissions::PermissionsContainer::check_write_path(self, p, api_name) } } @@ -74,28 +93,35 @@ impl DatabaseHandler for SqliteDbHandler

{ state: Rc>, path: Option, ) -> Result { - // Validate path - if let Some(path) = &path { - if path != ":memory:" { - if path.is_empty() { - return Err(type_error("Filename cannot be empty")); - } - if path.starts_with(':') { - return Err(type_error( - "Filename cannot start with ':' unless prefixed with './'", - )); - } - let path = Path::new(path); - { - let mut state = state.borrow_mut(); - let permissions = state.borrow_mut::

(); - permissions.check_read(path, "Deno.openKv")?; - permissions.check_write(path, "Deno.openKv")?; - } + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] + fn validate_path( + state: &RefCell, + path: Option, + ) -> Result, AnyError> { + let Some(path) = path else { + return Ok(None); + }; + if path == ":memory:" { + return Ok(Some(path)); + } + if path.is_empty() { + return Err(type_error("Filename cannot be empty")); + } + if path.starts_with(':') { + return Err(type_error( + "Filename cannot start with ':' unless prefixed with './'", + )); + } + { + let mut state = state.borrow_mut(); + let permissions = state.borrow_mut::

(); + let path = permissions.check_read(&path, "Deno.openKv")?; + let path = permissions.check_write(&path, "Deno.openKv")?; + Ok(Some(path.to_string_lossy().to_string())) } } - let path = path.clone(); + let path = validate_path::

(&state, path)?; let default_storage_dir = self.default_storage_dir.clone(); type ConnGen = Arc rusqlite::Result + Send + Sync>; diff --git a/ext/napi/lib.rs b/ext/napi/lib.rs index c9dc62a8b3..4500c66fd4 100644 --- a/ext/napi/lib.rs +++ b/ext/napi/lib.rs @@ -16,7 +16,6 @@ use deno_core::OpState; use deno_core::V8CrossThreadTaskSpawner; use std::cell::RefCell; use std::collections::HashMap; -use std::path::Path; use std::path::PathBuf; use std::rc::Rc; use std::thread_local; @@ -482,15 +481,15 @@ deno_core::extension!(deno_napi, ); pub trait NapiPermissions { - fn check(&mut self, path: Option<&Path>) - -> std::result::Result<(), AnyError>; + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] + fn check(&mut self, path: &str) -> std::result::Result; } // NOTE(bartlomieju): for now, NAPI uses `--allow-ffi` flag, but that might // change in the future. impl NapiPermissions for deno_permissions::PermissionsContainer { #[inline(always)] - fn check(&mut self, path: Option<&Path>) -> Result<(), AnyError> { + fn check(&mut self, path: &str) -> Result { deno_permissions::PermissionsContainer::check_ffi(self, path) } } @@ -501,7 +500,7 @@ unsafe impl Send for NapiModuleHandle {} struct NapiModuleHandle(*const NapiModule); static NAPI_LOADED_MODULES: std::sync::LazyLock< - RwLock>, + RwLock>, > = std::sync::LazyLock::new(|| RwLock::new(HashMap::new())); #[op2(reentrant)] @@ -519,15 +518,16 @@ where { // We must limit the OpState borrow because this function can trigger a // re-borrow through the NAPI module. - let (async_work_sender, cleanup_hooks, external_ops_tracker) = { + let (async_work_sender, cleanup_hooks, external_ops_tracker, path) = { let mut op_state = op_state.borrow_mut(); let permissions = op_state.borrow_mut::(); - permissions.check(Some(&PathBuf::from(&path)))?; + let path = permissions.check(&path)?; let napi_state = op_state.borrow::(); ( op_state.borrow::().clone(), napi_state.env_cleanup_hooks.clone(), op_state.external_ops_tracker.clone(), + path, ) }; @@ -612,7 +612,7 @@ where } else { return Err(type_error(format!( "Unable to find register Node-API module at {}", - path + path.display() ))); }; diff --git a/ext/net/lib.rs b/ext/net/lib.rs index 098d220dbc..0ef3e85c47 100644 --- a/ext/net/lib.rs +++ b/ext/net/lib.rs @@ -13,6 +13,7 @@ use deno_core::error::AnyError; use deno_core::OpState; use deno_tls::rustls::RootCertStore; use deno_tls::RootCertStoreProvider; +use std::borrow::Cow; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; @@ -22,12 +23,27 @@ pub const UNSTABLE_FEATURE_NAME: &str = "net"; pub trait NetPermissions { fn check_net>( &mut self, - _host: &(T, Option), - _api_name: &str, + host: &(T, Option), + api_name: &str, ) -> Result<(), AnyError>; - fn check_read(&mut self, _p: &Path, _api_name: &str) -> Result<(), AnyError>; - fn check_write(&mut self, _p: &Path, _api_name: &str) - -> Result<(), AnyError>; + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] + fn check_read( + &mut self, + p: &str, + api_name: &str, + ) -> Result; + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] + fn check_write( + &mut self, + p: &str, + api_name: &str, + ) -> Result; + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] + fn check_write_path<'a>( + &mut self, + p: &'a Path, + api_name: &str, + ) -> Result, AnyError>; } impl NetPermissions for deno_permissions::PermissionsContainer { @@ -43,20 +59,31 @@ impl NetPermissions for deno_permissions::PermissionsContainer { #[inline(always)] fn check_read( &mut self, - path: &Path, + path: &str, api_name: &str, - ) -> Result<(), AnyError> { + ) -> Result { deno_permissions::PermissionsContainer::check_read(self, path, api_name) } #[inline(always)] fn check_write( &mut self, - path: &Path, + path: &str, api_name: &str, - ) -> Result<(), AnyError> { + ) -> Result { deno_permissions::PermissionsContainer::check_write(self, path, api_name) } + + #[inline(always)] + fn check_write_path<'a>( + &mut self, + path: &'a Path, + api_name: &str, + ) -> Result, AnyError> { + deno_permissions::PermissionsContainer::check_write_path( + self, path, api_name, + ) + } } /// Helper for checking unstable features. Used for sync ops. diff --git a/ext/net/ops.rs b/ext/net/ops.rs index b74dc8d755..67d32fe034 100644 --- a/ext/net/ops.rs +++ b/ext/net/ops.rs @@ -784,6 +784,7 @@ mod tests { use std::net::Ipv6Addr; use std::net::ToSocketAddrs; use std::path::Path; + use std::path::PathBuf; use std::sync::Arc; use std::sync::Mutex; use trust_dns_proto::rr::rdata::a::A; @@ -991,18 +992,26 @@ mod tests { fn check_read( &mut self, - _p: &Path, + p: &str, _api_name: &str, - ) -> Result<(), AnyError> { - Ok(()) + ) -> Result { + Ok(PathBuf::from(p)) } fn check_write( &mut self, - _p: &Path, + p: &str, _api_name: &str, - ) -> Result<(), AnyError> { - Ok(()) + ) -> Result { + Ok(PathBuf::from(p)) + } + + fn check_write_path<'a>( + &mut self, + p: &'a Path, + _api_name: &str, + ) -> Result, AnyError> { + Ok(Cow::Borrowed(p)) } } diff --git a/ext/net/ops_tls.rs b/ext/net/ops_tls.rs index 3ca5adbbe5..064da5818c 100644 --- a/ext/net/ops_tls.rs +++ b/ext/net/ops_tls.rs @@ -52,7 +52,6 @@ use std::io::ErrorKind; use std::io::Read; use std::net::SocketAddr; use std::num::NonZeroUsize; -use std::path::Path; use std::rc::Rc; use std::sync::Arc; use tokio::io::AsyncReadExt; @@ -356,15 +355,17 @@ where .try_borrow::() .and_then(|it| it.0.clone()); - { + let cert_file = { let mut s = state.borrow_mut(); let permissions = s.borrow_mut::(); permissions .check_net(&(&addr.hostname, Some(addr.port)), "Deno.connectTls()")?; if let Some(path) = cert_file { - permissions.check_read(Path::new(path), "Deno.connectTls()")?; + Some(permissions.check_read(path, "Deno.connectTls()")?) + } else { + None } - } + }; let mut ca_certs = args .ca_certs diff --git a/ext/net/ops_unix.rs b/ext/net/ops_unix.rs index 7d2f6af3cb..95293284f0 100644 --- a/ext/net/ops_unix.rs +++ b/ext/net/ops_unix.rs @@ -94,22 +94,22 @@ pub async fn op_net_accept_unix( #[serde] pub async fn op_net_connect_unix( state: Rc>, - #[string] path: String, + #[string] address_path: String, ) -> Result<(ResourceId, Option, Option), AnyError> where NP: NetPermissions + 'static, { - let address_path = Path::new(&path); - { + let address_path = { let mut state_ = state.borrow_mut(); - state_ + let address_path = state_ .borrow_mut::() - .check_read(address_path, "Deno.connect()")?; - state_ + .check_read(&address_path, "Deno.connect()")?; + _ = state_ .borrow_mut::() - .check_write(address_path, "Deno.connect()")?; - } - let unix_stream = UnixStream::connect(Path::new(&path)).await?; + .check_write_path(&address_path, "Deno.connect()")?; + address_path + }; + let unix_stream = UnixStream::connect(&address_path).await?; let local_addr = unix_stream.local_addr()?; let remote_addr = unix_stream.peer_addr()?; let local_addr_path = local_addr.as_pathname().map(pathstring).transpose()?; @@ -148,18 +148,17 @@ pub async fn op_net_recv_unixpacket( pub async fn op_net_send_unixpacket( state: Rc>, #[smi] rid: ResourceId, - #[string] path: String, + #[string] address_path: String, #[buffer] zero_copy: JsBuffer, ) -> Result where NP: NetPermissions + 'static, { - let address_path = Path::new(&path); - { + let address_path = { let mut s = state.borrow_mut(); s.borrow_mut::() - .check_write(address_path, "Deno.DatagramConn.send()")?; - } + .check_write(&address_path, "Deno.DatagramConn.send()")? + }; let resource = state .borrow() @@ -178,17 +177,16 @@ where #[serde] pub fn op_net_listen_unix( state: &mut OpState, - #[string] path: String, + #[string] address_path: String, #[string] api_name: String, ) -> Result<(ResourceId, Option), AnyError> where NP: NetPermissions + 'static, { - let address_path = Path::new(&path); let permissions = state.borrow_mut::(); let api_call_expr = format!("{}()", api_name); - permissions.check_read(address_path, &api_call_expr)?; - permissions.check_write(address_path, &api_call_expr)?; + let address_path = permissions.check_read(&address_path, &api_call_expr)?; + _ = permissions.check_write_path(&address_path, &api_call_expr)?; let listener = UnixListener::bind(address_path)?; let local_addr = listener.local_addr()?; let pathname = local_addr.as_pathname().map(pathstring).transpose()?; @@ -199,15 +197,15 @@ where pub fn net_listen_unixpacket( state: &mut OpState, - path: String, + address_path: String, ) -> Result<(ResourceId, Option), AnyError> where NP: NetPermissions + 'static, { - let address_path = Path::new(&path); let permissions = state.borrow_mut::(); - permissions.check_read(address_path, "Deno.listenDatagram()")?; - permissions.check_write(address_path, "Deno.listenDatagram()")?; + let address_path = + permissions.check_read(&address_path, "Deno.listenDatagram()")?; + _ = permissions.check_write_path(&address_path, "Deno.listenDatagram()")?; let socket = UnixDatagram::bind(address_path)?; let local_addr = socket.local_addr()?; let pathname = local_addr.as_pathname().map(pathstring).transpose()?; diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 75581c128b..fd9d4f9afc 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -3,8 +3,10 @@ #![deny(clippy::print_stderr)] #![deny(clippy::print_stdout)] +use std::borrow::Cow; use std::collections::HashSet; use std::path::Path; +use std::path::PathBuf; use deno_core::error::AnyError; use deno_core::located_script_name; @@ -49,21 +51,29 @@ pub trait NodePermissions { url: &Url, api_name: &str, ) -> Result<(), AnyError>; + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] #[inline(always)] - fn check_read(&mut self, path: &Path) -> Result<(), AnyError> { + fn check_read(&mut self, path: &str) -> Result { self.check_read_with_api_name(path, None) } + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] fn check_read_with_api_name( &mut self, - path: &Path, + path: &str, api_name: Option<&str>, - ) -> Result<(), AnyError>; + ) -> Result; + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] + fn check_read_path<'a>( + &mut self, + path: &'a Path, + ) -> Result, AnyError>; fn check_sys(&mut self, kind: &str, api_name: &str) -> Result<(), AnyError>; + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] fn check_write_with_api_name( &mut self, - path: &Path, + path: &str, api_name: Option<&str>, - ) -> Result<(), AnyError>; + ) -> Result; } impl NodePermissions for deno_permissions::PermissionsContainer { @@ -79,20 +89,27 @@ impl NodePermissions for deno_permissions::PermissionsContainer { #[inline(always)] fn check_read_with_api_name( &mut self, - path: &Path, + path: &str, api_name: Option<&str>, - ) -> Result<(), AnyError> { + ) -> Result { deno_permissions::PermissionsContainer::check_read_with_api_name( self, path, api_name, ) } + fn check_read_path<'a>( + &mut self, + path: &'a Path, + ) -> Result, AnyError> { + deno_permissions::PermissionsContainer::check_read_path(self, path, None) + } + #[inline(always)] fn check_write_with_api_name( &mut self, - path: &Path, + path: &str, api_name: Option<&str>, - ) -> Result<(), AnyError> { + ) -> Result { deno_permissions::PermissionsContainer::check_write_with_api_name( self, path, api_name, ) diff --git a/ext/node/ops/fs.rs b/ext/node/ops/fs.rs index 687903325b..6253f32d05 100644 --- a/ext/node/ops/fs.rs +++ b/ext/node/ops/fs.rs @@ -1,8 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use std::cell::RefCell; -use std::path::Path; -use std::path::PathBuf; use std::rc::Rc; use deno_core::error::AnyError; @@ -21,8 +19,7 @@ pub fn op_node_fs_exists_sync

( where P: NodePermissions + 'static, { - let path = PathBuf::from(path); - state + let path = state .borrow_mut::

() .check_read_with_api_name(&path, Some("node:fs.existsSync()"))?; let fs = state.borrow::(); @@ -37,14 +34,12 @@ pub async fn op_node_fs_exists

( where P: NodePermissions + 'static, { - let path = PathBuf::from(path); - - let fs = { + let (fs, path) = { let mut state = state.borrow_mut(); - state + let path = state .borrow_mut::

() .check_read_with_api_name(&path, Some("node:fs.exists()"))?; - state.borrow::().clone() + (state.borrow::().clone(), path) }; Ok(fs.exists_async(path).await?) @@ -59,18 +54,15 @@ pub fn op_node_cp_sync

( where P: NodePermissions + 'static, { - let path = Path::new(path); - let new_path = Path::new(new_path); - - state + let path = state .borrow_mut::

() .check_read_with_api_name(path, Some("node:fs.cpSync"))?; - state + let new_path = state .borrow_mut::

() .check_write_with_api_name(new_path, Some("node:fs.cpSync"))?; let fs = state.borrow::(); - fs.cp_sync(path, new_path)?; + fs.cp_sync(&path, &new_path)?; Ok(()) } @@ -83,18 +75,15 @@ pub async fn op_node_cp

( where P: NodePermissions + 'static, { - let path = PathBuf::from(path); - let new_path = PathBuf::from(new_path); - - let fs = { + let (fs, path, new_path) = { let mut state = state.borrow_mut(); - state + let path = state .borrow_mut::

() .check_read_with_api_name(&path, Some("node:fs.cpSync"))?; - state + let new_path = state .borrow_mut::

() .check_write_with_api_name(&new_path, Some("node:fs.cpSync"))?; - state.borrow::().clone() + (state.borrow::().clone(), path, new_path) }; fs.cp_async(path, new_path).await?; @@ -123,21 +112,21 @@ pub fn op_node_statfs

( where P: NodePermissions + 'static, { - { + let path = { let mut state = state.borrow_mut(); - state + let path = state .borrow_mut::

() - .check_read_with_api_name(Path::new(&path), Some("node:fs.statfs"))?; + .check_read_with_api_name(&path, Some("node:fs.statfs"))?; state .borrow_mut::

() .check_sys("statfs", "node:fs.statfs")?; - } + path + }; #[cfg(unix)] { - use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; - let path = OsStr::new(&path); + let path = path.as_os_str(); let mut cpath = path.as_bytes().to_vec(); cpath.push(0); if bigint { @@ -196,7 +185,7 @@ where // Using a vfs here doesn't make sense, it won't align with the windows API // call below. #[allow(clippy::disallowed_methods)] - let path = Path::new(&path).canonicalize()?; + let path = path.canonicalize()?; let root = path .ancestors() .last() @@ -256,14 +245,12 @@ pub fn op_node_lutimes_sync

( where P: NodePermissions + 'static, { - let path = Path::new(path); - - state + let path = state .borrow_mut::

() .check_write_with_api_name(path, Some("node:fs.lutimes"))?; let fs = state.borrow::(); - fs.lutime_sync(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos)?; + fs.lutime_sync(&path, atime_secs, atime_nanos, mtime_secs, mtime_nanos)?; Ok(()) } @@ -279,14 +266,12 @@ pub async fn op_node_lutimes

( where P: NodePermissions + 'static, { - let path = PathBuf::from(path); - - let fs = { + let (fs, path) = { let mut state = state.borrow_mut(); - state + let path = state .borrow_mut::

() .check_write_with_api_name(&path, Some("node:fs.lutimesSync"))?; - state.borrow::().clone() + (state.borrow::().clone(), path) }; fs.lutime_async(path, atime_secs, atime_nanos, mtime_secs, mtime_nanos) @@ -305,8 +290,7 @@ pub fn op_node_lchown_sync

( where P: NodePermissions + 'static, { - let path = PathBuf::from(path); - state + let path = state .borrow_mut::

() .check_write_with_api_name(&path, Some("node:fs.lchownSync"))?; let fs = state.borrow::(); @@ -324,13 +308,12 @@ pub async fn op_node_lchown

( where P: NodePermissions + 'static, { - let path = PathBuf::from(path); - let fs = { + let (fs, path) = { let mut state = state.borrow_mut(); - state + let path = state .borrow_mut::

() .check_write_with_api_name(&path, Some("node:fs.lchown"))?; - state.borrow::().clone() + (state.borrow::().clone(), path) }; fs.lchown_async(path, uid, gid).await?; Ok(()) diff --git a/runtime/examples/extension/main.rs b/runtime/examples/extension/main.rs index 4339bdf679..6f4f02508b 100644 --- a/runtime/examples/extension/main.rs +++ b/runtime/examples/extension/main.rs @@ -5,12 +5,15 @@ use std::path::Path; use std::rc::Rc; +use std::sync::Arc; use deno_core::error::AnyError; use deno_core::op2; use deno_core::FsModuleLoader; use deno_core::ModuleSpecifier; +use deno_fs::RealFs; use deno_runtime::deno_permissions::PermissionsContainer; +use deno_runtime::permissions::RuntimePermissionDescriptorParser; use deno_runtime::worker::MainWorker; use deno_runtime::worker::WorkerOptions; @@ -34,7 +37,9 @@ async fn main() -> Result<(), AnyError> { eprintln!("Running {main_module}..."); let mut worker = MainWorker::bootstrap_from_options( main_module.clone(), - PermissionsContainer::allow_all(), + PermissionsContainer::allow_all(Arc::new( + RuntimePermissionDescriptorParser::new(Arc::new(RealFs)), + )), WorkerOptions { module_loader: Rc::new(FsModuleLoader), extensions: vec![hello_runtime::init_ops_and_esm()], diff --git a/runtime/lib.rs b/runtime/lib.rs index ed3f9fbc6a..034c094a08 100644 --- a/runtime/lib.rs +++ b/runtime/lib.rs @@ -33,6 +33,7 @@ pub mod fs_util; pub mod inspector_server; pub mod js; pub mod ops; +pub mod permissions; pub mod snapshot; pub mod tokio_util; pub mod web_worker; diff --git a/runtime/ops/fs_events.rs b/runtime/ops/fs_events.rs index 58fe9d5fd2..d88a32d917 100644 --- a/runtime/ops/fs_events.rs +++ b/runtime/ops/fs_events.rs @@ -123,10 +123,9 @@ fn op_fs_events_open( RecursiveMode::NonRecursive }; for path in &args.paths { - let path = PathBuf::from(path); - state + let path = state .borrow_mut::() - .check_read(&path, "Deno.watchFs()")?; + .check_read(path, "Deno.watchFs()")?; watcher.watch(&path, recursive_mode)?; } let resource = FsEventsResource { diff --git a/runtime/ops/permissions.rs b/runtime/ops/permissions.rs index e6974efadf..9b46dd0193 100644 --- a/runtime/ops/permissions.rs +++ b/runtime/ops/permissions.rs @@ -1,7 +1,7 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use ::deno_permissions::parse_sys_kind; -use ::deno_permissions::NetDescriptor; +use ::deno_permissions::PermissionDescriptorParser; use ::deno_permissions::PermissionState; use ::deno_permissions::PermissionsContainer; use deno_core::error::custom_error; @@ -10,7 +10,7 @@ use deno_core::op2; use deno_core::OpState; use serde::Deserialize; use serde::Serialize; -use std::path::Path; +use std::sync::Arc; deno_core::extension!( deno_permissions, @@ -19,6 +19,12 @@ deno_core::extension!( op_revoke_permission, op_request_permission, ], + options = { + permission_desc_parser: Arc, + }, + state = |state, options| { + state.put(options.permission_desc_parser); + }, ); #[derive(Deserialize)] @@ -56,15 +62,37 @@ pub fn op_query_permission( state: &mut OpState, #[serde] args: PermissionArgs, ) -> Result { - let permissions = state.borrow::().0.lock(); + let permissions_container = state.borrow::(); + // todo(dsherret): don't have this function use the properties of + // permission container + let desc_parser = &permissions_container.descriptor_parser; + let permissions = permissions_container.inner.lock(); let path = args.path.as_deref(); let perm = match args.name.as_ref() { - "read" => permissions.read.query(path.map(Path::new)), - "write" => permissions.write.query(path.map(Path::new)), + "read" => permissions.read.query( + path + .map(|path| { + Result::<_, AnyError>::Ok( + desc_parser.parse_path_query(path)?.into_read(), + ) + }) + .transpose()? + .as_ref(), + ), + "write" => permissions.write.query( + path + .map(|path| { + Result::<_, AnyError>::Ok( + desc_parser.parse_path_query(path)?.into_write(), + ) + }) + .transpose()? + .as_ref(), + ), "net" => permissions.net.query( match args.host.as_deref() { None => None, - Some(h) => Some(NetDescriptor::parse(h)?), + Some(h) => Some(desc_parser.parse_net_descriptor(h)?), } .as_ref(), ), @@ -72,8 +100,24 @@ pub fn op_query_permission( "sys" => permissions .sys .query(args.kind.as_deref().map(parse_sys_kind).transpose()?), - "run" => permissions.run.query(args.command.as_deref()), - "ffi" => permissions.ffi.query(args.path.as_deref().map(Path::new)), + "run" => permissions.run.query( + args + .command + .as_deref() + .map(|request| desc_parser.parse_run_query(request)) + .transpose()? + .as_ref(), + ), + "ffi" => permissions.ffi.query( + path + .map(|path| { + Result::<_, AnyError>::Ok( + desc_parser.parse_path_query(path)?.into_ffi(), + ) + }) + .transpose()? + .as_ref(), + ), n => { return Err(custom_error( "ReferenceError", @@ -90,15 +134,37 @@ pub fn op_revoke_permission( state: &mut OpState, #[serde] args: PermissionArgs, ) -> Result { - let mut permissions = state.borrow_mut::().0.lock(); + // todo(dsherret): don't have this function use the properties of + // permission container + let permissions_container = state.borrow_mut::(); + let desc_parser = &permissions_container.descriptor_parser; + let mut permissions = permissions_container.inner.lock(); let path = args.path.as_deref(); let perm = match args.name.as_ref() { - "read" => permissions.read.revoke(path.map(Path::new)), - "write" => permissions.write.revoke(path.map(Path::new)), + "read" => permissions.read.revoke( + path + .map(|path| { + Result::<_, AnyError>::Ok( + desc_parser.parse_path_query(path)?.into_read(), + ) + }) + .transpose()? + .as_ref(), + ), + "write" => permissions.write.revoke( + path + .map(|path| { + Result::<_, AnyError>::Ok( + desc_parser.parse_path_query(path)?.into_write(), + ) + }) + .transpose()? + .as_ref(), + ), "net" => permissions.net.revoke( match args.host.as_deref() { None => None, - Some(h) => Some(NetDescriptor::parse(h)?), + Some(h) => Some(desc_parser.parse_net_descriptor(h)?), } .as_ref(), ), @@ -106,8 +172,24 @@ pub fn op_revoke_permission( "sys" => permissions .sys .revoke(args.kind.as_deref().map(parse_sys_kind).transpose()?), - "run" => permissions.run.revoke(args.command.as_deref()), - "ffi" => permissions.ffi.revoke(args.path.as_deref().map(Path::new)), + "run" => permissions.run.revoke( + args + .command + .as_deref() + .map(|request| desc_parser.parse_run_query(request)) + .transpose()? + .as_ref(), + ), + "ffi" => permissions.ffi.revoke( + path + .map(|path| { + Result::<_, AnyError>::Ok( + desc_parser.parse_path_query(path)?.into_ffi(), + ) + }) + .transpose()? + .as_ref(), + ), n => { return Err(custom_error( "ReferenceError", @@ -124,15 +206,37 @@ pub fn op_request_permission( state: &mut OpState, #[serde] args: PermissionArgs, ) -> Result { - let mut permissions = state.borrow_mut::().0.lock(); + // todo(dsherret): don't have this function use the properties of + // permission container + let permissions_container = state.borrow_mut::(); + let desc_parser = &permissions_container.descriptor_parser; + let mut permissions = permissions_container.inner.lock(); let path = args.path.as_deref(); let perm = match args.name.as_ref() { - "read" => permissions.read.request(path.map(Path::new)), - "write" => permissions.write.request(path.map(Path::new)), + "read" => permissions.read.request( + path + .map(|path| { + Result::<_, AnyError>::Ok( + desc_parser.parse_path_query(path)?.into_read(), + ) + }) + .transpose()? + .as_ref(), + ), + "write" => permissions.write.request( + path + .map(|path| { + Result::<_, AnyError>::Ok( + desc_parser.parse_path_query(path)?.into_write(), + ) + }) + .transpose()? + .as_ref(), + ), "net" => permissions.net.request( match args.host.as_deref() { None => None, - Some(h) => Some(NetDescriptor::parse(h)?), + Some(h) => Some(desc_parser.parse_net_descriptor(h)?), } .as_ref(), ), @@ -140,8 +244,24 @@ pub fn op_request_permission( "sys" => permissions .sys .request(args.kind.as_deref().map(parse_sys_kind).transpose()?), - "run" => permissions.run.request(args.command.as_deref()), - "ffi" => permissions.ffi.request(args.path.as_deref().map(Path::new)), + "run" => permissions.run.request( + args + .command + .as_deref() + .map(|request| desc_parser.parse_run_query(request)) + .transpose()? + .as_ref(), + ), + "ffi" => permissions.ffi.request( + path + .map(|path| { + Result::<_, AnyError>::Ok( + desc_parser.parse_path_query(path)?.into_ffi(), + ) + }) + .transpose()? + .as_ref(), + ), n => { return Err(custom_error( "ReferenceError", diff --git a/runtime/ops/process.rs b/runtime/ops/process.rs index b7242c07f2..a39bb5f045 100644 --- a/runtime/ops/process.rs +++ b/runtime/ops/process.rs @@ -17,12 +17,13 @@ use deno_io::ChildStderrResource; use deno_io::ChildStdinResource; use deno_io::ChildStdoutResource; use deno_permissions::PermissionsContainer; -use deno_permissions::RunPathQuery; +use deno_permissions::RunQueryDescriptor; use serde::Deserialize; use serde::Serialize; use std::borrow::Cow; use std::cell::RefCell; use std::collections::HashMap; +use std::ffi::OsString; use std::path::Path; use std::path::PathBuf; use std::process::ExitStatus; @@ -536,9 +537,9 @@ fn compute_run_cmd_and_check_permissions( .with_context(|| format!("Failed to spawn '{}'", arg_cmd))?; check_run_permission( state, - RunPathQuery { - requested: arg_cmd, - resolved: &cmd, + &RunQueryDescriptor::Path { + requested: arg_cmd.to_string(), + resolved: cmd.clone(), }, &run_env, api_name, @@ -547,7 +548,7 @@ fn compute_run_cmd_and_check_permissions( } struct RunEnv { - envs: HashMap, + envs: HashMap, cwd: PathBuf, } @@ -567,11 +568,32 @@ fn compute_run_env( .map(|cwd_arg| resolve_path(cwd_arg, &cwd)) .unwrap_or(cwd); let envs = if arg_clear_env { - arg_envs.iter().cloned().collect() + arg_envs + .iter() + .map(|(k, v)| (OsString::from(k), OsString::from(v))) + .collect() } else { - let mut envs = std::env::vars().collect::>(); + let mut envs = std::env::vars_os() + .map(|(k, v)| { + ( + if cfg!(windows) { + k.to_ascii_uppercase() + } else { + k + }, + v, + ) + }) + .collect::>(); for (key, value) in arg_envs { - envs.insert(key.clone(), value.clone()); + envs.insert( + OsString::from(if cfg!(windows) { + key.to_ascii_uppercase() + } else { + key.clone() + }), + OsString::from(value.clone()), + ); } envs }; @@ -585,19 +607,7 @@ fn resolve_cmd(cmd: &str, env: &RunEnv) -> Result { if is_path { Ok(resolve_path(cmd, &env.cwd)) } else { - let path = env.envs.get("PATH").or_else(|| { - if cfg!(windows) { - env.envs.iter().find_map(|(k, v)| { - if k.to_uppercase() == "PATH" { - Some(v) - } else { - None - } - }) - } else { - None - } - }); + let path = env.envs.get(&OsString::from("PATH")); match which::which_in(cmd, path, &env.cwd) { Ok(cmd) => Ok(cmd), Err(which::Error::CannotFindBinaryPath) => { @@ -614,7 +624,7 @@ fn resolve_path(path: &str, cwd: &Path) -> PathBuf { fn check_run_permission( state: &mut OpState, - cmd: RunPathQuery, + cmd: &RunQueryDescriptor, run_env: &RunEnv, api_name: &str, ) -> Result<(), AnyError> { @@ -647,11 +657,22 @@ fn get_requires_allow_all_env_vars(env: &RunEnv) -> Vec<&str> { key.starts_with("LD_") || key.starts_with("DYLD_") } + fn is_empty(value: &OsString) -> bool { + value.is_empty() + || value.to_str().map(|v| v.trim().is_empty()).unwrap_or(false) + } + let mut found_envs = env .envs .iter() - .filter(|(k, v)| requires_allow_all(k) && !v.trim().is_empty()) - .map(|(k, _)| k.as_str()) + .filter_map(|(k, v)| { + let key = k.to_str()?; + if requires_allow_all(key) && !is_empty(v) { + Some(key) + } else { + None + } + }) .collect::>(); found_envs.sort(); found_envs diff --git a/runtime/ops/worker_host.rs b/runtime/ops/worker_host.rs index 19475fedf1..3c00356455 100644 --- a/runtime/ops/worker_host.rs +++ b/runtime/ops/worker_host.rs @@ -19,6 +19,7 @@ use deno_core::ModuleSpecifier; use deno_core::OpState; use deno_permissions::create_child_permissions; use deno_permissions::ChildPermissionsArg; +use deno_permissions::PermissionDescriptorParser; use deno_permissions::PermissionsContainer; use deno_web::deserialize_js_transferables; use deno_web::JsMessageData; @@ -153,13 +154,19 @@ fn op_create_worker( "Worker.deno.permissions", ); } + let permission_desc_parser = state + .borrow::>() + .clone(); let parent_permissions = state.borrow_mut::(); let worker_permissions = if let Some(child_permissions_arg) = args.permissions { - let mut parent_permissions = parent_permissions.0.lock(); - let perms = - create_child_permissions(&mut parent_permissions, child_permissions_arg)?; - PermissionsContainer::new(perms) + let mut parent_permissions = parent_permissions.inner.lock(); + let perms = create_child_permissions( + permission_desc_parser.as_ref(), + &mut parent_permissions, + child_permissions_arg, + )?; + PermissionsContainer::new(permission_desc_parser, perms) } else { parent_permissions.clone() }; diff --git a/runtime/permissions.rs b/runtime/permissions.rs new file mode 100644 index 0000000000..a28ba35b14 --- /dev/null +++ b/runtime/permissions.rs @@ -0,0 +1,164 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::path::Path; +use std::path::PathBuf; + +use deno_core::anyhow::bail; +use deno_core::anyhow::Context; +use deno_core::error::AnyError; +use deno_core::normalize_path; +use deno_permissions::AllowRunDescriptor; +use deno_permissions::AllowRunDescriptorParseResult; +use deno_permissions::DenyRunDescriptor; +use deno_permissions::EnvDescriptor; +use deno_permissions::FfiDescriptor; +use deno_permissions::NetDescriptor; +use deno_permissions::PathQueryDescriptor; +use deno_permissions::ReadDescriptor; +use deno_permissions::RunQueryDescriptor; +use deno_permissions::SysDescriptor; +use deno_permissions::WriteDescriptor; + +#[derive(Debug)] +pub struct RuntimePermissionDescriptorParser { + fs: deno_fs::FileSystemRc, +} + +impl RuntimePermissionDescriptorParser { + pub fn new(fs: deno_fs::FileSystemRc) -> Self { + Self { fs } + } + + fn resolve_from_cwd(&self, path: &str) -> Result { + if path.is_empty() { + bail!("Empty path is not allowed"); + } + let path = Path::new(path); + if path.is_absolute() { + Ok(normalize_path(path)) + } else { + let cwd = self.resolve_cwd()?; + Ok(normalize_path(cwd.join(path))) + } + } + + fn resolve_cwd(&self) -> Result { + self + .fs + .cwd() + .map_err(|e| e.into_io_error()) + .context("failed resolving cwd") + } +} + +impl deno_permissions::PermissionDescriptorParser + for RuntimePermissionDescriptorParser +{ + fn parse_read_descriptor( + &self, + text: &str, + ) -> Result { + Ok(ReadDescriptor(self.resolve_from_cwd(text)?)) + } + + fn parse_write_descriptor( + &self, + text: &str, + ) -> Result { + Ok(WriteDescriptor(self.resolve_from_cwd(text)?)) + } + + fn parse_net_descriptor( + &self, + text: &str, + ) -> Result { + NetDescriptor::parse(text) + } + + fn parse_env_descriptor( + &self, + text: &str, + ) -> Result { + if text.is_empty() { + Err(AnyError::msg("Empty env not allowed")) + } else { + Ok(EnvDescriptor::new(text)) + } + } + + fn parse_sys_descriptor( + &self, + text: &str, + ) -> Result { + if text.is_empty() { + Err(AnyError::msg("Empty sys not allowed")) + } else { + Ok(SysDescriptor(text.to_string())) + } + } + + fn parse_allow_run_descriptor( + &self, + text: &str, + ) -> Result { + Ok(AllowRunDescriptor::parse(text, &self.resolve_cwd()?)?) + } + + fn parse_deny_run_descriptor( + &self, + text: &str, + ) -> Result { + Ok(DenyRunDescriptor::parse(text, &self.resolve_cwd()?)) + } + + fn parse_ffi_descriptor( + &self, + text: &str, + ) -> Result { + Ok(FfiDescriptor(self.resolve_from_cwd(text)?)) + } + + // queries + + fn parse_path_query( + &self, + path: &str, + ) -> Result { + Ok(PathQueryDescriptor { + resolved: self.resolve_from_cwd(path)?, + requested: path.to_string(), + }) + } + + fn parse_run_query( + &self, + requested: &str, + ) -> Result { + if requested.is_empty() { + bail!("Empty run query is not allowed"); + } + RunQueryDescriptor::parse(requested) + } +} + +#[cfg(test)] +mod test { + use std::sync::Arc; + + use deno_fs::RealFs; + use deno_permissions::PermissionDescriptorParser; + + use super::*; + + #[test] + fn test_handle_empty_value() { + let parser = RuntimePermissionDescriptorParser::new(Arc::new(RealFs)); + assert!(parser.parse_read_descriptor("").is_err()); + assert!(parser.parse_write_descriptor("").is_err()); + assert!(parser.parse_env_descriptor("").is_err()); + assert!(parser.parse_net_descriptor("").is_err()); + assert!(parser.parse_ffi_descriptor("").is_err()); + assert!(parser.parse_path_query("").is_err()); + assert!(parser.parse_run_query("").is_err()); + } +} diff --git a/runtime/permissions/lib.rs b/runtime/permissions/lib.rs index 36750ae383..ad84b9fc3e 100644 --- a/runtime/permissions/lib.rs +++ b/runtime/permissions/lib.rs @@ -1,5 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use deno_core::anyhow::bail; use deno_core::anyhow::Context; use deno_core::error::custom_error; use deno_core::error::type_error; @@ -51,14 +52,11 @@ macro_rules! skip_check_if_is_permission_fully_granted { } #[inline] -fn resolve_from_cwd(path: &Path) -> Result { +fn resolve_from_known_cwd(path: &Path, cwd: &Path) -> PathBuf { if path.is_absolute() { - Ok(normalize_path(path)) + normalize_path(path) } else { - #[allow(clippy::disallowed_methods)] - let cwd = std::env::current_dir() - .context("Failed to get current working directory")?; - Ok(normalize_path(cwd.join(path))) + normalize_path(cwd.join(path)) } } @@ -310,12 +308,17 @@ impl AsRef for EnvVarName { } } -pub trait Descriptor: Eq + Clone + Hash { - type Arg: From; +pub trait QueryDescriptor: Debug { + type AllowDesc: Debug + Eq + Clone + Hash; + type DenyDesc: Debug + Eq + Clone + Hash; - /// Parse this descriptor from a list of Self::Arg, which may have been converted from - /// command-line strings. - fn parse(list: Option<&[Self::Arg]>) -> Result, AnyError>; + fn flag_name() -> &'static str; + fn display_name(&self) -> Cow; + + fn from_allow(allow: &Self::AllowDesc) -> Self; + + fn as_allow(&self) -> Option; + fn as_deny(&self) -> Self::DenyDesc; /// Generic check function to check this descriptor against a `UnaryPermission`. fn check_in_permission( @@ -324,27 +327,35 @@ pub trait Descriptor: Eq + Clone + Hash { api_name: Option<&str>, ) -> Result<(), AnyError>; - fn flag_name() -> &'static str; - fn name(&self) -> Cow; - // By default, specifies no-stronger-than relationship. - // As this is not strict, it's only true when descriptors are the same. - fn stronger_than(&self, other: &Self) -> bool { - self == other + fn matches_allow(&self, other: &Self::AllowDesc) -> bool; + fn matches_deny(&self, other: &Self::DenyDesc) -> bool; + + /// Gets if this query descriptor should revoke the provided allow descriptor. + fn revokes(&self, other: &Self::AllowDesc) -> bool; + fn stronger_than_deny(&self, other: &Self::DenyDesc) -> bool; + fn overlaps_deny(&self, other: &Self::DenyDesc) -> bool; +} + +fn format_display_name(display_name: Cow) -> String { + if display_name.starts_with('<') && display_name.ends_with('>') { + display_name.into_owned() + } else { + format!("\"{}\"", display_name) } } -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct UnaryPermission { +#[derive(Debug, Eq, PartialEq)] +pub struct UnaryPermission { granted_global: bool, - granted_list: HashSet, + granted_list: HashSet, flag_denied_global: bool, - flag_denied_list: HashSet, + flag_denied_list: HashSet, prompt_denied_global: bool, - prompt_denied_list: HashSet, + prompt_denied_list: HashSet, prompt: bool, } -impl Default for UnaryPermission { +impl Default for UnaryPermission { fn default() -> Self { UnaryPermission { granted_global: Default::default(), @@ -358,7 +369,21 @@ impl Default for UnaryPermission { } } -impl UnaryPermission { +impl Clone for UnaryPermission { + fn clone(&self) -> Self { + Self { + granted_global: self.granted_global, + granted_list: self.granted_list.clone(), + flag_denied_global: self.flag_denied_global, + flag_denied_list: self.flag_denied_list.clone(), + prompt_denied_global: self.prompt_denied_global, + prompt_denied_list: self.prompt_denied_list.clone(), + prompt: self.prompt, + } + } +} + +impl UnaryPermission { pub fn allow_all() -> Self { Self { granted_global: true, @@ -377,26 +402,21 @@ impl UnaryPermission { api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc(None, false, api_name, || None) + self.check_desc(None, false, api_name) } fn check_desc( &mut self, - desc: Option<&T>, + desc: Option<&TQuery>, assert_non_partial: bool, api_name: Option<&str>, - get_display_name: impl Fn() -> Option, ) -> Result<(), AnyError> { - skip_check_if_is_permission_fully_granted!(self); let (result, prompted, is_allow_all) = self .query_desc(desc, AllowPartial::from(!assert_non_partial)) .check2( - T::flag_name(), + TQuery::flag_name(), api_name, - || match get_display_name() { - Some(display_name) => Some(display_name), - None => desc.map(|d| format!("\"{}\"", d.name())), - }, + || desc.map(|d| format_display_name(d.display_name())), self.prompt, ); if prompted { @@ -404,10 +424,10 @@ impl UnaryPermission { if is_allow_all { self.insert_granted(None); } else { - self.insert_granted(desc.cloned()); + self.insert_granted(desc); } } else { - self.insert_prompt_denied(desc.cloned()); + self.insert_prompt_denied(desc.map(|d| d.as_deny())); } } result @@ -415,7 +435,7 @@ impl UnaryPermission { fn query_desc( &self, - desc: Option<&T>, + desc: Option<&TQuery>, allow_partial: AllowPartial, ) -> PermissionState { if self.is_flag_denied(desc) || self.is_prompt_denied(desc) { @@ -447,43 +467,33 @@ impl UnaryPermission { } } - fn request_desc( - &mut self, - desc: Option<&T>, - get_display_name: impl Fn() -> Option, - ) -> PermissionState { + fn request_desc(&mut self, desc: Option<&TQuery>) -> PermissionState { let state = self.query_desc(desc, AllowPartial::TreatAsPartialGranted); if state == PermissionState::Granted { - self.insert_granted(desc.cloned()); + self.insert_granted(desc); return state; } if state != PermissionState::Prompt { return state; } let mut message = String::with_capacity(40); - message.push_str(&format!("{} access", T::flag_name())); - match get_display_name() { - Some(display_name) => { - message.push_str(&format!(" to \"{}\"", display_name)) - } - None => { - if let Some(desc) = desc { - message.push_str(&format!(" to \"{}\"", desc.name())); - } - } + message.push_str(&format!("{} access", TQuery::flag_name())); + if let Some(desc) = desc { + message + .push_str(&format!(" to {}", format_display_name(desc.display_name()))); } match permission_prompt( &message, - T::flag_name(), + TQuery::flag_name(), Some("Deno.permissions.request()"), true, ) { PromptResponse::Allow => { - self.insert_granted(desc.cloned()); + self.insert_granted(desc); PermissionState::Granted } PromptResponse::Deny => { - self.insert_prompt_denied(desc.cloned()); + self.insert_prompt_denied(desc.map(|d| d.as_deny())); PermissionState::Denied } PromptResponse::AllowAll => { @@ -493,10 +503,10 @@ impl UnaryPermission { } } - fn revoke_desc(&mut self, desc: Option<&T>) -> PermissionState { + fn revoke_desc(&mut self, desc: Option<&TQuery>) -> PermissionState { match desc { Some(desc) => { - self.granted_list.retain(|v| !v.stronger_than(desc)); + self.granted_list.retain(|v| !desc.revokes(v)); } None => { self.granted_global = false; @@ -509,47 +519,61 @@ impl UnaryPermission { self.query_desc(desc, AllowPartial::TreatAsPartialGranted) } - fn is_granted(&self, desc: Option<&T>) -> bool { - Self::list_contains(desc, self.granted_global, &self.granted_list) + fn is_granted(&self, query: Option<&TQuery>) -> bool { + match query { + Some(query) => { + self.granted_global + || self.granted_list.iter().any(|v| query.matches_allow(v)) + } + None => self.granted_global, + } } - fn is_flag_denied(&self, desc: Option<&T>) -> bool { - Self::list_contains(desc, self.flag_denied_global, &self.flag_denied_list) + fn is_flag_denied(&self, query: Option<&TQuery>) -> bool { + match query { + Some(query) => { + self.flag_denied_global + || self.flag_denied_list.iter().any(|v| query.matches_deny(v)) + } + None => self.flag_denied_global, + } } - fn is_prompt_denied(&self, desc: Option<&T>) -> bool { - match desc { - Some(desc) => self + fn is_prompt_denied(&self, query: Option<&TQuery>) -> bool { + match query { + Some(query) => self .prompt_denied_list .iter() - .any(|v| desc.stronger_than(v)), + .any(|v| query.stronger_than_deny(v)), None => self.prompt_denied_global || !self.prompt_denied_list.is_empty(), } } - fn is_partial_flag_denied(&self, desc: Option<&T>) -> bool { - match desc { + fn is_partial_flag_denied(&self, query: Option<&TQuery>) -> bool { + match query { None => !self.flag_denied_list.is_empty(), - Some(desc) => self.flag_denied_list.iter().any(|v| desc.stronger_than(v)), + Some(query) => { + self.flag_denied_list.iter().any(|v| query.overlaps_deny(v)) + } } } - fn list_contains( - desc: Option<&T>, - list_global: bool, - list: &HashSet, - ) -> bool { - match desc { - Some(desc) => list_global || list.iter().any(|v| v.stronger_than(desc)), - None => list_global, - } - } - - fn insert_granted(&mut self, desc: Option) { + fn insert_granted(&mut self, query: Option<&TQuery>) -> bool { + let desc = match query.map(|q| q.as_allow()) { + Some(Some(allow_desc)) => Some(allow_desc), + Some(None) => { + // the user was prompted for this descriptor in order to not + // expose anything about the system to the program, but the + // descriptor wasn't valid so no permission was raised + return false; + } + None => None, + }; Self::list_insert(desc, &mut self.granted_global, &mut self.granted_list); + true } - fn insert_prompt_denied(&mut self, desc: Option) { + fn insert_prompt_denied(&mut self, desc: Option) { Self::list_insert( desc, &mut self.prompt_denied_global, @@ -557,7 +581,7 @@ impl UnaryPermission { ); } - fn list_insert( + fn list_insert( desc: Option, list_global: &mut bool, list: &mut HashSet, @@ -573,7 +597,8 @@ impl UnaryPermission { fn create_child_permissions( &mut self, flag: ChildUnaryPermissionArg, - ) -> Result, AnyError> { + parse: impl Fn(&str) -> Result, AnyError>, + ) -> Result, AnyError> { let mut perms = Self::default(); match flag { @@ -588,14 +613,15 @@ impl UnaryPermission { } ChildUnaryPermissionArg::NotGranted => {} ChildUnaryPermissionArg::GrantedList(granted_list) => { - let granted: Vec = - granted_list.into_iter().map(From::from).collect(); - perms.granted_list = T::parse(Some(&granted))?; - if !perms - .granted_list + perms.granted_list = granted_list .iter() - .all(|desc| desc.check_in_permission(self, None).is_ok()) - { + .filter_map(|i| parse(i).transpose()) + .collect::>()?; + if !perms.granted_list.iter().all(|desc| { + TQuery::from_allow(desc) + .check_in_permission(self, None) + .is_ok() + }) { return Err(escalation_error()); } } @@ -613,42 +639,55 @@ impl UnaryPermission { } #[derive(Clone, Eq, PartialEq, Hash, Debug)] -pub struct ReadDescriptor(pub PathBuf); +pub struct PathQueryDescriptor { + pub requested: String, + pub resolved: PathBuf, +} -impl Descriptor for ReadDescriptor { - type Arg = PathBuf; - - fn check_in_permission( - &self, - perm: &mut UnaryPermission, - api_name: Option<&str>, - ) -> Result<(), AnyError> { - skip_check_if_is_permission_fully_granted!(perm); - perm.check_desc(Some(self), true, api_name, || None) +impl PathQueryDescriptor { + pub fn into_ffi(self) -> FfiQueryDescriptor { + FfiQueryDescriptor(self) } - fn parse(args: Option<&[Self::Arg]>) -> Result, AnyError> { - parse_path_list(args, ReadDescriptor) + pub fn into_read(self) -> ReadQueryDescriptor { + ReadQueryDescriptor(self) } + pub fn into_write(self) -> WriteQueryDescriptor { + WriteQueryDescriptor(self) + } +} + +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub struct ReadQueryDescriptor(pub PathQueryDescriptor); + +impl QueryDescriptor for ReadQueryDescriptor { + type AllowDesc = ReadDescriptor; + type DenyDesc = ReadDescriptor; + fn flag_name() -> &'static str { "read" } - fn name(&self) -> Cow { - Cow::from(self.0.display().to_string()) + fn display_name(&self) -> Cow { + Cow::Borrowed(self.0.requested.as_str()) } - fn stronger_than(&self, other: &Self) -> bool { - other.0.starts_with(&self.0) + fn from_allow(allow: &Self::AllowDesc) -> Self { + PathQueryDescriptor { + requested: allow.0.to_string_lossy().into_owned(), + resolved: allow.0.clone(), + } + .into_read() } -} -#[derive(Clone, Eq, PartialEq, Hash, Debug)] -pub struct WriteDescriptor(pub PathBuf); + fn as_allow(&self) -> Option { + Some(ReadDescriptor(self.0.resolved.clone())) + } -impl Descriptor for WriteDescriptor { - type Arg = PathBuf; + fn as_deny(&self) -> Self::DenyDesc { + ReadDescriptor(self.0.resolved.clone()) + } fn check_in_permission( &self, @@ -656,26 +695,96 @@ impl Descriptor for WriteDescriptor { api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(perm); - perm.check_desc(Some(self), true, api_name, || None) + perm.check_desc(Some(self), true, api_name) } - fn parse(args: Option<&[Self::Arg]>) -> Result, AnyError> { - parse_path_list(args, WriteDescriptor) + fn matches_allow(&self, other: &Self::AllowDesc) -> bool { + self.0.resolved.starts_with(&other.0) } + fn matches_deny(&self, other: &Self::DenyDesc) -> bool { + self.0.resolved.starts_with(&other.0) + } + + fn revokes(&self, other: &Self::AllowDesc) -> bool { + self.matches_allow(other) + } + + fn stronger_than_deny(&self, other: &Self::DenyDesc) -> bool { + other.0.starts_with(&self.0.resolved) + } + + fn overlaps_deny(&self, other: &Self::DenyDesc) -> bool { + self.stronger_than_deny(other) + } +} + +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub struct ReadDescriptor(pub PathBuf); + +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub struct WriteQueryDescriptor(pub PathQueryDescriptor); + +impl QueryDescriptor for WriteQueryDescriptor { + type AllowDesc = WriteDescriptor; + type DenyDesc = WriteDescriptor; + fn flag_name() -> &'static str { "write" } - fn name(&self) -> Cow { - Cow::from(self.0.display().to_string()) + fn display_name(&self) -> Cow { + Cow::Borrowed(&self.0.requested) } - fn stronger_than(&self, other: &Self) -> bool { - other.0.starts_with(&self.0) + fn from_allow(allow: &Self::AllowDesc) -> Self { + WriteQueryDescriptor(PathQueryDescriptor { + requested: allow.0.to_string_lossy().into_owned(), + resolved: allow.0.clone(), + }) + } + + fn as_allow(&self) -> Option { + Some(WriteDescriptor(self.0.resolved.clone())) + } + + fn as_deny(&self) -> Self::DenyDesc { + WriteDescriptor(self.0.resolved.clone()) + } + + fn check_in_permission( + &self, + perm: &mut UnaryPermission, + api_name: Option<&str>, + ) -> Result<(), AnyError> { + skip_check_if_is_permission_fully_granted!(perm); + perm.check_desc(Some(self), true, api_name) + } + + fn matches_allow(&self, other: &Self::AllowDesc) -> bool { + self.0.resolved.starts_with(&other.0) + } + + fn matches_deny(&self, other: &Self::DenyDesc) -> bool { + self.0.resolved.starts_with(&other.0) + } + + fn revokes(&self, other: &Self::AllowDesc) -> bool { + self.matches_allow(other) + } + + fn stronger_than_deny(&self, other: &Self::DenyDesc) -> bool { + other.0.starts_with(&self.0.resolved) + } + + fn overlaps_deny(&self, other: &Self::DenyDesc) -> bool { + self.stronger_than_deny(other) } } +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub struct WriteDescriptor(pub PathBuf); + #[derive(Clone, Eq, PartialEq, Hash, Debug)] pub enum Host { Fqdn(FQDN), @@ -728,8 +837,29 @@ impl Host { #[derive(Clone, Eq, PartialEq, Hash, Debug)] pub struct NetDescriptor(pub Host, pub Option); -impl Descriptor for NetDescriptor { - type Arg = String; +impl QueryDescriptor for NetDescriptor { + type AllowDesc = NetDescriptor; + type DenyDesc = NetDescriptor; + + fn flag_name() -> &'static str { + "net" + } + + fn display_name(&self) -> Cow { + Cow::from(format!("{}", self)) + } + + fn from_allow(allow: &Self::AllowDesc) -> Self { + allow.clone() + } + + fn as_allow(&self) -> Option { + Some(self.clone()) + } + + fn as_deny(&self) -> Self::DenyDesc { + self.clone() + } fn check_in_permission( &self, @@ -737,23 +867,27 @@ impl Descriptor for NetDescriptor { api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(perm); - perm.check_desc(Some(self), false, api_name, || None) + perm.check_desc(Some(self), false, api_name) } - fn parse(args: Option<&[Self::Arg]>) -> Result, AnyError> { - parse_net_list(args) + fn matches_allow(&self, other: &Self::AllowDesc) -> bool { + self.0 == other.0 && (other.1.is_none() || self.1 == other.1) } - fn flag_name() -> &'static str { - "net" + fn matches_deny(&self, other: &Self::DenyDesc) -> bool { + self.0 == other.0 && (other.1.is_none() || self.1 == other.1) } - fn name(&self) -> Cow { - Cow::from(format!("{}", self)) + fn revokes(&self, other: &Self::AllowDesc) -> bool { + self.matches_allow(other) } - fn stronger_than(&self, other: &Self) -> bool { - self.0 == other.0 && (self.1.is_none() || self.1 == other.1) + fn stronger_than_deny(&self, other: &Self::DenyDesc) -> bool { + self.matches_deny(other) + } + + fn overlaps_deny(&self, _other: &Self::DenyDesc) -> bool { + false } } @@ -837,8 +971,29 @@ impl EnvDescriptor { } } -impl Descriptor for EnvDescriptor { - type Arg = String; +impl QueryDescriptor for EnvDescriptor { + type AllowDesc = EnvDescriptor; + type DenyDesc = EnvDescriptor; + + fn flag_name() -> &'static str { + "env" + } + + fn display_name(&self) -> Cow { + Cow::from(self.0.as_ref()) + } + + fn from_allow(allow: &Self::AllowDesc) -> Self { + allow.clone() + } + + fn as_allow(&self) -> Option { + Some(self.clone()) + } + + fn as_deny(&self) -> Self::DenyDesc { + self.clone() + } fn check_in_permission( &self, @@ -846,19 +1001,27 @@ impl Descriptor for EnvDescriptor { api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(perm); - perm.check_desc(Some(self), false, api_name, || None) + perm.check_desc(Some(self), false, api_name) } - fn parse(list: Option<&[Self::Arg]>) -> Result, AnyError> { - parse_env_list(list) + fn matches_allow(&self, other: &Self::AllowDesc) -> bool { + self == other } - fn flag_name() -> &'static str { - "env" + fn matches_deny(&self, other: &Self::DenyDesc) -> bool { + self == other } - fn name(&self) -> Cow { - Cow::from(self.0.as_ref()) + fn revokes(&self, other: &Self::AllowDesc) -> bool { + self == other + } + + fn stronger_than_deny(&self, other: &Self::DenyDesc) -> bool { + self == other + } + + fn overlaps_deny(&self, _other: &Self::DenyDesc) -> bool { + false } } @@ -869,9 +1032,154 @@ impl AsRef for EnvDescriptor { } #[derive(Clone, Eq, PartialEq, Hash, Debug, Serialize, Deserialize)] -pub struct RunPathQuery<'a> { - pub requested: &'a str, - pub resolved: &'a Path, +pub enum RunQueryDescriptor { + Path { + requested: String, + resolved: PathBuf, + }, + /// This variant won't actually grant permissions because the path of + /// the executable is unresolved. It's mostly used so that prompts and + /// everything works the same way as when the command is resolved, + /// meaning that a script can't tell + /// if a command is resolved or not based on how long something + /// takes to ask for permissions. + Name(String), +} + +impl RunQueryDescriptor { + pub fn parse(requested: &str) -> Result { + if is_path(requested) { + let path = PathBuf::from(requested); + let resolved = if path.is_absolute() { + normalize_path(path) + } else { + let cwd = std::env::current_dir().context("failed resolving cwd")?; + normalize_path(cwd.join(path)) + }; + Ok(RunQueryDescriptor::Path { + requested: requested.to_string(), + resolved, + }) + } else { + match which::which(requested) { + Ok(resolved) => Ok(RunQueryDescriptor::Path { + requested: requested.to_string(), + resolved, + }), + Err(_) => Ok(RunQueryDescriptor::Name(requested.to_string())), + } + } + } +} + +impl QueryDescriptor for RunQueryDescriptor { + type AllowDesc = AllowRunDescriptor; + type DenyDesc = DenyRunDescriptor; + + fn flag_name() -> &'static str { + "run" + } + + fn display_name(&self) -> Cow { + match self { + RunQueryDescriptor::Path { requested, .. } => Cow::Borrowed(requested), + RunQueryDescriptor::Name(name) => Cow::Borrowed(name), + } + } + + fn from_allow(allow: &Self::AllowDesc) -> Self { + RunQueryDescriptor::Path { + requested: allow.0.to_string_lossy().into_owned(), + resolved: allow.0.clone(), + } + } + + fn as_allow(&self) -> Option { + match self { + RunQueryDescriptor::Path { resolved, .. } => { + Some(AllowRunDescriptor(resolved.clone())) + } + RunQueryDescriptor::Name(_) => None, + } + } + + fn as_deny(&self) -> Self::DenyDesc { + match self { + RunQueryDescriptor::Path { + resolved, + requested, + } => { + if requested.contains('/') + || (cfg!(windows) && requested.contains("\\")) + { + DenyRunDescriptor::Path(resolved.clone()) + } else { + DenyRunDescriptor::Name(requested.clone()) + } + } + RunQueryDescriptor::Name(name) => DenyRunDescriptor::Name(name.clone()), + } + } + + fn check_in_permission( + &self, + perm: &mut UnaryPermission, + api_name: Option<&str>, + ) -> Result<(), AnyError> { + skip_check_if_is_permission_fully_granted!(perm); + perm.check_desc(Some(self), false, api_name) + } + + fn matches_allow(&self, other: &Self::AllowDesc) -> bool { + match self { + RunQueryDescriptor::Path { resolved, .. } => *resolved == other.0, + RunQueryDescriptor::Name(_) => false, + } + } + + fn matches_deny(&self, other: &Self::DenyDesc) -> bool { + match other { + DenyRunDescriptor::Name(deny_desc) => match self { + RunQueryDescriptor::Path { resolved, .. } => { + denies_run_name(deny_desc, resolved) + } + RunQueryDescriptor::Name(query) => query == deny_desc, + }, + DenyRunDescriptor::Path(deny_desc) => match self { + RunQueryDescriptor::Path { resolved, .. } => { + resolved.starts_with(deny_desc) + } + RunQueryDescriptor::Name(query) => denies_run_name(query, deny_desc), + }, + } + } + + fn revokes(&self, other: &Self::AllowDesc) -> bool { + match self { + RunQueryDescriptor::Path { + resolved, + requested, + } => { + if *resolved == other.0 { + return true; + } + if is_path(requested) { + false + } else { + denies_run_name(requested, &other.0) + } + } + RunQueryDescriptor::Name(query) => denies_run_name(query, &other.0), + } + } + + fn stronger_than_deny(&self, other: &Self::DenyDesc) -> bool { + self.matches_deny(other) + } + + fn overlaps_deny(&self, _other: &Self::DenyDesc) -> bool { + false + } } pub enum RunDescriptorArg { @@ -879,8 +1187,49 @@ pub enum RunDescriptorArg { Path(PathBuf), } +pub enum AllowRunDescriptorParseResult { + /// An error occured getting the descriptor that should + /// be surfaced as a warning when launching deno, but should + /// be ignored when creating a worker. + Unresolved(Box), + Descriptor(AllowRunDescriptor), +} + +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub struct AllowRunDescriptor(pub PathBuf); + +impl AllowRunDescriptor { + pub fn parse( + text: &str, + cwd: &Path, + ) -> Result { + let is_path = is_path(text); + // todo(dsherret): canonicalize in #25458 + let path = if is_path { + resolve_from_known_cwd(Path::new(text), cwd) + } else { + match which::which_in(text, std::env::var_os("PATH"), cwd) { + Ok(path) => path, + Err(err) => match err { + which::Error::BadAbsolutePath | which::Error::BadRelativePath => { + return Err(err); + } + which::Error::CannotFindBinaryPath + | which::Error::CannotGetCurrentDir + | which::Error::CannotCanonicalize => { + return Ok(AllowRunDescriptorParseResult::Unresolved(Box::new(err))) + } + }, + } + }; + Ok(AllowRunDescriptorParseResult::Descriptor( + AllowRunDescriptor(path), + )) + } +} + #[derive(Clone, Eq, PartialEq, Hash, Debug)] -pub enum RunDescriptor { +pub enum DenyRunDescriptor { /// Warning: You may want to construct with `RunDescriptor::from()` for case /// handling. Name(String), @@ -889,98 +1238,69 @@ pub enum RunDescriptor { Path(PathBuf), } -impl From for RunDescriptorArg { - fn from(s: String) -> Self { - #[cfg(windows)] - let s = s.to_lowercase(); - let is_path = s.contains('/'); - #[cfg(windows)] - let is_path = is_path || s.contains('\\') || Path::new(&s).is_absolute(); - if is_path { - Self::Path(resolve_from_cwd(Path::new(&s)).unwrap()) +impl DenyRunDescriptor { + pub fn parse(text: &str, cwd: &Path) -> Self { + if text.contains('/') || cfg!(windows) && text.contains('\\') { + let path = resolve_from_known_cwd(Path::new(&text), cwd); + DenyRunDescriptor::Path(path) } else { - match which::which(&s) { - Ok(path) => Self::Path(path), - Err(_) => Self::Name(s), - } + DenyRunDescriptor::Name(text.to_string()) } } } -impl Descriptor for RunDescriptor { - type Arg = RunDescriptorArg; - - fn check_in_permission( - &self, - perm: &mut UnaryPermission, - api_name: Option<&str>, - ) -> Result<(), AnyError> { - skip_check_if_is_permission_fully_granted!(perm); - perm.check_desc(Some(self), false, api_name, || None) - } - - fn parse(args: Option<&[Self::Arg]>) -> Result, AnyError> { - parse_run_list(args) - } - - fn flag_name() -> &'static str { - "run" - } - - fn name(&self) -> Cow { - Cow::from(self.to_string()) +fn is_path(text: &str) -> bool { + if cfg!(windows) { + text.contains('/') || text.contains('\\') || Path::new(text).is_absolute() + } else { + text.contains('/') } } -impl From for RunDescriptor { - fn from(s: String) -> Self { - #[cfg(windows)] - let s = s.to_lowercase(); - let is_path = s.contains('/'); - #[cfg(windows)] - let is_path = is_path || s.contains('\\') || Path::new(&s).is_absolute(); - if is_path { - Self::Path(resolve_from_cwd(Path::new(&s)).unwrap()) - } else { - match which::which(&s) { - Ok(path) => Self::Path(path), - Err(_) => Self::Name(s), - } - } +fn denies_run_name(name: &str, cmd_path: &Path) -> bool { + let Some(file_stem) = cmd_path.file_stem() else { + return false; + }; + let Some(file_stem) = file_stem.to_str() else { + return false; + }; + if file_stem.len() < name.len() { + return false; } -} - -impl From for RunDescriptor { - fn from(p: PathBuf) -> Self { - #[cfg(windows)] - let p = PathBuf::from(p.to_string_lossy().to_string().to_lowercase()); - Self::Path(resolve_from_cwd(&p).unwrap()) - } -} - -impl std::fmt::Display for RunDescriptor { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - RunDescriptor::Name(s) => f.write_str(s), - RunDescriptor::Path(p) => f.write_str(&p.display().to_string()), - } - } -} - -impl AsRef for RunDescriptor { - fn as_ref(&self) -> &Path { - match self { - RunDescriptor::Name(s) => s.as_ref(), - RunDescriptor::Path(s) => s.as_ref(), - } + let (prefix, suffix) = file_stem.split_at(name.len()); + if !prefix.eq_ignore_ascii_case(name) { + return false; } + // be broad and consider anything like `deno.something` as matching deny perms + suffix.is_empty() || suffix.starts_with('.') } #[derive(Clone, Eq, PartialEq, Hash, Debug)] pub struct SysDescriptor(pub String); -impl Descriptor for SysDescriptor { - type Arg = String; +impl QueryDescriptor for SysDescriptor { + type AllowDesc = SysDescriptor; + type DenyDesc = SysDescriptor; + + fn flag_name() -> &'static str { + "sys" + } + + fn display_name(&self) -> Cow { + Cow::from(self.0.to_string()) + } + + fn from_allow(allow: &Self::AllowDesc) -> Self { + allow.clone() + } + + fn as_allow(&self) -> Option { + Some(self.clone()) + } + + fn as_deny(&self) -> Self::DenyDesc { + self.clone() + } fn check_in_permission( &self, @@ -988,19 +1308,27 @@ impl Descriptor for SysDescriptor { api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(perm); - perm.check_desc(Some(self), false, api_name, || None) + perm.check_desc(Some(self), false, api_name) } - fn parse(list: Option<&[Self::Arg]>) -> Result, AnyError> { - parse_sys_list(list) + fn matches_allow(&self, other: &Self::AllowDesc) -> bool { + self == other } - fn flag_name() -> &'static str { - "sys" + fn matches_deny(&self, other: &Self::DenyDesc) -> bool { + self == other } - fn name(&self) -> Cow { - Cow::from(self.0.to_string()) + fn revokes(&self, other: &Self::AllowDesc) -> bool { + self == other + } + + fn stronger_than_deny(&self, other: &Self::DenyDesc) -> bool { + self == other + } + + fn overlaps_deny(&self, _other: &Self::DenyDesc) -> bool { + false } } @@ -1014,10 +1342,35 @@ pub fn parse_sys_kind(kind: &str) -> Result<&str, AnyError> { } #[derive(Clone, Eq, PartialEq, Hash, Debug)] -pub struct FfiDescriptor(pub PathBuf); +pub struct FfiQueryDescriptor(pub PathQueryDescriptor); -impl Descriptor for FfiDescriptor { - type Arg = PathBuf; +impl QueryDescriptor for FfiQueryDescriptor { + type AllowDesc = FfiDescriptor; + type DenyDesc = FfiDescriptor; + + fn flag_name() -> &'static str { + "ffi" + } + + fn display_name(&self) -> Cow { + Cow::Borrowed(&self.0.requested) + } + + fn from_allow(allow: &Self::AllowDesc) -> Self { + PathQueryDescriptor { + requested: allow.0.to_string_lossy().into_owned(), + resolved: allow.0.clone(), + } + .into_ffi() + } + + fn as_allow(&self) -> Option { + Some(FfiDescriptor(self.0.resolved.clone())) + } + + fn as_deny(&self) -> Self::DenyDesc { + FfiDescriptor(self.0.resolved.clone()) + } fn check_in_permission( &self, @@ -1025,175 +1378,118 @@ impl Descriptor for FfiDescriptor { api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(perm); - perm.check_desc(Some(self), true, api_name, || None) + perm.check_desc(Some(self), true, api_name) } - fn parse(list: Option<&[Self::Arg]>) -> Result, AnyError> { - parse_path_list(list, FfiDescriptor) + fn matches_allow(&self, other: &Self::AllowDesc) -> bool { + self.0.resolved.starts_with(&other.0) } - fn flag_name() -> &'static str { - "ffi" + fn matches_deny(&self, other: &Self::DenyDesc) -> bool { + self.0.resolved.starts_with(&other.0) } - fn name(&self) -> Cow { - Cow::from(self.0.display().to_string()) + fn revokes(&self, other: &Self::AllowDesc) -> bool { + self.matches_allow(other) } - fn stronger_than(&self, other: &Self) -> bool { - other.0.starts_with(&self.0) + fn stronger_than_deny(&self, other: &Self::DenyDesc) -> bool { + other.0.starts_with(&self.0.resolved) + } + + fn overlaps_deny(&self, other: &Self::DenyDesc) -> bool { + self.stronger_than_deny(other) } } -impl UnaryPermission { - pub fn query(&self, path: Option<&Path>) -> PermissionState { - self.query_desc( - path - .map(|p| ReadDescriptor(resolve_from_cwd(p).unwrap())) - .as_ref(), - AllowPartial::TreatAsPartialGranted, - ) +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub struct FfiDescriptor(pub PathBuf); + +impl UnaryPermission { + pub fn query(&self, desc: Option<&ReadQueryDescriptor>) -> PermissionState { + self.query_desc(desc, AllowPartial::TreatAsPartialGranted) } - pub fn request(&mut self, path: Option<&Path>) -> PermissionState { - self.request_desc( - path - .map(|p| ReadDescriptor(resolve_from_cwd(p).unwrap())) - .as_ref(), - || Some(path?.display().to_string()), - ) + pub fn request( + &mut self, + path: Option<&ReadQueryDescriptor>, + ) -> PermissionState { + self.request_desc(path) } - pub fn revoke(&mut self, path: Option<&Path>) -> PermissionState { - self.revoke_desc( - path - .map(|p| ReadDescriptor(resolve_from_cwd(p).unwrap())) - .as_ref(), - ) + pub fn revoke( + &mut self, + desc: Option<&ReadQueryDescriptor>, + ) -> PermissionState { + self.revoke_desc(desc) } pub fn check( &mut self, - path: &Path, + desc: &ReadQueryDescriptor, api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc( - Some(&ReadDescriptor(resolve_from_cwd(path)?)), - true, - api_name, - || Some(format!("\"{}\"", path.display())), - ) + self.check_desc(Some(desc), true, api_name) } #[inline] pub fn check_partial( &mut self, - path: &Path, + desc: &ReadQueryDescriptor, api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - let desc = ReadDescriptor(resolve_from_cwd(path)?); - self.check_desc(Some(&desc), false, api_name, || { - Some(format!("\"{}\"", path.display())) - }) - } - - /// As `check()`, but permission error messages will anonymize the path - /// by replacing it with the given `display`. - pub fn check_blind( - &mut self, - path: &Path, - display: &str, - api_name: &str, - ) -> Result<(), AnyError> { - skip_check_if_is_permission_fully_granted!(self); - let desc = ReadDescriptor(resolve_from_cwd(path)?); - self.check_desc(Some(&desc), false, Some(api_name), || { - Some(format!("<{display}>")) - }) + self.check_desc(Some(desc), false, api_name) } pub fn check_all(&mut self, api_name: Option<&str>) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc(None, false, api_name, || None) + self.check_desc(None, false, api_name) } } -impl UnaryPermission { - pub fn query(&self, path: Option<&Path>) -> PermissionState { - self.query_desc( - path - .map(|p| WriteDescriptor(resolve_from_cwd(p).unwrap())) - .as_ref(), - AllowPartial::TreatAsPartialGranted, - ) +impl UnaryPermission { + pub fn query(&self, path: Option<&WriteQueryDescriptor>) -> PermissionState { + self.query_desc(path, AllowPartial::TreatAsPartialGranted) } - pub fn request(&mut self, path: Option<&Path>) -> PermissionState { - self.request_desc( - path - .map(|p| WriteDescriptor(resolve_from_cwd(p).unwrap())) - .as_ref(), - || Some(path?.display().to_string()), - ) + pub fn request( + &mut self, + path: Option<&WriteQueryDescriptor>, + ) -> PermissionState { + self.request_desc(path) } - pub fn revoke(&mut self, path: Option<&Path>) -> PermissionState { - self.revoke_desc( - path - .map(|p| WriteDescriptor(resolve_from_cwd(p).unwrap())) - .as_ref(), - ) + pub fn revoke( + &mut self, + path: Option<&WriteQueryDescriptor>, + ) -> PermissionState { + self.revoke_desc(path) } pub fn check( &mut self, - path: &Path, + path: &WriteQueryDescriptor, api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc( - Some(&WriteDescriptor(resolve_from_cwd(path)?)), - true, - api_name, - || Some(format!("\"{}\"", path.display())), - ) + self.check_desc(Some(path), true, api_name) } #[inline] pub fn check_partial( &mut self, - path: &Path, + path: &WriteQueryDescriptor, api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc( - Some(&WriteDescriptor(resolve_from_cwd(path)?)), - false, - api_name, - || Some(format!("\"{}\"", path.display())), - ) - } - - /// As `check()`, but permission error messages will anonymize the path - /// by replacing it with the given `display`. - pub fn check_blind( - &mut self, - path: &Path, - display: &str, - api_name: &str, - ) -> Result<(), AnyError> { - skip_check_if_is_permission_fully_granted!(self); - let desc = WriteDescriptor(resolve_from_cwd(path)?); - self.check_desc(Some(&desc), false, Some(api_name), || { - Some(format!("<{display}>")) - }) + self.check_desc(Some(path), false, api_name) } pub fn check_all(&mut self, api_name: Option<&str>) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc(None, false, api_name, || None) + self.check_desc(None, false, api_name) } } @@ -1203,7 +1499,7 @@ impl UnaryPermission { } pub fn request(&mut self, host: Option<&NetDescriptor>) -> PermissionState { - self.request_desc(host, || None) + self.request_desc(host) } pub fn revoke(&mut self, host: Option<&NetDescriptor>) -> PermissionState { @@ -1216,7 +1512,7 @@ impl UnaryPermission { api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc(Some(host), false, api_name, || None) + self.check_desc(Some(host), false, api_name) } pub fn check_url( @@ -1231,14 +1527,12 @@ impl UnaryPermission { let host = Host::parse(host)?; let port = url.port_or_known_default(); let descriptor = NetDescriptor(host, port); - self.check_desc(Some(&descriptor), false, api_name, || { - Some(format!("\"{descriptor}\"")) - }) + self.check_desc(Some(&descriptor), false, api_name) } pub fn check_all(&mut self) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc(None, false, None, || None) + self.check_desc(None, false, None) } } @@ -1251,7 +1545,7 @@ impl UnaryPermission { } pub fn request(&mut self, env: Option<&str>) -> PermissionState { - self.request_desc(env.map(EnvDescriptor::new).as_ref(), || None) + self.request_desc(env.map(EnvDescriptor::new).as_ref()) } pub fn revoke(&mut self, env: Option<&str>) -> PermissionState { @@ -1264,12 +1558,12 @@ impl UnaryPermission { api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc(Some(&EnvDescriptor::new(env)), false, api_name, || None) + self.check_desc(Some(&EnvDescriptor::new(env)), false, api_name) } pub fn check_all(&mut self) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc(None, false, None, || None) + self.check_desc(None, false, None) } } @@ -1282,10 +1576,7 @@ impl UnaryPermission { } pub fn request(&mut self, kind: Option<&str>) -> PermissionState { - self - .request_desc(kind.map(|k| SysDescriptor(k.to_string())).as_ref(), || { - None - }) + self.request_desc(kind.map(|k| SysDescriptor(k.to_string())).as_ref()) } pub fn revoke(&mut self, kind: Option<&str>) -> PermissionState { @@ -1298,57 +1589,44 @@ impl UnaryPermission { api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc( - Some(&SysDescriptor(kind.to_string())), - false, - api_name, - || None, - ) + self.check_desc(Some(&SysDescriptor(kind.to_string())), false, api_name) } pub fn check_all(&mut self) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc(None, false, None, || None) + self.check_desc(None, false, None) } } -impl UnaryPermission { - pub fn query(&self, cmd: Option<&str>) -> PermissionState { - self.query_desc( - cmd.map(|c| RunDescriptor::from(c.to_string())).as_ref(), - AllowPartial::TreatAsPartialGranted, - ) +impl UnaryPermission { + pub fn query(&self, cmd: Option<&RunQueryDescriptor>) -> PermissionState { + self.query_desc(cmd, AllowPartial::TreatAsPartialGranted) } - pub fn request(&mut self, cmd: Option<&str>) -> PermissionState { - self.request_desc( - cmd.map(|c| RunDescriptor::from(c.to_string())).as_ref(), - || Some(cmd?.to_string()), - ) + pub fn request( + &mut self, + cmd: Option<&RunQueryDescriptor>, + ) -> PermissionState { + self.request_desc(cmd) } - pub fn revoke(&mut self, cmd: Option<&str>) -> PermissionState { - self.revoke_desc(cmd.map(|c| RunDescriptor::from(c.to_string())).as_ref()) + pub fn revoke( + &mut self, + cmd: Option<&RunQueryDescriptor>, + ) -> PermissionState { + self.revoke_desc(cmd) } pub fn check( &mut self, - cmd: RunPathQuery, + cmd: &RunQueryDescriptor, api_name: Option<&str>, ) -> Result<(), AnyError> { - debug_assert!(cmd.resolved.is_absolute()); - skip_check_if_is_permission_fully_granted!(self); - self.check_desc( - Some(&RunDescriptor::Path(cmd.resolved.to_path_buf())), - false, - api_name, - || Some(format!("\"{}\"", cmd.requested)), - ) + self.check_desc(Some(cmd), false, api_name) } pub fn check_all(&mut self, api_name: Option<&str>) -> Result<(), AnyError> { - skip_check_if_is_permission_fully_granted!(self); - self.check_desc(None, false, api_name, || None) + self.check_desc(None, false, api_name) } /// Queries without prompting @@ -1358,7 +1636,7 @@ impl UnaryPermission { } let (result, _prompted, _is_allow_all) = self.query_desc(None, AllowPartial::TreatAsDenied).check2( - RunDescriptor::flag_name(), + RunQueryDescriptor::flag_name(), api_name, || None, /* prompt */ false, @@ -1367,73 +1645,57 @@ impl UnaryPermission { } } -impl UnaryPermission { - pub fn query(&self, path: Option<&Path>) -> PermissionState { - self.query_desc( - path - .map(|p| FfiDescriptor(resolve_from_cwd(p).unwrap())) - .as_ref(), - AllowPartial::TreatAsPartialGranted, - ) +impl UnaryPermission { + pub fn query(&self, path: Option<&FfiQueryDescriptor>) -> PermissionState { + self.query_desc(path, AllowPartial::TreatAsPartialGranted) } - pub fn request(&mut self, path: Option<&Path>) -> PermissionState { - self.request_desc( - path - .map(|p| FfiDescriptor(resolve_from_cwd(p).unwrap())) - .as_ref(), - || Some(path?.display().to_string()), - ) + pub fn request( + &mut self, + path: Option<&FfiQueryDescriptor>, + ) -> PermissionState { + self.request_desc(path) } - pub fn revoke(&mut self, path: Option<&Path>) -> PermissionState { - self.revoke_desc( - path - .map(|p| FfiDescriptor(resolve_from_cwd(p).unwrap())) - .as_ref(), - ) + pub fn revoke( + &mut self, + path: Option<&FfiQueryDescriptor>, + ) -> PermissionState { + self.revoke_desc(path) } pub fn check( &mut self, - path: &Path, + path: &FfiQueryDescriptor, api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc( - Some(&FfiDescriptor(resolve_from_cwd(path)?)), - true, - api_name, - || Some(format!("\"{}\"", path.display())), - ) + self.check_desc(Some(path), true, api_name) } - pub fn check_partial(&mut self, path: Option<&Path>) -> Result<(), AnyError> { + pub fn check_partial( + &mut self, + path: Option<&FfiQueryDescriptor>, + ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - let desc = match path { - Some(path) => Some(FfiDescriptor(resolve_from_cwd(path)?)), - None => None, - }; - self.check_desc(desc.as_ref(), false, None, || { - Some(format!("\"{}\"", path?.display())) - }) + self.check_desc(path, false, None) } pub fn check_all(&mut self) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc(None, false, Some("all"), || None) + self.check_desc(None, false, Some("all")) } } #[derive(Clone, Debug, Eq, PartialEq)] pub struct Permissions { - pub read: UnaryPermission, - pub write: UnaryPermission, + pub read: UnaryPermission, + pub write: UnaryPermission, pub net: UnaryPermission, pub env: UnaryPermission, pub sys: UnaryPermission, - pub run: UnaryPermission, - pub ffi: UnaryPermission, + pub run: UnaryPermission, + pub ffi: UnaryPermission, pub all: UnitPermission, } @@ -1444,33 +1706,33 @@ pub struct PermissionsOptions { pub deny_env: Option>, pub allow_net: Option>, pub deny_net: Option>, - pub allow_ffi: Option>, - pub deny_ffi: Option>, - pub allow_read: Option>, - pub deny_read: Option>, - pub allow_run: Option>, + pub allow_ffi: Option>, + pub deny_ffi: Option>, + pub allow_read: Option>, + pub deny_read: Option>, + pub allow_run: Option>, pub deny_run: Option>, pub allow_sys: Option>, pub deny_sys: Option>, - pub allow_write: Option>, - pub deny_write: Option>, + pub allow_write: Option>, + pub deny_write: Option>, pub prompt: bool, } impl Permissions { - pub fn new_unary( - allow_list: Option<&[T::Arg]>, - deny_list: Option<&[T::Arg]>, + pub fn new_unary( + allow_list: Option>, + deny_list: Option>, prompt: bool, - ) -> Result, AnyError> + ) -> Result, AnyError> where - T: Descriptor + Hash, + TQuery: QueryDescriptor, { - Ok(UnaryPermission:: { - granted_global: global_from_option(allow_list), - granted_list: T::parse(allow_list)?, - flag_denied_global: global_from_option(deny_list), - flag_denied_list: T::parse(deny_list)?, + Ok(UnaryPermission:: { + granted_global: global_from_option(allow_list.as_ref()), + granted_list: allow_list.unwrap_or_default(), + flag_denied_global: global_from_option(deny_list.as_ref()), + flag_denied_list: deny_list.unwrap_or_default(), prompt, ..Default::default() }) @@ -1486,57 +1748,140 @@ impl Permissions { ) } - pub fn from_options(opts: &PermissionsOptions) -> Result { + pub fn from_options( + parser: &dyn PermissionDescriptorParser, + opts: &PermissionsOptions, + ) -> Result { + fn resolve_allow_run( + parser: &dyn PermissionDescriptorParser, + allow_run: &[String], + ) -> Result, AnyError> { + let mut new_allow_run = HashSet::with_capacity(allow_run.len()); + for unresolved in allow_run { + if unresolved.is_empty() { + bail!("Empty command name not allowed in --allow-run=...") + } + match parser.parse_allow_run_descriptor(unresolved)? { + AllowRunDescriptorParseResult::Descriptor(descriptor) => { + new_allow_run.insert(descriptor); + } + AllowRunDescriptorParseResult::Unresolved(err) => { + log::info!( + "{} Failed to resolve '{}' for allow-run: {}", + colors::gray("Info"), + unresolved, + err + ); + } + } + } + Ok(new_allow_run) + } + + fn parse_maybe_vec( + items: Option<&[String]>, + parse: impl Fn(&str) -> Result, + ) -> Result>, AnyError> { + match items { + Some(items) => Ok(Some( + items + .iter() + .map(|item| parse(item)) + .collect::, _>>()?, + )), + None => Ok(None), + } + } + + let mut deny_write = parse_maybe_vec(opts.deny_write.as_deref(), |item| { + parser.parse_write_descriptor(item) + })?; + let allow_run = opts + .allow_run + .as_ref() + .and_then(|raw_allow_run| { + match resolve_allow_run(parser, raw_allow_run) { + Ok(resolved_allow_run) => { + if resolved_allow_run.is_empty() && !raw_allow_run.is_empty() { + None // convert to no permissions if now empty + } else { + Some(Ok(resolved_allow_run)) + } + } + Err(err) => Some(Err(err)), + } + }) + .transpose()?; + // add the allow_run list to deny_write + if let Some(allow_run_vec) = &allow_run { + if !allow_run_vec.is_empty() { + let deny_write = deny_write.get_or_insert_with(Default::default); + deny_write.extend( + allow_run_vec + .iter() + .map(|item| WriteDescriptor(item.0.clone())), + ); + } + } + Ok(Self { read: Permissions::new_unary( - opts.allow_read.as_deref(), - opts.deny_read.as_deref(), + parse_maybe_vec(opts.allow_read.as_deref(), |item| { + parser.parse_read_descriptor(item) + })?, + parse_maybe_vec(opts.deny_read.as_deref(), |item| { + parser.parse_read_descriptor(item) + })?, opts.prompt, )?, write: Permissions::new_unary( - opts.allow_write.as_deref(), - opts.deny_write.as_deref(), + parse_maybe_vec(opts.allow_write.as_deref(), |item| { + parser.parse_write_descriptor(item) + })?, + deny_write, opts.prompt, )?, net: Permissions::new_unary( - opts.allow_net.as_deref(), - opts.deny_net.as_deref(), + parse_maybe_vec(opts.allow_net.as_deref(), |item| { + parser.parse_net_descriptor(item) + })?, + parse_maybe_vec(opts.deny_net.as_deref(), |item| { + parser.parse_net_descriptor(item) + })?, opts.prompt, )?, env: Permissions::new_unary( - opts.allow_env.as_deref(), - opts.deny_env.as_deref(), + parse_maybe_vec(opts.allow_env.as_deref(), |item| { + parser.parse_env_descriptor(item) + })?, + parse_maybe_vec(opts.deny_env.as_deref(), |text| { + parser.parse_env_descriptor(text) + })?, opts.prompt, )?, sys: Permissions::new_unary( - opts.allow_sys.as_deref(), - opts.deny_sys.as_deref(), + parse_maybe_vec(opts.allow_sys.as_deref(), |text| { + parser.parse_sys_descriptor(text) + })?, + parse_maybe_vec(opts.deny_sys.as_deref(), |text| { + parser.parse_sys_descriptor(text) + })?, opts.prompt, )?, run: Permissions::new_unary( - opts - .allow_run - .as_ref() - .map(|d| { - d.iter() - .map(|s| RunDescriptorArg::Path(s.clone())) - .collect::>() - }) - .as_deref(), - opts - .deny_run - .as_ref() - .map(|d| { - d.iter() - .map(|s| RunDescriptorArg::from(s.clone())) - .collect::>() - }) - .as_deref(), + allow_run, + parse_maybe_vec(opts.deny_run.as_deref(), |text| { + parser.parse_deny_run_descriptor(text) + })?, opts.prompt, )?, ffi: Permissions::new_unary( - opts.allow_ffi.as_deref(), - opts.deny_ffi.as_deref(), + parse_maybe_vec(opts.allow_ffi.as_deref(), |text| { + parser.parse_ffi_descriptor(text) + })?, + parse_maybe_vec(opts.deny_ffi.as_deref(), |text| { + parser.parse_ffi_descriptor(text) + })?, opts.prompt, )?, all: Permissions::new_all(opts.allow_all), @@ -1588,7 +1933,14 @@ impl Permissions { ) -> Result<(), AnyError> { match specifier.scheme() { "file" => match specifier.to_file_path() { - Ok(path) => self.read.check(&path, Some("import()")), + Ok(path) => self.read.check( + &PathQueryDescriptor { + requested: path.to_string_lossy().into_owned(), + resolved: path, + } + .into_read(), + Some("import()"), + ), Err(_) => Err(uri_error(format!( "Invalid file path.\n Specifier: {specifier}" ))), @@ -1607,15 +1959,30 @@ impl Permissions { /// case might need to be mutated). Also for the Web Worker API we need a way /// to send permissions to a new thread. #[derive(Clone, Debug)] -pub struct PermissionsContainer(pub Arc>); +pub struct PermissionsContainer { + // todo(dsherret): make both of these private as the functionality + // can just be methods on PermissionsContainer. Additionally, a separate + // struct should be created in here that handles creating child permissions + // so that the code is not so verbose elsewhere. + pub descriptor_parser: Arc, + pub inner: Arc>, +} impl PermissionsContainer { - pub fn new(perms: Permissions) -> Self { - Self(Arc::new(Mutex::new(perms))) + pub fn new( + descriptor_parser: Arc, + perms: Permissions, + ) -> Self { + Self { + descriptor_parser, + inner: Arc::new(Mutex::new(perms)), + } } - pub fn allow_all() -> Self { - Self::new(Permissions::allow_all()) + pub fn allow_all( + descriptor_parser: Arc, + ) -> Self { + Self::new(descriptor_parser, Permissions::allow_all()) } #[inline(always)] @@ -1623,27 +1990,61 @@ impl PermissionsContainer { &self, specifier: &ModuleSpecifier, ) -> Result<(), AnyError> { - self.0.lock().check_specifier(specifier) + self.inner.lock().check_specifier(specifier) } + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] #[inline(always)] pub fn check_read( - &mut self, - path: &Path, + &self, + path: &str, api_name: &str, - ) -> Result<(), AnyError> { - self.0.lock().read.check(path, Some(api_name)) + ) -> Result { + self.check_read_with_api_name(path, Some(api_name)) } + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] #[inline(always)] pub fn check_read_with_api_name( &self, - path: &Path, + path: &str, api_name: Option<&str>, - ) -> Result<(), AnyError> { - self.0.lock().read.check(path, api_name) + ) -> Result { + let mut inner = self.inner.lock(); + let inner = &mut inner.read; + if inner.is_allow_all() { + Ok(PathBuf::from(path)) + } else { + let desc = self.descriptor_parser.parse_path_query(path)?.into_read(); + inner.check(&desc, api_name)?; + Ok(desc.0.resolved) + } } + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] + #[inline(always)] + pub fn check_read_path<'a>( + &self, + path: &'a Path, + api_name: Option<&str>, + ) -> Result, AnyError> { + let mut inner = self.inner.lock(); + let inner = &mut inner.read; + if inner.is_allow_all() { + Ok(Cow::Borrowed(path)) + } else { + let desc = PathQueryDescriptor { + requested: path.to_string_lossy().into_owned(), + resolved: path.to_path_buf(), + } + .into_read(); + inner.check(&desc, api_name)?; + Ok(Cow::Owned(desc.0.resolved)) + } + } + + /// As `check_read()`, but permission error messages will anonymize the path + /// by replacing it with the given `display`. #[inline(always)] pub fn check_read_blind( &mut self, @@ -1651,105 +2052,167 @@ impl PermissionsContainer { display: &str, api_name: &str, ) -> Result<(), AnyError> { - self.0.lock().read.check_blind(path, display, api_name) + let mut inner = self.inner.lock(); + let inner = &mut inner.read; + skip_check_if_is_permission_fully_granted!(inner); + inner.check( + &PathQueryDescriptor { + requested: format!("<{}>", display), + resolved: path.to_path_buf(), + } + .into_read(), + Some(api_name), + ) } #[inline(always)] - pub fn check_read_all(&mut self, api_name: &str) -> Result<(), AnyError> { - self.0.lock().read.check_all(Some(api_name)) + pub fn check_read_all(&self, api_name: &str) -> Result<(), AnyError> { + self.inner.lock().read.check_all(Some(api_name)) } + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] #[inline(always)] pub fn check_write( - &mut self, - path: &Path, + &self, + path: &str, api_name: &str, - ) -> Result<(), AnyError> { - self.0.lock().write.check(path, Some(api_name)) + ) -> Result { + self.check_write_with_api_name(path, Some(api_name)) } + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] #[inline(always)] pub fn check_write_with_api_name( &self, - path: &Path, + path: &str, api_name: Option<&str>, - ) -> Result<(), AnyError> { - self.0.lock().write.check(path, api_name) + ) -> Result { + let mut inner = self.inner.lock(); + let inner = &mut inner.write; + if inner.is_allow_all() { + Ok(PathBuf::from(path)) + } else { + let desc = self.descriptor_parser.parse_path_query(path)?.into_write(); + inner.check(&desc, api_name)?; + Ok(desc.0.resolved) + } + } + + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] + #[inline(always)] + pub fn check_write_path<'a>( + &self, + path: &'a Path, + api_name: &str, + ) -> Result, AnyError> { + let mut inner = self.inner.lock(); + let inner = &mut inner.write; + if inner.is_allow_all() { + Ok(Cow::Borrowed(path)) + } else { + let desc = PathQueryDescriptor { + requested: path.to_string_lossy().into_owned(), + resolved: path.to_path_buf(), + } + .into_write(); + inner.check(&desc, Some(api_name))?; + Ok(Cow::Owned(desc.0.resolved)) + } } #[inline(always)] - pub fn check_write_all(&mut self, api_name: &str) -> Result<(), AnyError> { - self.0.lock().write.check_all(Some(api_name)) + pub fn check_write_all(&self, api_name: &str) -> Result<(), AnyError> { + self.inner.lock().write.check_all(Some(api_name)) } + /// As `check_write()`, but permission error messages will anonymize the path + /// by replacing it with the given `display`. #[inline(always)] pub fn check_write_blind( - &mut self, + &self, path: &Path, display: &str, api_name: &str, ) -> Result<(), AnyError> { - self.0.lock().write.check_blind(path, display, api_name) + let mut inner = self.inner.lock(); + let inner = &mut inner.write; + skip_check_if_is_permission_fully_granted!(inner); + inner.check( + &PathQueryDescriptor { + requested: format!("<{}>", display), + resolved: path.to_path_buf(), + } + .into_write(), + Some(api_name), + ) } #[inline(always)] pub fn check_write_partial( &mut self, - path: &Path, + path: &str, api_name: &str, - ) -> Result<(), AnyError> { - self.0.lock().write.check_partial(path, Some(api_name)) + ) -> Result { + let mut inner = self.inner.lock(); + let inner = &mut inner.write; + if inner.is_allow_all() { + Ok(PathBuf::from(path)) + } else { + let desc = self.descriptor_parser.parse_path_query(path)?.into_write(); + inner.check_partial(&desc, Some(api_name))?; + Ok(desc.0.resolved) + } } #[inline(always)] pub fn check_run( &mut self, - cmd: RunPathQuery, + cmd: &RunQueryDescriptor, api_name: &str, ) -> Result<(), AnyError> { - self.0.lock().run.check(cmd, Some(api_name)) + self.inner.lock().run.check(cmd, Some(api_name)) } #[inline(always)] pub fn check_run_all(&mut self, api_name: &str) -> Result<(), AnyError> { - self.0.lock().run.check_all(Some(api_name)) + self.inner.lock().run.check_all(Some(api_name)) } #[inline(always)] pub fn query_run_all(&mut self, api_name: &str) -> bool { - self.0.lock().run.query_all(Some(api_name)) + self.inner.lock().run.query_all(Some(api_name)) } #[inline(always)] pub fn check_sys(&self, kind: &str, api_name: &str) -> Result<(), AnyError> { - self.0.lock().sys.check(kind, Some(api_name)) + self.inner.lock().sys.check(kind, Some(api_name)) } #[inline(always)] pub fn check_env(&mut self, var: &str) -> Result<(), AnyError> { - self.0.lock().env.check(var, None) + self.inner.lock().env.check(var, None) } #[inline(always)] pub fn check_env_all(&mut self) -> Result<(), AnyError> { - self.0.lock().env.check_all() + self.inner.lock().env.check_all() } #[inline(always)] pub fn check_sys_all(&mut self) -> Result<(), AnyError> { - self.0.lock().sys.check_all() + self.inner.lock().sys.check_all() } #[inline(always)] pub fn check_ffi_all(&mut self) -> Result<(), AnyError> { - self.0.lock().ffi.check_all() + self.inner.lock().ffi.check_all() } /// This checks to see if the allow-all flag was passed, not whether all /// permissions are enabled! #[inline(always)] pub fn check_was_allow_all_flag_passed(&mut self) -> Result<(), AnyError> { - self.0.lock().all.check() + self.inner.lock().all.check() } /// Checks special file access, returning the failed permission type if @@ -1862,7 +2325,7 @@ impl PermissionsContainer { url: &Url, api_name: &str, ) -> Result<(), AnyError> { - self.0.lock().net.check_url(url, Some(api_name)) + self.inner.lock().net.check_url(url, Some(api_name)) } #[inline(always)] @@ -1871,22 +2334,54 @@ impl PermissionsContainer { host: &(T, Option), api_name: &str, ) -> Result<(), AnyError> { + let mut inner = self.inner.lock(); + let inner = &mut inner.net; + skip_check_if_is_permission_fully_granted!(inner); let hostname = Host::parse(host.0.as_ref())?; let descriptor = NetDescriptor(hostname, host.1); - self.0.lock().net.check(&descriptor, Some(api_name)) + inner.check(&descriptor, Some(api_name)) } #[inline(always)] - pub fn check_ffi(&mut self, path: Option<&Path>) -> Result<(), AnyError> { - self.0.lock().ffi.check(path.unwrap(), None) + pub fn check_ffi(&mut self, path: &str) -> Result { + let mut inner = self.inner.lock(); + let inner = &mut inner.ffi; + if inner.is_allow_all() { + Ok(PathBuf::from(path)) + } else { + let desc = self.descriptor_parser.parse_path_query(path)?.into_ffi(); + inner.check(&desc, None)?; + Ok(desc.0.resolved) + } } + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] #[inline(always)] - pub fn check_ffi_partial( + pub fn check_ffi_partial_no_path(&mut self) -> Result<(), AnyError> { + let mut inner = self.inner.lock(); + let inner = &mut inner.ffi; + if inner.is_allow_all() { + Ok(()) + } else { + inner.check_partial(None) + } + } + + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] + #[inline(always)] + pub fn check_ffi_partial_with_path( &mut self, - path: Option<&Path>, - ) -> Result<(), AnyError> { - self.0.lock().ffi.check_partial(path) + path: &str, + ) -> Result { + let mut inner = self.inner.lock(); + let inner = &mut inner.ffi; + if inner.is_allow_all() { + Ok(PathBuf::from(path)) + } else { + let desc = self.descriptor_parser.parse_path_query(path)?.into_ffi(); + inner.check_partial(Some(&desc))?; + Ok(desc.0.resolved) + } } } @@ -1911,93 +2406,10 @@ const fn unit_permission_from_flag_bools( } } -fn global_from_option(flag: Option<&[T]>) -> bool { +fn global_from_option(flag: Option<&HashSet>) -> bool { matches!(flag, Some(v) if v.is_empty()) } -fn parse_net_list( - list: Option<&[String]>, -) -> Result, AnyError> { - if let Some(v) = list { - v.iter() - .map(|x| NetDescriptor::parse(x)) - .collect::, AnyError>>() - } else { - Ok(HashSet::new()) - } -} - -fn parse_env_list( - list: Option<&[String]>, -) -> Result, AnyError> { - if let Some(v) = list { - v.iter() - .map(|x| { - if x.is_empty() { - Err(AnyError::msg("Empty path is not allowed")) - } else { - Ok(EnvDescriptor::new(x)) - } - }) - .collect() - } else { - Ok(HashSet::new()) - } -} - -fn parse_path_list( - list: Option<&[PathBuf]>, - f: fn(PathBuf) -> T, -) -> Result, AnyError> { - if let Some(v) = list { - v.iter() - .map(|raw_path| { - if raw_path.as_os_str().is_empty() { - Err(AnyError::msg("Empty path is not allowed")) - } else { - resolve_from_cwd(Path::new(&raw_path)).map(f) - } - }) - .collect() - } else { - Ok(HashSet::new()) - } -} - -fn parse_sys_list( - list: Option<&[String]>, -) -> Result, AnyError> { - if let Some(v) = list { - v.iter() - .map(|x| { - if x.is_empty() { - Err(AnyError::msg("empty")) - } else { - Ok(SysDescriptor(x.to_string())) - } - }) - .collect() - } else { - Ok(HashSet::new()) - } -} - -fn parse_run_list( - list: Option<&[RunDescriptorArg]>, -) -> Result, AnyError> { - let Some(v) = list else { - return Ok(HashSet::new()); - }; - Ok( - v.iter() - .map(|arg| match arg { - RunDescriptorArg::Name(s) => RunDescriptor::Name(s.clone()), - RunDescriptorArg::Path(l) => RunDescriptor::Path(l.clone()), - }) - .collect(), - ) -} - fn escalation_error() -> AnyError { custom_error("NotCapable", "Can't escalate parent thread permissions") } @@ -2246,7 +2658,58 @@ impl<'de> Deserialize<'de> for ChildPermissionsArg { } } +/// Parses and normalizes permissions. +/// +/// This trait is necessary because this crate doesn't have access +/// to the file system. +pub trait PermissionDescriptorParser: Debug + Send + Sync { + fn parse_read_descriptor( + &self, + text: &str, + ) -> Result; + + fn parse_write_descriptor( + &self, + text: &str, + ) -> Result; + + fn parse_net_descriptor(&self, text: &str) + -> Result; + + fn parse_env_descriptor(&self, text: &str) + -> Result; + + fn parse_sys_descriptor(&self, text: &str) + -> Result; + + fn parse_allow_run_descriptor( + &self, + text: &str, + ) -> Result; + + fn parse_deny_run_descriptor( + &self, + text: &str, + ) -> Result; + + fn parse_ffi_descriptor(&self, text: &str) + -> Result; + + // queries + + fn parse_path_query( + &self, + path: &str, + ) -> Result; + + fn parse_run_query( + &self, + requested: &str, + ) -> Result; +} + pub fn create_child_permissions( + parser: &dyn PermissionDescriptorParser, main_perms: &mut Permissions, child_permissions_arg: ChildPermissionsArg, ) -> Result { @@ -2287,25 +2750,41 @@ pub fn create_child_permissions( // in the worker_perms.all block above worker_perms.read = main_perms .read - .create_child_permissions(child_permissions_arg.read)?; + .create_child_permissions(child_permissions_arg.read, |text| { + Ok(Some(parser.parse_read_descriptor(text)?)) + })?; worker_perms.write = main_perms .write - .create_child_permissions(child_permissions_arg.write)?; + .create_child_permissions(child_permissions_arg.write, |text| { + Ok(Some(parser.parse_write_descriptor(text)?)) + })?; worker_perms.net = main_perms .net - .create_child_permissions(child_permissions_arg.net)?; + .create_child_permissions(child_permissions_arg.net, |text| { + Ok(Some(parser.parse_net_descriptor(text)?)) + })?; worker_perms.env = main_perms .env - .create_child_permissions(child_permissions_arg.env)?; + .create_child_permissions(child_permissions_arg.env, |text| { + Ok(Some(parser.parse_env_descriptor(text)?)) + })?; worker_perms.sys = main_perms .sys - .create_child_permissions(child_permissions_arg.sys)?; - worker_perms.run = main_perms - .run - .create_child_permissions(child_permissions_arg.run)?; + .create_child_permissions(child_permissions_arg.sys, |text| { + Ok(Some(parser.parse_sys_descriptor(text)?)) + })?; + worker_perms.run = main_perms.run.create_child_permissions( + child_permissions_arg.run, + |text| match parser.parse_allow_run_descriptor(text)? { + AllowRunDescriptorParseResult::Unresolved(_) => Ok(None), + AllowRunDescriptorParseResult::Descriptor(desc) => Ok(Some(desc)), + }, + )?; worker_perms.ffi = main_perms .ffi - .create_child_permissions(child_permissions_arg.ffi)?; + .create_child_permissions(child_permissions_arg.ffi, |text| { + Ok(Some(parser.parse_ffi_descriptor(text)?)) + })?; Ok(worker_perms) } @@ -2332,128 +2811,167 @@ mod tests { macro_rules! svec { ($($x:expr),*) => (vec![$($x.to_string()),*]); } - macro_rules! sarr { - ($($x:expr),*) => ([$($x.to_string()),*]); + + #[derive(Debug)] + struct TestPermissionDescriptorParser; + + impl TestPermissionDescriptorParser { + fn join_path_with_root(&self, path: &str) -> PathBuf { + if path.starts_with("C:\\") { + PathBuf::from(path) + } else { + PathBuf::from("/").join(path) + } + } + } + + impl PermissionDescriptorParser for TestPermissionDescriptorParser { + fn parse_read_descriptor( + &self, + text: &str, + ) -> Result { + Ok(ReadDescriptor(self.join_path_with_root(text))) + } + + fn parse_write_descriptor( + &self, + text: &str, + ) -> Result { + Ok(WriteDescriptor(self.join_path_with_root(text))) + } + + fn parse_net_descriptor( + &self, + text: &str, + ) -> Result { + NetDescriptor::parse(text) + } + + fn parse_env_descriptor( + &self, + text: &str, + ) -> Result { + Ok(EnvDescriptor::new(text)) + } + + fn parse_sys_descriptor( + &self, + text: &str, + ) -> Result { + Ok(SysDescriptor(text.to_string())) + } + + fn parse_allow_run_descriptor( + &self, + text: &str, + ) -> Result { + Ok(AllowRunDescriptorParseResult::Descriptor( + AllowRunDescriptor(self.join_path_with_root(text)), + )) + } + + fn parse_deny_run_descriptor( + &self, + text: &str, + ) -> Result { + if text.contains("/") { + Ok(DenyRunDescriptor::Path(self.join_path_with_root(text))) + } else { + Ok(DenyRunDescriptor::Name(text.to_string())) + } + } + + fn parse_ffi_descriptor( + &self, + text: &str, + ) -> Result { + Ok(FfiDescriptor(self.join_path_with_root(text))) + } + + fn parse_path_query( + &self, + path: &str, + ) -> Result { + Ok(PathQueryDescriptor { + resolved: self.join_path_with_root(path), + requested: path.to_string(), + }) + } + + fn parse_run_query( + &self, + requested: &str, + ) -> Result { + RunQueryDescriptor::parse(requested) + } } #[test] fn check_paths() { set_prompter(Box::new(TestPrompter)); - let allowlist = vec![ - PathBuf::from("/a/specific/dir/name"), - PathBuf::from("/a/specific"), - PathBuf::from("/b/c"), + let allowlist = svec!["/a/specific/dir/name", "/a/specific", "/b/c"]; + + let parser = TestPermissionDescriptorParser; + let perms = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_read: Some(allowlist.clone()), + allow_write: Some(allowlist.clone()), + allow_ffi: Some(allowlist), + ..Default::default() + }, + ) + .unwrap(); + let mut perms = PermissionsContainer::new(Arc::new(parser), perms); + + let cases = [ + // Inside of /a/specific and /a/specific/dir/name + ("/a/specific/dir/name", true), + // Inside of /a/specific but outside of /a/specific/dir/name + ("/a/specific/dir", true), + // Inside of /a/specific and /a/specific/dir/name + ("/a/specific/dir/name/inner", true), + // Inside of /a/specific but outside of /a/specific/dir/name + ("/a/specific/other/dir", true), + // Exact match with /b/c + ("/b/c", true), + // Sub path within /b/c + ("/b/c/sub/path", true), + // Sub path within /b/c, needs normalizing + ("/b/c/sub/path/../path/.", true), + // Inside of /b but outside of /b/c + ("/b/e", false), + // Inside of /a but outside of /a/specific + ("/a/b", false), ]; - let mut perms = Permissions::from_options(&PermissionsOptions { - allow_read: Some(allowlist.clone()), - allow_write: Some(allowlist.clone()), - allow_ffi: Some(allowlist), - ..Default::default() - }) - .unwrap(); - - // Inside of /a/specific and /a/specific/dir/name - assert!(perms - .read - .check(Path::new("/a/specific/dir/name"), None) - .is_ok()); - assert!(perms - .write - .check(Path::new("/a/specific/dir/name"), None) - .is_ok()); - assert!(perms - .ffi - .check(Path::new("/a/specific/dir/name"), None) - .is_ok()); - - // Inside of /a/specific but outside of /a/specific/dir/name - assert!(perms.read.check(Path::new("/a/specific/dir"), None).is_ok()); - assert!(perms - .write - .check(Path::new("/a/specific/dir"), None) - .is_ok()); - assert!(perms.ffi.check(Path::new("/a/specific/dir"), None).is_ok()); - - // Inside of /a/specific and /a/specific/dir/name - assert!(perms - .read - .check(Path::new("/a/specific/dir/name/inner"), None) - .is_ok()); - assert!(perms - .write - .check(Path::new("/a/specific/dir/name/inner"), None) - .is_ok()); - assert!(perms - .ffi - .check(Path::new("/a/specific/dir/name/inner"), None) - .is_ok()); - - // Inside of /a/specific but outside of /a/specific/dir/name - assert!(perms - .read - .check(Path::new("/a/specific/other/dir"), None) - .is_ok()); - assert!(perms - .write - .check(Path::new("/a/specific/other/dir"), None) - .is_ok()); - assert!(perms - .ffi - .check(Path::new("/a/specific/other/dir"), None) - .is_ok()); - - // Exact match with /b/c - assert!(perms.read.check(Path::new("/b/c"), None).is_ok()); - assert!(perms.write.check(Path::new("/b/c"), None).is_ok()); - assert!(perms.ffi.check(Path::new("/b/c"), None).is_ok()); - - // Sub path within /b/c - assert!(perms.read.check(Path::new("/b/c/sub/path"), None).is_ok()); - assert!(perms.write.check(Path::new("/b/c/sub/path"), None).is_ok()); - assert!(perms.ffi.check(Path::new("/b/c/sub/path"), None).is_ok()); - - // Sub path within /b/c, needs normalizing - assert!(perms - .read - .check(Path::new("/b/c/sub/path/../path/."), None) - .is_ok()); - assert!(perms - .write - .check(Path::new("/b/c/sub/path/../path/."), None) - .is_ok()); - assert!(perms - .ffi - .check(Path::new("/b/c/sub/path/../path/."), None) - .is_ok()); - - // Inside of /b but outside of /b/c - assert!(perms.read.check(Path::new("/b/e"), None).is_err()); - assert!(perms.write.check(Path::new("/b/e"), None).is_err()); - assert!(perms.ffi.check(Path::new("/b/e"), None).is_err()); - - // Inside of /a but outside of /a/specific - assert!(perms.read.check(Path::new("/a/b"), None).is_err()); - assert!(perms.write.check(Path::new("/a/b"), None).is_err()); - assert!(perms.ffi.check(Path::new("/a/b"), None).is_err()); + for (path, is_ok) in cases { + assert_eq!(perms.check_read(path, "api").is_ok(), is_ok); + assert_eq!(perms.check_write(path, "api").is_ok(), is_ok); + assert_eq!(perms.check_ffi(path).is_ok(), is_ok); + } } #[test] fn test_check_net_with_values() { set_prompter(Box::new(TestPrompter)); - let mut perms = Permissions::from_options(&PermissionsOptions { - allow_net: Some(svec![ - "localhost", - "deno.land", - "github.com:3000", - "127.0.0.1", - "172.16.0.2:8000", - "www.github.com:443", - "80.example.com:80", - "443.example.com:443" - ]), - ..Default::default() - }) + let parser = TestPermissionDescriptorParser; + let mut perms = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_net: Some(svec![ + "localhost", + "deno.land", + "github.com:3000", + "127.0.0.1", + "172.16.0.2:8000", + "www.github.com:443", + "80.example.com:80", + "443.example.com:443" + ]), + ..Default::default() + }, + ) .unwrap(); let domain_tests = vec![ @@ -2496,10 +3014,14 @@ mod tests { #[test] fn test_check_net_only_flag() { set_prompter(Box::new(TestPrompter)); - let mut perms = Permissions::from_options(&PermissionsOptions { - allow_net: Some(svec![]), // this means `--allow-net` is present without values following `=` sign - ..Default::default() - }) + let parser = TestPermissionDescriptorParser; + let mut perms = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_net: Some(svec![]), // this means `--allow-net` is present without values following `=` sign + ..Default::default() + }, + ) .unwrap(); let domain_tests = vec![ @@ -2537,10 +3059,14 @@ mod tests { #[test] fn test_check_net_no_flag() { set_prompter(Box::new(TestPrompter)); - let mut perms = Permissions::from_options(&PermissionsOptions { - allow_net: None, - ..Default::default() - }) + let parser = TestPermissionDescriptorParser; + let mut perms = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_net: None, + ..Default::default() + }, + ) .unwrap(); let domain_tests = vec![ @@ -2577,17 +3103,20 @@ mod tests { #[test] fn test_check_net_url() { - let mut perms = Permissions::from_options(&PermissionsOptions { - allow_net: Some(svec![ - "localhost", - "deno.land", - "github.com:3000", - "127.0.0.1", - "172.16.0.2:8000", - "www.github.com:443" - ]), - ..Default::default() - }) + let mut perms = Permissions::from_options( + &TestPermissionDescriptorParser, + &PermissionsOptions { + allow_net: Some(svec![ + "localhost", + "deno.land", + "github.com:3000", + "127.0.0.1", + "172.16.0.2:8000", + "www.github.com:443" + ]), + ..Default::default() + }, + ) .unwrap(); let url_tests = vec![ @@ -2639,15 +3168,18 @@ mod tests { fn check_specifiers() { set_prompter(Box::new(TestPrompter)); let read_allowlist = if cfg!(target_os = "windows") { - vec![PathBuf::from("C:\\a")] + svec!["C:\\a"] } else { - vec![PathBuf::from("/a")] + svec!["/a"] }; - let mut perms = Permissions::from_options(&PermissionsOptions { - allow_read: Some(read_allowlist), - allow_net: Some(svec!["localhost"]), - ..Default::default() - }) + let mut perms = Permissions::from_options( + &TestPermissionDescriptorParser, + &PermissionsOptions { + allow_read: Some(read_allowlist), + allow_net: Some(svec!["localhost"]), + ..Default::default() + }, + ) .unwrap(); let mut fixtures = vec![ @@ -2713,130 +3245,99 @@ mod tests { #[test] fn test_query() { set_prompter(Box::new(TestPrompter)); + let parser = TestPermissionDescriptorParser; let perms1 = Permissions::allow_all(); - let perms2 = Permissions { - read: Permissions::new_unary(Some(&[PathBuf::from("/foo")]), None, false) - .unwrap(), - write: Permissions::new_unary( - Some(&[PathBuf::from("/foo")]), - None, - false, - ) - .unwrap(), - ffi: Permissions::new_unary(Some(&[PathBuf::from("/foo")]), None, false) - .unwrap(), - net: Permissions::new_unary(Some(&sarr!["127.0.0.1:8000"]), None, false) - .unwrap(), - env: Permissions::new_unary(Some(&sarr!["HOME"]), None, false).unwrap(), - sys: Permissions::new_unary(Some(&sarr!["hostname"]), None, false) - .unwrap(), - run: Permissions::new_unary( - Some(&["deno".to_string().into()]), - None, - false, - ) - .unwrap(), - all: Permissions::new_all(false), - }; - let perms3 = Permissions { - read: Permissions::new_unary(None, Some(&[PathBuf::from("/foo")]), false) - .unwrap(), - write: Permissions::new_unary( - None, - Some(&[PathBuf::from("/foo")]), - false, - ) - .unwrap(), - ffi: Permissions::new_unary(None, Some(&[PathBuf::from("/foo")]), false) - .unwrap(), - net: Permissions::new_unary(None, Some(&sarr!["127.0.0.1:8000"]), false) - .unwrap(), - env: Permissions::new_unary(None, Some(&sarr!["HOME"]), false).unwrap(), - sys: Permissions::new_unary(None, Some(&sarr!["hostname"]), false) - .unwrap(), - run: Permissions::new_unary( - None, - Some(&["deno".to_string().into()]), - false, - ) - .unwrap(), - all: Permissions::new_all(false), - }; - let perms4 = Permissions { - read: Permissions::new_unary( - Some(&[]), - Some(&[PathBuf::from("/foo")]), - false, - ) - .unwrap(), - write: Permissions::new_unary( - Some(&[]), - Some(&[PathBuf::from("/foo")]), - false, - ) - .unwrap(), - ffi: Permissions::new_unary( - Some(&[]), - Some(&[PathBuf::from("/foo")]), - false, - ) - .unwrap(), - net: Permissions::new_unary( - Some(&[]), - Some(&sarr!["127.0.0.1:8000"]), - false, - ) - .unwrap(), - env: Permissions::new_unary(Some(&[]), Some(&sarr!["HOME"]), false) - .unwrap(), - sys: Permissions::new_unary(Some(&[]), Some(&sarr!["hostname"]), false) - .unwrap(), - run: Permissions::new_unary( - Some(&[]), - Some(&["deno".to_string().into()]), - false, - ) - .unwrap(), - all: Permissions::new_all(false), - }; + let perms2 = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_read: Some(svec!["/foo"]), + allow_write: Some(svec!["/foo"]), + allow_ffi: Some(svec!["/foo"]), + allow_net: Some(svec!["127.0.0.1:8000"]), + allow_env: Some(svec!["HOME"]), + allow_sys: Some(svec!["hostname"]), + allow_run: Some(svec!["/deno"]), + allow_all: false, + ..Default::default() + }, + ) + .unwrap(); + let perms3 = Permissions::from_options( + &parser, + &PermissionsOptions { + deny_read: Some(svec!["/foo"]), + deny_write: Some(svec!["/foo"]), + deny_ffi: Some(svec!["/foo"]), + deny_net: Some(svec!["127.0.0.1:8000"]), + deny_env: Some(svec!["HOME"]), + deny_sys: Some(svec!["hostname"]), + deny_run: Some(svec!["deno"]), + ..Default::default() + }, + ) + .unwrap(); + let perms4 = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_read: Some(vec![]), + deny_read: Some(svec!["/foo"]), + allow_write: Some(vec![]), + deny_write: Some(svec!["/foo"]), + allow_ffi: Some(vec![]), + deny_ffi: Some(svec!["/foo"]), + allow_net: Some(vec![]), + deny_net: Some(svec!["127.0.0.1:8000"]), + allow_env: Some(vec![]), + deny_env: Some(svec!["HOME"]), + allow_sys: Some(vec![]), + deny_sys: Some(svec!["hostname"]), + allow_run: Some(vec![]), + deny_run: Some(svec!["deno"]), + ..Default::default() + }, + ) + .unwrap(); #[rustfmt::skip] { + let read_query = |path: &str| parser.parse_path_query(path).unwrap().into_read(); + let write_query = |path: &str| parser.parse_path_query(path).unwrap().into_write(); + let ffi_query = |path: &str| parser.parse_path_query(path).unwrap().into_ffi(); assert_eq!(perms1.read.query(None), PermissionState::Granted); - assert_eq!(perms1.read.query(Some(Path::new("/foo"))), PermissionState::Granted); + assert_eq!(perms1.read.query(Some(&read_query("/foo"))), PermissionState::Granted); assert_eq!(perms2.read.query(None), PermissionState::Prompt); - assert_eq!(perms2.read.query(Some(Path::new("/foo"))), PermissionState::Granted); - assert_eq!(perms2.read.query(Some(Path::new("/foo/bar"))), PermissionState::Granted); + assert_eq!(perms2.read.query(Some(&read_query("/foo"))), PermissionState::Granted); + assert_eq!(perms2.read.query(Some(&read_query("/foo/bar"))), PermissionState::Granted); assert_eq!(perms3.read.query(None), PermissionState::Prompt); - assert_eq!(perms3.read.query(Some(Path::new("/foo"))), PermissionState::Denied); - assert_eq!(perms3.read.query(Some(Path::new("/foo/bar"))), PermissionState::Denied); + assert_eq!(perms3.read.query(Some(&read_query("/foo"))), PermissionState::Denied); + assert_eq!(perms3.read.query(Some(&read_query("/foo/bar"))), PermissionState::Denied); assert_eq!(perms4.read.query(None), PermissionState::GrantedPartial); - assert_eq!(perms4.read.query(Some(Path::new("/foo"))), PermissionState::Denied); - assert_eq!(perms4.read.query(Some(Path::new("/foo/bar"))), PermissionState::Denied); - assert_eq!(perms4.read.query(Some(Path::new("/bar"))), PermissionState::Granted); + assert_eq!(perms4.read.query(Some(&read_query("/foo"))), PermissionState::Denied); + assert_eq!(perms4.read.query(Some(&read_query("/foo/bar"))), PermissionState::Denied); + assert_eq!(perms4.read.query(Some(&read_query("/bar"))), PermissionState::Granted); assert_eq!(perms1.write.query(None), PermissionState::Granted); - assert_eq!(perms1.write.query(Some(Path::new("/foo"))), PermissionState::Granted); + assert_eq!(perms1.write.query(Some(&write_query("/foo"))), PermissionState::Granted); assert_eq!(perms2.write.query(None), PermissionState::Prompt); - assert_eq!(perms2.write.query(Some(Path::new("/foo"))), PermissionState::Granted); - assert_eq!(perms2.write.query(Some(Path::new("/foo/bar"))), PermissionState::Granted); + assert_eq!(perms2.write.query(Some(&write_query("/foo"))), PermissionState::Granted); + assert_eq!(perms2.write.query(Some(&write_query("/foo/bar"))), PermissionState::Granted); assert_eq!(perms3.write.query(None), PermissionState::Prompt); - assert_eq!(perms3.write.query(Some(Path::new("/foo"))), PermissionState::Denied); - assert_eq!(perms3.write.query(Some(Path::new("/foo/bar"))), PermissionState::Denied); + assert_eq!(perms3.write.query(Some(&write_query("/foo"))), PermissionState::Denied); + assert_eq!(perms3.write.query(Some(&write_query("/foo/bar"))), PermissionState::Denied); assert_eq!(perms4.write.query(None), PermissionState::GrantedPartial); - assert_eq!(perms4.write.query(Some(Path::new("/foo"))), PermissionState::Denied); - assert_eq!(perms4.write.query(Some(Path::new("/foo/bar"))), PermissionState::Denied); - assert_eq!(perms4.write.query(Some(Path::new("/bar"))), PermissionState::Granted); + assert_eq!(perms4.write.query(Some(&write_query("/foo"))), PermissionState::Denied); + assert_eq!(perms4.write.query(Some(&write_query("/foo/bar"))), PermissionState::Denied); + assert_eq!(perms4.write.query(Some(&write_query("/bar"))), PermissionState::Granted); assert_eq!(perms1.ffi.query(None), PermissionState::Granted); - assert_eq!(perms1.ffi.query(Some(Path::new("/foo"))), PermissionState::Granted); + assert_eq!(perms1.ffi.query(Some(&ffi_query("/foo"))), PermissionState::Granted); assert_eq!(perms2.ffi.query(None), PermissionState::Prompt); - assert_eq!(perms2.ffi.query(Some(Path::new("/foo"))), PermissionState::Granted); - assert_eq!(perms2.ffi.query(Some(Path::new("/foo/bar"))), PermissionState::Granted); + assert_eq!(perms2.ffi.query(Some(&ffi_query("/foo"))), PermissionState::Granted); + assert_eq!(perms2.ffi.query(Some(&ffi_query("/foo/bar"))), PermissionState::Granted); assert_eq!(perms3.ffi.query(None), PermissionState::Prompt); - assert_eq!(perms3.ffi.query(Some(Path::new("/foo"))), PermissionState::Denied); - assert_eq!(perms3.ffi.query(Some(Path::new("/foo/bar"))), PermissionState::Denied); + assert_eq!(perms3.ffi.query(Some(&ffi_query("/foo"))), PermissionState::Denied); + assert_eq!(perms3.ffi.query(Some(&ffi_query("/foo/bar"))), PermissionState::Denied); assert_eq!(perms4.ffi.query(None), PermissionState::GrantedPartial); - assert_eq!(perms4.ffi.query(Some(Path::new("/foo"))), PermissionState::Denied); - assert_eq!(perms4.ffi.query(Some(Path::new("/foo/bar"))), PermissionState::Denied); - assert_eq!(perms4.ffi.query(Some(Path::new("/bar"))), PermissionState::Granted); + assert_eq!(perms4.ffi.query(Some(&ffi_query("/foo"))), PermissionState::Denied); + assert_eq!(perms4.ffi.query(Some(&ffi_query("/foo/bar"))), PermissionState::Denied); + assert_eq!(perms4.ffi.query(Some(&ffi_query("/bar"))), PermissionState::Granted); assert_eq!(perms1.net.query(None), PermissionState::Granted); assert_eq!(perms1.net.query(Some(&NetDescriptor(Host::must_parse("127.0.0.1"), None))), PermissionState::Granted); assert_eq!(perms2.net.query(None), PermissionState::Prompt); @@ -2865,37 +3366,54 @@ mod tests { assert_eq!(perms4.sys.query(Some("hostname")), PermissionState::Denied); assert_eq!(perms4.sys.query(Some("uid")), PermissionState::Granted); assert_eq!(perms1.run.query(None), PermissionState::Granted); - assert_eq!(perms1.run.query(Some("deno")), PermissionState::Granted); + let deno_run_query = RunQueryDescriptor::Path { + requested: "deno".to_string(), + resolved: PathBuf::from("/deno"), + }; + let node_run_query = RunQueryDescriptor::Path { + requested: "node".to_string(), + resolved: PathBuf::from("/node"), + }; + assert_eq!(perms1.run.query(Some(&deno_run_query)), PermissionState::Granted); + assert_eq!(perms1.write.query(Some(&write_query("/deno"))), PermissionState::Granted); assert_eq!(perms2.run.query(None), PermissionState::Prompt); - assert_eq!(perms2.run.query(Some("deno")), PermissionState::Granted); + assert_eq!(perms2.run.query(Some(&deno_run_query)), PermissionState::Granted); + assert_eq!(perms2.write.query(Some(&write_query("/deno"))), PermissionState::Denied); assert_eq!(perms3.run.query(None), PermissionState::Prompt); - assert_eq!(perms3.run.query(Some("deno")), PermissionState::Denied); + assert_eq!(perms3.run.query(Some(&deno_run_query)), PermissionState::Denied); assert_eq!(perms4.run.query(None), PermissionState::GrantedPartial); - assert_eq!(perms4.run.query(Some("deno")), PermissionState::Denied); - assert_eq!(perms4.run.query(Some("node")), PermissionState::Granted); + assert_eq!(perms4.run.query(Some(&deno_run_query)), PermissionState::Denied); + assert_eq!(perms4.run.query(Some(&node_run_query)), PermissionState::Granted); }; } #[test] fn test_request() { set_prompter(Box::new(TestPrompter)); + let parser = TestPermissionDescriptorParser; let mut perms: Permissions = Permissions::none_without_prompt(); + let read_query = + |path: &str| parser.parse_path_query(path).unwrap().into_read(); + let write_query = + |path: &str| parser.parse_path_query(path).unwrap().into_write(); + let ffi_query = + |path: &str| parser.parse_path_query(path).unwrap().into_ffi(); #[rustfmt::skip] { let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock(); prompt_value.set(true); - assert_eq!(perms.read.request(Some(Path::new("/foo"))), PermissionState::Granted); + assert_eq!(perms.read.request(Some(&read_query("/foo"))), PermissionState::Granted); assert_eq!(perms.read.query(None), PermissionState::Prompt); prompt_value.set(false); - assert_eq!(perms.read.request(Some(Path::new("/foo/bar"))), PermissionState::Granted); + assert_eq!(perms.read.request(Some(&read_query("/foo/bar"))), PermissionState::Granted); prompt_value.set(false); - assert_eq!(perms.write.request(Some(Path::new("/foo"))), PermissionState::Denied); - assert_eq!(perms.write.query(Some(Path::new("/foo/bar"))), PermissionState::Prompt); + assert_eq!(perms.write.request(Some(&write_query("/foo"))), PermissionState::Denied); + assert_eq!(perms.write.query(Some(&write_query("/foo/bar"))), PermissionState::Prompt); prompt_value.set(true); assert_eq!(perms.write.request(None), PermissionState::Denied); prompt_value.set(false); - assert_eq!(perms.ffi.request(Some(Path::new("/foo"))), PermissionState::Denied); - assert_eq!(perms.ffi.query(Some(Path::new("/foo/bar"))), PermissionState::Prompt); + assert_eq!(perms.ffi.request(Some(&ffi_query("/foo"))), PermissionState::Denied); + assert_eq!(perms.ffi.query(Some(&ffi_query("/foo/bar"))), PermissionState::Prompt); prompt_value.set(true); assert_eq!(perms.ffi.request(None), PermissionState::Denied); prompt_value.set(true); @@ -2913,69 +3431,62 @@ mod tests { prompt_value.set(false); assert_eq!(perms.sys.request(Some("hostname")), PermissionState::Granted); prompt_value.set(true); - assert_eq!(perms.run.request(Some("deno")), PermissionState::Granted); + let run_query = RunQueryDescriptor::Path { + requested: "deno".to_string(), + resolved: PathBuf::from("/deno"), + }; + assert_eq!(perms.run.request(Some(&run_query)), PermissionState::Granted); assert_eq!(perms.run.query(None), PermissionState::Prompt); prompt_value.set(false); - assert_eq!(perms.run.request(Some("deno")), PermissionState::Granted); + assert_eq!(perms.run.request(Some(&run_query)), PermissionState::Granted); }; } #[test] fn test_revoke() { set_prompter(Box::new(TestPrompter)); - let mut perms = Permissions { - read: Permissions::new_unary( - Some(&[PathBuf::from("/foo"), PathBuf::from("/foo/baz")]), - None, - false, - ) - .unwrap(), - write: Permissions::new_unary( - Some(&[PathBuf::from("/foo"), PathBuf::from("/foo/baz")]), - None, - false, - ) - .unwrap(), - ffi: Permissions::new_unary( - Some(&[PathBuf::from("/foo"), PathBuf::from("/foo/baz")]), - None, - false, - ) - .unwrap(), - net: Permissions::new_unary( - Some(&sarr!["127.0.0.1", "127.0.0.1:8000"]), - None, - false, - ) - .unwrap(), - env: Permissions::new_unary(Some(&sarr!["HOME"]), None, false).unwrap(), - sys: Permissions::new_unary(Some(&sarr!["hostname"]), None, false) - .unwrap(), - run: Permissions::new_unary( - Some(&["deno".to_string().into()]), - None, - false, - ) - .unwrap(), - all: Permissions::new_all(false), - }; + let parser = TestPermissionDescriptorParser; + let mut perms = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_read: Some(svec!["/foo", "/foo/baz"]), + allow_write: Some(svec!["/foo", "/foo/baz"]), + allow_ffi: Some(svec!["/foo", "/foo/baz"]), + allow_net: Some(svec!["127.0.0.1", "127.0.0.1:8000"]), + allow_env: Some(svec!["HOME"]), + allow_sys: Some(svec!["hostname"]), + allow_run: Some(svec!["/deno"]), + ..Default::default() + }, + ) + .unwrap(); + let read_query = + |path: &str| parser.parse_path_query(path).unwrap().into_read(); + let write_query = + |path: &str| parser.parse_path_query(path).unwrap().into_write(); + let ffi_query = + |path: &str| parser.parse_path_query(path).unwrap().into_ffi(); #[rustfmt::skip] { - assert_eq!(perms.read.revoke(Some(Path::new("/foo/bar"))), PermissionState::Prompt); - assert_eq!(perms.read.query(Some(Path::new("/foo"))), PermissionState::Prompt); - assert_eq!(perms.read.query(Some(Path::new("/foo/baz"))), PermissionState::Granted); - assert_eq!(perms.write.revoke(Some(Path::new("/foo/bar"))), PermissionState::Prompt); - assert_eq!(perms.write.query(Some(Path::new("/foo"))), PermissionState::Prompt); - assert_eq!(perms.write.query(Some(Path::new("/foo/baz"))), PermissionState::Granted); - assert_eq!(perms.ffi.revoke(Some(Path::new("/foo/bar"))), PermissionState::Prompt); - assert_eq!(perms.ffi.query(Some(Path::new("/foo"))), PermissionState::Prompt); - assert_eq!(perms.ffi.query(Some(Path::new("/foo/baz"))), PermissionState::Granted); + assert_eq!(perms.read.revoke(Some(&read_query("/foo/bar"))), PermissionState::Prompt); + assert_eq!(perms.read.query(Some(&read_query("/foo"))), PermissionState::Prompt); + assert_eq!(perms.read.query(Some(&read_query("/foo/baz"))), PermissionState::Granted); + assert_eq!(perms.write.revoke(Some(&write_query("/foo/bar"))), PermissionState::Prompt); + assert_eq!(perms.write.query(Some(&write_query("/foo"))), PermissionState::Prompt); + assert_eq!(perms.write.query(Some(&write_query("/foo/baz"))), PermissionState::Granted); + assert_eq!(perms.ffi.revoke(Some(&ffi_query("/foo/bar"))), PermissionState::Prompt); + assert_eq!(perms.ffi.query(Some(&ffi_query("/foo"))), PermissionState::Prompt); + assert_eq!(perms.ffi.query(Some(&ffi_query("/foo/baz"))), PermissionState::Granted); assert_eq!(perms.net.revoke(Some(&NetDescriptor(Host::must_parse("127.0.0.1"), Some(9000)))), PermissionState::Prompt); assert_eq!(perms.net.query(Some(&NetDescriptor(Host::must_parse("127.0.0.1"), None))), PermissionState::Prompt); assert_eq!(perms.net.query(Some(&NetDescriptor(Host::must_parse("127.0.0.1"), Some(8000)))), PermissionState::Granted); assert_eq!(perms.env.revoke(Some("HOME")), PermissionState::Prompt); assert_eq!(perms.env.revoke(Some("hostname")), PermissionState::Prompt); - assert_eq!(perms.run.revoke(Some("deno")), PermissionState::Prompt); + let run_query = RunQueryDescriptor::Path { + requested: "deno".to_string(), + resolved: PathBuf::from("/deno"), + }; + assert_eq!(perms.run.revoke(Some(&run_query)), PermissionState::Prompt); }; } @@ -2984,24 +3495,31 @@ mod tests { set_prompter(Box::new(TestPrompter)); let mut perms = Permissions::none_with_prompt(); let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock(); + let parser = TestPermissionDescriptorParser; + let read_query = + |path: &str| parser.parse_path_query(path).unwrap().into_read(); + let write_query = + |path: &str| parser.parse_path_query(path).unwrap().into_write(); + let ffi_query = + |path: &str| parser.parse_path_query(path).unwrap().into_ffi(); prompt_value.set(true); - assert!(perms.read.check(Path::new("/foo"), None).is_ok()); + assert!(perms.read.check(&read_query("/foo"), None).is_ok()); prompt_value.set(false); - assert!(perms.read.check(Path::new("/foo"), None).is_ok()); - assert!(perms.read.check(Path::new("/bar"), None).is_err()); + assert!(perms.read.check(&read_query("/foo"), None).is_ok()); + assert!(perms.read.check(&read_query("/bar"), None).is_err()); prompt_value.set(true); - assert!(perms.write.check(Path::new("/foo"), None).is_ok()); + assert!(perms.write.check(&write_query("/foo"), None).is_ok()); prompt_value.set(false); - assert!(perms.write.check(Path::new("/foo"), None).is_ok()); - assert!(perms.write.check(Path::new("/bar"), None).is_err()); + assert!(perms.write.check(&write_query("/foo"), None).is_ok()); + assert!(perms.write.check(&write_query("/bar"), None).is_err()); prompt_value.set(true); - assert!(perms.ffi.check(Path::new("/foo"), None).is_ok()); + assert!(perms.ffi.check(&ffi_query("/foo"), None).is_ok()); prompt_value.set(false); - assert!(perms.ffi.check(Path::new("/foo"), None).is_ok()); - assert!(perms.ffi.check(Path::new("/bar"), None).is_err()); + assert!(perms.ffi.check(&ffi_query("/foo"), None).is_ok()); + assert!(perms.ffi.check(&ffi_query("/bar"), None).is_err()); prompt_value.set(true); assert!(perms @@ -3048,9 +3566,9 @@ mod tests { assert!(perms .run .check( - RunPathQuery { - requested: "cat", - resolved: &cwd.join("cat") + &RunQueryDescriptor::Path { + requested: "cat".to_string(), + resolved: cwd.join("cat") }, None ) @@ -3059,9 +3577,9 @@ mod tests { assert!(perms .run .check( - RunPathQuery { - requested: "cat", - resolved: &cwd.join("cat") + &RunQueryDescriptor::Path { + requested: "cat".to_string(), + resolved: cwd.join("cat") }, None ) @@ -3069,9 +3587,9 @@ mod tests { assert!(perms .run .check( - RunPathQuery { - requested: "ls", - resolved: &cwd.join("ls") + &RunQueryDescriptor::Path { + requested: "ls".to_string(), + resolved: cwd.join("ls") }, None ) @@ -3095,30 +3613,37 @@ mod tests { set_prompter(Box::new(TestPrompter)); let mut perms = Permissions::none_with_prompt(); let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock(); + let parser = TestPermissionDescriptorParser; + let read_query = + |path: &str| parser.parse_path_query(path).unwrap().into_read(); + let write_query = + |path: &str| parser.parse_path_query(path).unwrap().into_write(); + let ffi_query = + |path: &str| parser.parse_path_query(path).unwrap().into_ffi(); prompt_value.set(false); - assert!(perms.read.check(Path::new("/foo"), None).is_err()); + assert!(perms.read.check(&read_query("/foo"), None).is_err()); prompt_value.set(true); - assert!(perms.read.check(Path::new("/foo"), None).is_err()); - assert!(perms.read.check(Path::new("/bar"), None).is_ok()); + assert!(perms.read.check(&read_query("/foo"), None).is_err()); + assert!(perms.read.check(&read_query("/bar"), None).is_ok()); prompt_value.set(false); - assert!(perms.read.check(Path::new("/bar"), None).is_ok()); + assert!(perms.read.check(&read_query("/bar"), None).is_ok()); prompt_value.set(false); - assert!(perms.write.check(Path::new("/foo"), None).is_err()); + assert!(perms.write.check(&write_query("/foo"), None).is_err()); prompt_value.set(true); - assert!(perms.write.check(Path::new("/foo"), None).is_err()); - assert!(perms.write.check(Path::new("/bar"), None).is_ok()); + assert!(perms.write.check(&write_query("/foo"), None).is_err()); + assert!(perms.write.check(&write_query("/bar"), None).is_ok()); prompt_value.set(false); - assert!(perms.write.check(Path::new("/bar"), None).is_ok()); + assert!(perms.write.check(&write_query("/bar"), None).is_ok()); prompt_value.set(false); - assert!(perms.ffi.check(Path::new("/foo"), None).is_err()); + assert!(perms.ffi.check(&ffi_query("/foo"), None).is_err()); prompt_value.set(true); - assert!(perms.ffi.check(Path::new("/foo"), None).is_err()); - assert!(perms.ffi.check(Path::new("/bar"), None).is_ok()); + assert!(perms.ffi.check(&ffi_query("/foo"), None).is_err()); + assert!(perms.ffi.check(&ffi_query("/bar"), None).is_ok()); prompt_value.set(false); - assert!(perms.ffi.check(Path::new("/bar"), None).is_ok()); + assert!(perms.ffi.check(&ffi_query("/bar"), None).is_ok()); prompt_value.set(false); assert!(perms @@ -3172,9 +3697,9 @@ mod tests { assert!(perms .run .check( - RunPathQuery { - requested: "cat", - resolved: &cwd.join("cat") + &RunQueryDescriptor::Path { + requested: "cat".to_string(), + resolved: cwd.join("cat") }, None ) @@ -3183,9 +3708,9 @@ mod tests { assert!(perms .run .check( - RunPathQuery { - requested: "cat", - resolved: &cwd.join("cat") + &RunQueryDescriptor::Path { + requested: "cat".to_string(), + resolved: cwd.join("cat") }, None ) @@ -3193,9 +3718,9 @@ mod tests { assert!(perms .run .check( - RunPathQuery { - requested: "ls", - resolved: &cwd.join("ls") + &RunQueryDescriptor::Path { + requested: "ls".to_string(), + resolved: cwd.join("ls") }, None ) @@ -3204,9 +3729,9 @@ mod tests { assert!(perms .run .check( - RunPathQuery { - requested: "ls", - resolved: &cwd.join("ls") + &RunQueryDescriptor::Path { + requested: "ls".to_string(), + resolved: cwd.join("ls") }, None ) @@ -3237,7 +3762,12 @@ mod tests { let mut perms = Permissions::allow_all(); perms.env = UnaryPermission { granted_global: false, - ..Permissions::new_unary(Some(&sarr!["HOME"]), None, false).unwrap() + ..Permissions::new_unary( + Some(HashSet::from([EnvDescriptor::new("HOME")])), + None, + false, + ) + .unwrap() }; prompt_value.set(true); @@ -3251,63 +3781,52 @@ mod tests { #[test] fn test_check_partial_denied() { - let mut perms = Permissions { - read: Permissions::new_unary( - Some(&[]), - Some(&[PathBuf::from("/foo/bar")]), - false, - ) - .unwrap(), - write: Permissions::new_unary( - Some(&[]), - Some(&[PathBuf::from("/foo/bar")]), - false, - ) - .unwrap(), - ..Permissions::none_without_prompt() - }; + let parser = TestPermissionDescriptorParser; + let mut perms = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_read: Some(vec![]), + deny_read: Some(svec!["/foo/bar"]), + allow_write: Some(vec![]), + deny_write: Some(svec!["/foo/bar"]), + ..Default::default() + }, + ) + .unwrap(); - perms.read.check_partial(Path::new("/foo"), None).unwrap(); - assert!(perms.read.check(Path::new("/foo"), None).is_err()); + let read_query = parser.parse_path_query("/foo").unwrap().into_read(); + perms.read.check_partial(&read_query, None).unwrap(); + assert!(perms.read.check(&read_query, None).is_err()); - perms.write.check_partial(Path::new("/foo"), None).unwrap(); - assert!(perms.write.check(Path::new("/foo"), None).is_err()); + let write_query = parser.parse_path_query("/foo").unwrap().into_write(); + perms.write.check_partial(&write_query, None).unwrap(); + assert!(perms.write.check(&write_query, None).is_err()); } #[test] fn test_net_fully_qualified_domain_name() { - let mut perms = Permissions { - net: Permissions::new_unary( - Some(&["allowed.domain".to_string(), "1.1.1.1".to_string()]), - Some(&["denied.domain".to_string(), "2.2.2.2".to_string()]), - false, - ) - .unwrap(), - ..Permissions::none_without_prompt() - }; + set_prompter(Box::new(TestPrompter)); + let parser = TestPermissionDescriptorParser; + let perms = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_net: Some(svec!["allowed.domain", "1.1.1.1"]), + deny_net: Some(svec!["denied.domain", "2.2.2.2"]), + ..Default::default() + }, + ) + .unwrap(); + let mut perms = PermissionsContainer::new(Arc::new(parser), perms); + let cases = [ + ("allowed.domain.", true), + ("1.1.1.1", true), + ("denied.domain.", false), + ("2.2.2.2", false), + ]; - perms - .net - .check( - &NetDescriptor(Host::must_parse("allowed.domain."), None), - None, - ) - .unwrap(); - perms - .net - .check(&NetDescriptor(Host::must_parse("1.1.1.1"), None), None) - .unwrap(); - assert!(perms - .net - .check( - &NetDescriptor(Host::must_parse("denied.domain."), None), - None - ) - .is_err()); - assert!(perms - .net - .check(&NetDescriptor(Host::must_parse("2.2.2.2"), None), None) - .is_err()); + for (host, is_ok) in cases { + assert_eq!(perms.check_net(&(host, None), "api").is_ok(), is_ok); + } } #[test] @@ -3443,14 +3962,19 @@ mod tests { #[test] fn test_create_child_permissions() { set_prompter(Box::new(TestPrompter)); - let mut main_perms = Permissions { - env: Permissions::new_unary(Some(&[]), None, false).unwrap(), - net: Permissions::new_unary(Some(&sarr!["foo", "bar"]), None, false) - .unwrap(), - ..Permissions::none_without_prompt() - }; + let parser = TestPermissionDescriptorParser; + let mut main_perms = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_env: Some(vec![]), + allow_net: Some(svec!["foo", "bar"]), + ..Default::default() + }, + ) + .unwrap(); assert_eq!( create_child_permissions( + &parser, &mut main_perms.clone(), ChildPermissionsArg { env: ChildUnaryPermissionArg::Inherit, @@ -3461,12 +3985,18 @@ mod tests { ) .unwrap(), Permissions { - env: Permissions::new_unary(Some(&[]), None, false).unwrap(), - net: Permissions::new_unary(Some(&sarr!["foo"]), None, false).unwrap(), + env: Permissions::new_unary(Some(HashSet::new()), None, false).unwrap(), + net: Permissions::new_unary( + Some(HashSet::from([NetDescriptor::parse("foo").unwrap()])), + None, + false + ) + .unwrap(), ..Permissions::none_without_prompt() } ); assert!(create_child_permissions( + &parser, &mut main_perms.clone(), ChildPermissionsArg { net: ChildUnaryPermissionArg::Granted, @@ -3475,6 +4005,7 @@ mod tests { ) .is_err()); assert!(create_child_permissions( + &parser, &mut main_perms.clone(), ChildPermissionsArg { net: ChildUnaryPermissionArg::GrantedList(svec!["foo", "bar", "baz"]), @@ -3483,6 +4014,7 @@ mod tests { ) .is_err()); assert!(create_child_permissions( + &parser, &mut main_perms, ChildPermissionsArg { ffi: ChildUnaryPermissionArg::GrantedList(svec!["foo"]), @@ -3496,13 +4028,17 @@ mod tests { fn test_create_child_permissions_with_prompt() { set_prompter(Box::new(TestPrompter)); let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock(); - let mut main_perms = Permissions::from_options(&PermissionsOptions { - prompt: true, - ..Default::default() - }) + let mut main_perms = Permissions::from_options( + &TestPermissionDescriptorParser, + &PermissionsOptions { + prompt: true, + ..Default::default() + }, + ) .unwrap(); prompt_value.set(true); let worker_perms = create_child_permissions( + &TestPermissionDescriptorParser, &mut main_perms, ChildPermissionsArg { read: ChildUnaryPermissionArg::Granted, @@ -3515,8 +4051,8 @@ mod tests { assert_eq!( main_perms.run.granted_list, HashSet::from([ - RunDescriptor::Name("bar".to_owned()), - RunDescriptor::Name("foo".to_owned()) + AllowRunDescriptor(PathBuf::from("/bar")), + AllowRunDescriptor(PathBuf::from("/foo")), ]) ); } @@ -3525,14 +4061,22 @@ mod tests { fn test_create_child_permissions_with_inherited_denied_list() { set_prompter(Box::new(TestPrompter)); let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock(); - let mut main_perms = Permissions::from_options(&PermissionsOptions { - prompt: true, - ..Default::default() - }) + let parser = TestPermissionDescriptorParser; + let mut main_perms = Permissions::from_options( + &parser, + &PermissionsOptions { + prompt: true, + ..Default::default() + }, + ) .unwrap(); prompt_value.set(false); - assert!(main_perms.write.check(&PathBuf::from("foo"), None).is_err()); + assert!(main_perms + .write + .check(&parser.parse_path_query("foo").unwrap().into_write(), None) + .is_err()); let worker_perms = create_child_permissions( + &TestPermissionDescriptorParser, &mut main_perms.clone(), ChildPermissionsArg::none(), ) @@ -3543,30 +4087,6 @@ mod tests { ); } - #[test] - fn test_handle_empty_value() { - set_prompter(Box::new(TestPrompter)); - - assert!(Permissions::new_unary::( - Some(&[Default::default()]), - None, - false - ) - .is_err()); - assert!(Permissions::new_unary::( - Some(&[Default::default()]), - None, - false - ) - .is_err()); - assert!(Permissions::new_unary::( - Some(&[Default::default()]), - None, - false - ) - .is_err()); - } - #[test] fn test_host_parse() { let hosts = &[ @@ -3671,4 +4191,35 @@ mod tests { assert_eq!(NetDescriptor::parse(input).ok(), *expected, "'{input}'"); } } + + #[test] + fn test_denies_run_name() { + let cases = [ + #[cfg(windows)] + ("deno", "C:\\deno.exe", true), + #[cfg(windows)] + ("deno", "C:\\sub\\deno.cmd", true), + #[cfg(windows)] + ("deno", "C:\\sub\\DeNO.cmd", true), + #[cfg(windows)] + ("DEno", "C:\\sub\\deno.cmd", true), + #[cfg(windows)] + ("deno", "C:\\other\\sub\\deno.batch", true), + #[cfg(windows)] + ("deno", "C:\\other\\sub\\deno", true), + #[cfg(windows)] + ("denort", "C:\\other\\sub\\deno.exe", false), + ("deno", "/home/test/deno", true), + ("deno", "/home/test/denot", false), + ]; + for (name, cmd_path, denies) in cases { + assert_eq!( + denies_run_name(name, &PathBuf::from(cmd_path)), + denies, + "{} {}", + name, + cmd_path + ); + } + } } diff --git a/runtime/snapshot.rs b/runtime/snapshot.rs index fd422603fe..db6688b464 100644 --- a/runtime/snapshot.rs +++ b/runtime/snapshot.rs @@ -2,6 +2,7 @@ use crate::ops; use crate::ops::bootstrap::SnapshotOptions; +use crate::permissions::RuntimePermissionDescriptorParser; use crate::shared::maybe_transpile_source; use crate::shared::runtime; use deno_cache::SqliteBackedCache; @@ -11,6 +12,7 @@ use deno_core::v8; use deno_core::Extension; use deno_http::DefaultHttpPropertyExtractor; use deno_io::fs::FsError; +use std::borrow::Cow; use std::io::Write; use std::path::Path; use std::path::PathBuf; @@ -45,29 +47,32 @@ impl deno_fetch::FetchPermissions for Permissions { unreachable!("snapshotting!") } - fn check_read( + fn check_read<'a>( &mut self, - _p: &Path, + _p: &'a Path, _api_name: &str, - ) -> Result<(), deno_core::error::AnyError> { + ) -> Result, AnyError> { unreachable!("snapshotting!") } } impl deno_ffi::FfiPermissions for Permissions { - fn check_partial( + fn check_partial_no_path( &mut self, - _path: Option<&Path>, ) -> Result<(), deno_core::error::AnyError> { unreachable!("snapshotting!") } + + fn check_partial_with_path( + &mut self, + _path: &str, + ) -> Result { + unreachable!("snapshotting!") + } } impl deno_napi::NapiPermissions for Permissions { - fn check( - &mut self, - _path: Option<&Path>, - ) -> Result<(), deno_core::error::AnyError> { + fn check(&mut self, _path: &str) -> std::result::Result { unreachable!("snapshotting!") } } @@ -80,18 +85,24 @@ impl deno_node::NodePermissions for Permissions { ) -> Result<(), deno_core::error::AnyError> { unreachable!("snapshotting!") } + fn check_read_path<'a>( + &mut self, + _path: &'a Path, + ) -> Result, AnyError> { + unreachable!("snapshotting!") + } fn check_read_with_api_name( &mut self, - _p: &Path, + _p: &str, _api_name: Option<&str>, - ) -> Result<(), deno_core::error::AnyError> { + ) -> Result { unreachable!("snapshotting!") } fn check_write_with_api_name( &mut self, - _p: &Path, + _p: &str, _api_name: Option<&str>, - ) -> Result<(), deno_core::error::AnyError> { + ) -> Result { unreachable!("snapshotting!") } fn check_sys( @@ -114,17 +125,25 @@ impl deno_net::NetPermissions for Permissions { fn check_read( &mut self, - _p: &Path, + _p: &str, _api_name: &str, - ) -> Result<(), deno_core::error::AnyError> { + ) -> Result { unreachable!("snapshotting!") } fn check_write( &mut self, - _p: &Path, + _p: &str, _api_name: &str, - ) -> Result<(), deno_core::error::AnyError> { + ) -> Result { + unreachable!("snapshotting!") + } + + fn check_write_path<'a>( + &mut self, + _p: &'a Path, + _api_name: &str, + ) -> Result, AnyError> { unreachable!("snapshotting!") } } @@ -143,9 +162,9 @@ impl deno_fs::FsPermissions for Permissions { fn check_read( &mut self, - _path: &Path, + _path: &str, _api_name: &str, - ) -> Result<(), AnyError> { + ) -> Result { unreachable!("snapshotting!") } @@ -164,17 +183,17 @@ impl deno_fs::FsPermissions for Permissions { fn check_write( &mut self, - _path: &Path, + _path: &str, _api_name: &str, - ) -> Result<(), AnyError> { + ) -> Result { unreachable!("snapshotting!") } fn check_write_partial( &mut self, - _path: &Path, + _path: &str, _api_name: &str, - ) -> Result<(), AnyError> { + ) -> Result { unreachable!("snapshotting!") } @@ -190,22 +209,38 @@ impl deno_fs::FsPermissions for Permissions { ) -> Result<(), AnyError> { unreachable!("snapshotting!") } + + fn check_read_path<'a>( + &mut self, + _path: &'a Path, + _api_name: &str, + ) -> Result, AnyError> { + unreachable!("snapshotting!") + } + + fn check_write_path<'a>( + &mut self, + _path: &'a Path, + _api_name: &str, + ) -> Result, AnyError> { + unreachable!("snapshotting!") + } } impl deno_kv::sqlite::SqliteDbHandlerPermissions for Permissions { fn check_read( &mut self, - _path: &Path, + _path: &str, _api_name: &str, - ) -> Result<(), AnyError> { + ) -> Result { unreachable!("snapshotting!") } - fn check_write( + fn check_write<'a>( &mut self, - _path: &Path, + _path: &'a Path, _api_name: &str, - ) -> Result<(), AnyError> { + ) -> Result, AnyError> { unreachable!("snapshotting!") } } @@ -255,7 +290,7 @@ pub fn create_runtime_snapshot( deno_http::deno_http::init_ops_and_esm::(), deno_io::deno_io::init_ops_and_esm(Default::default()), deno_fs::deno_fs::init_ops_and_esm::(fs.clone()), - deno_node::deno_node::init_ops_and_esm::(None, fs), + deno_node::deno_node::init_ops_and_esm::(None, fs.clone()), runtime::init_ops_and_esm(), ops::runtime::deno_runtime::init_ops("deno:runtime".parse().unwrap()), ops::worker_host::deno_worker_host::init_ops( @@ -264,7 +299,9 @@ pub fn create_runtime_snapshot( ), ops::fs_events::deno_fs_events::init_ops(), ops::os::deno_os::init_ops(Default::default()), - ops::permissions::deno_permissions::init_ops(), + ops::permissions::deno_permissions::init_ops(Arc::new( + RuntimePermissionDescriptorParser::new(fs), + )), ops::process::deno_process::init_ops(), ops::signal::deno_signal::init_ops(), ops::tty::deno_tty::init_ops(), diff --git a/runtime/web_worker.rs b/runtime/web_worker.rs index e143288618..7b69a97982 100644 --- a/runtime/web_worker.rs +++ b/runtime/web_worker.rs @@ -44,6 +44,7 @@ use deno_http::DefaultHttpPropertyExtractor; use deno_io::Stdio; use deno_kv::dynamic::MultiBackendDbHandler; use deno_node::NodeExtInitServices; +use deno_permissions::PermissionDescriptorParser; use deno_permissions::PermissionsContainer; use deno_terminal::colors; use deno_tls::RootCertStoreProvider; @@ -356,6 +357,7 @@ pub struct WebWorker { } pub struct WebWorkerOptions { + // todo(dsherret): extract out the service structs from this options bag pub bootstrap: BootstrapOptions, pub extensions: Vec, pub startup_snapshot: Option<&'static [u8]>, @@ -377,6 +379,7 @@ pub struct WebWorkerOptions { pub cache_storage_dir: Option, pub stdio: Stdio, pub feature_checker: Arc, + pub permission_desc_parser: Arc, pub strace_ops: Option>, pub close_on_idle: bool, pub maybe_worker_metadata: Option, @@ -501,7 +504,9 @@ impl WebWorker { ), ops::fs_events::deno_fs_events::init_ops_and_esm(), ops::os::deno_os_worker::init_ops_and_esm(), - ops::permissions::deno_permissions::init_ops_and_esm(), + ops::permissions::deno_permissions::init_ops_and_esm( + options.permission_desc_parser.clone(), + ), ops::process::deno_process::init_ops_and_esm(), ops::signal::deno_signal::init_ops_and_esm(), ops::tty::deno_tty::init_ops_and_esm(), diff --git a/runtime/worker.rs b/runtime/worker.rs index 02749e7c18..3d8c8a0b9b 100644 --- a/runtime/worker.rs +++ b/runtime/worker.rs @@ -49,6 +49,7 @@ use crate::code_cache::CodeCache; use crate::code_cache::CodeCacheType; use crate::inspector_server::InspectorServer; use crate::ops; +use crate::permissions::RuntimePermissionDescriptorParser; use crate::shared::maybe_transpile_source; use crate::shared::runtime; use crate::BootstrapOptions; @@ -157,6 +158,8 @@ pub struct WorkerOptions { /// executed tries to load modules. pub module_loader: Rc, pub node_services: Option, + pub permission_desc_parser: + Arc, // Callbacks invoked when creating new instance of WebWorker pub create_web_worker_cb: Arc, pub format_js_error_fn: Option>, @@ -201,13 +204,16 @@ pub struct WorkerOptions { pub v8_code_cache: Option>, } +// todo(dsherret): this is error prone to use. We should separate +// out the WorkerOptions from the services. impl Default for WorkerOptions { fn default() -> Self { + let real_fs = Arc::new(deno_fs::RealFs); Self { create_web_worker_cb: Arc::new(|_| { unimplemented!("web workers are not supported") }), - fs: Arc::new(deno_fs::RealFs), + fs: real_fs.clone(), module_loader: Rc::new(FsModuleLoader), skip_op_registration: false, seed: None, @@ -232,6 +238,9 @@ impl Default for WorkerOptions { bootstrap: Default::default(), stdio: Default::default(), feature_checker: Default::default(), + permission_desc_parser: Arc::new(RuntimePermissionDescriptorParser::new( + real_fs, + )), v8_code_cache: Default::default(), } } @@ -425,7 +434,9 @@ impl MainWorker { ), ops::fs_events::deno_fs_events::init_ops_and_esm(), ops::os::deno_os::init_ops_and_esm(exit_code.clone()), - ops::permissions::deno_permissions::init_ops_and_esm(), + ops::permissions::deno_permissions::init_ops_and_esm( + options.permission_desc_parser, + ), ops::process::deno_process::init_ops_and_esm(), ops::signal::deno_signal::init_ops_and_esm(), ops::tty::deno_tty::init_ops_and_esm(), diff --git a/tests/integration/run_tests.rs b/tests/integration/run_tests.rs index 95b6e5a018..10a266fccd 100644 --- a/tests/integration/run_tests.rs +++ b/tests/integration/run_tests.rs @@ -255,12 +255,6 @@ itest!(_052_no_remote_flag { http_server: true, }); -itest!(_056_make_temp_file_write_perm { - args: - "run --quiet --allow-read --allow-write=./subdir/ run/056_make_temp_file_write_perm.ts", - output: "run/056_make_temp_file_write_perm.out", -}); - itest!(_058_tasks_microtasks_close { args: "run --quiet run/058_tasks_microtasks_close.ts", output: "run/058_tasks_microtasks_close.ts.out", diff --git a/tests/specs/permission/deny_run_binary_absolute_path/__test__.jsonc b/tests/specs/permission/deny_run_binary_absolute_path/__test__.jsonc new file mode 100644 index 0000000000..fac0d928ac --- /dev/null +++ b/tests/specs/permission/deny_run_binary_absolute_path/__test__.jsonc @@ -0,0 +1,8 @@ +{ + "envs": { + "DYLD_FALLBACK_LIBRARY_PATH": "", + "LD_LIBRARY_PATH": "" + }, + "args": "run --allow-run --deny-run=deno --allow-read main.ts", + "output": "main.out" +} diff --git a/tests/specs/permission/deny_run_binary_absolute_path/main.out b/tests/specs/permission/deny_run_binary_absolute_path/main.out new file mode 100644 index 0000000000..45b2283879 --- /dev/null +++ b/tests/specs/permission/deny_run_binary_absolute_path/main.out @@ -0,0 +1,8 @@ +NotCapable: Requires run access to "deno", run again with the --allow-run flag + at [WILDCARD] { + name: "NotCapable" +} +NotCapable: Requires run access to "[WILDLINE]", run again with the --allow-run flag + at [WILDCARD] { + name: "NotCapable" +} diff --git a/tests/specs/permission/deny_run_binary_absolute_path/main.ts b/tests/specs/permission/deny_run_binary_absolute_path/main.ts new file mode 100644 index 0000000000..eca5e5a33e --- /dev/null +++ b/tests/specs/permission/deny_run_binary_absolute_path/main.ts @@ -0,0 +1,15 @@ +try { + new Deno.Command("deno", { + args: ["--version"], + }).outputSync(); +} catch (err) { + console.error(err); +} + +try { + new Deno.Command(Deno.execPath(), { + args: ["--version"], + }).outputSync(); +} catch (err) { + console.error(err); +} diff --git a/tests/specs/permission/make_temp_write_perm/056_make_temp_file_write_perm.out b/tests/specs/permission/make_temp_write_perm/056_make_temp_file_write_perm.out new file mode 100644 index 0000000000..7144e088ce --- /dev/null +++ b/tests/specs/permission/make_temp_write_perm/056_make_temp_file_write_perm.out @@ -0,0 +1,4 @@ +good [WILDCARD]subdir[WILDCARD] +good [WILDCARD]subdir[WILDCARD] +good [WILDCARD]subdir[WILDCARD] +good [WILDCARD]subdir[WILDCARD] diff --git a/tests/specs/permission/make_temp_write_perm/056_make_temp_file_write_perm.ts b/tests/specs/permission/make_temp_write_perm/056_make_temp_file_write_perm.ts new file mode 100644 index 0000000000..28661973cf --- /dev/null +++ b/tests/specs/permission/make_temp_write_perm/056_make_temp_file_write_perm.ts @@ -0,0 +1,52 @@ +Deno.mkdirSync("subdir"); + +// async file +{ + const path = await Deno.makeTempFile({ dir: `subdir` }); + try { + if (!path.match(/^subdir[/\\][^/\\]+/)) { + throw Error("bad " + path); + } + console.log("good", path); + } finally { + await Deno.remove(path); + } +} +// sync file +{ + const path = Deno.makeTempFileSync({ dir: `subdir` }); + try { + if (!path.match(/^subdir[/\\][^/\\]+/)) { + throw Error("bad " + path); + } + console.log("good", path); + } finally { + await Deno.remove(path); + } +} + +// async dir +{ + const path = await Deno.makeTempDir({ dir: `subdir` }); + try { + if (!path.match(/^subdir[/\\][^/\\]+/)) { + throw Error("bad " + path); + } + console.log("good", path); + } finally { + await Deno.remove(path); + } +} + +// sync dir +{ + const path = Deno.makeTempDirSync({ dir: `subdir` }); + try { + if (!path.match(/^subdir[/\\][^/\\]+/)) { + throw Error("bad " + path); + } + console.log("good", path); + } finally { + await Deno.remove(path); + } +} diff --git a/tests/specs/permission/make_temp_write_perm/__test__.jsonc b/tests/specs/permission/make_temp_write_perm/__test__.jsonc new file mode 100644 index 0000000000..80a503215e --- /dev/null +++ b/tests/specs/permission/make_temp_write_perm/__test__.jsonc @@ -0,0 +1,15 @@ +{ + "tempDir": true, + "tests": { + "reduced_perms": { + // this should not expose the full directory + "args": "run --quiet --allow-read --allow-write=./subdir/ 056_make_temp_file_write_perm.ts", + "output": "056_make_temp_file_write_perm.out" + }, + "all_perms": { + // this will work the same as above + "args": "run --quiet -A 056_make_temp_file_write_perm.ts", + "output": "056_make_temp_file_write_perm.out" + } + } +} diff --git a/tests/specs/run/allow_run_allowlist_resolution/__test__.jsonc b/tests/specs/run/allow_run_allowlist_resolution/__test__.jsonc index 173e13027f..3e5d86adf4 100644 --- a/tests/specs/run/allow_run_allowlist_resolution/__test__.jsonc +++ b/tests/specs/run/allow_run_allowlist_resolution/__test__.jsonc @@ -1,8 +1,9 @@ { - "args": "run --quiet -A main.ts", - "output": "main.out", + "tempDir": true, "envs": { "DYLD_FALLBACK_LIBRARY_PATH": "", "LD_LIBRARY_PATH": "" - } + }, + "args": "run --quiet -A main.ts", + "output": "main.out" } diff --git a/tests/specs/run/allow_run_allowlist_resolution/main.out b/tests/specs/run/allow_run_allowlist_resolution/main.out index f61f9b5503..b494bb52f2 100644 --- a/tests/specs/run/allow_run_allowlist_resolution/main.out +++ b/tests/specs/run/allow_run_allowlist_resolution/main.out @@ -3,7 +3,7 @@ PermissionStatus { state: "granted", onchange: null } PermissionStatus { state: "prompt", onchange: null } PermissionStatus { state: "granted", onchange: null } --- -Info Failed to resolve 'deno' for allow-run: cannot find binary path +Info Failed to resolve 'binary' for allow-run: cannot find binary path PermissionStatus { state: "prompt", onchange: null } PermissionStatus { state: "prompt", onchange: null } PermissionStatus { state: "prompt", onchange: null } diff --git a/tests/specs/run/allow_run_allowlist_resolution/main.ts b/tests/specs/run/allow_run_allowlist_resolution/main.ts index bf33d8cbe1..e43e0b5da0 100644 --- a/tests/specs/run/allow_run_allowlist_resolution/main.ts +++ b/tests/specs/run/allow_run_allowlist_resolution/main.ts @@ -1,36 +1,41 @@ -// Testing the following (but with `deno` instead of `echo`): -// | `deno run --allow-run=echo` | `which path == "/usr/bin/echo"` at startup | `which path != "/usr/bin/echo"` at startup | -// |-------------------------------------|--------------------------------------------|--------------------------------------------| -// | **`Deno.Command("echo")`** | ✅ | ✅ | -// | **`Deno.Command("/usr/bin/echo")`** | ✅ | ❌ | +// Testing the following: +// | `deno run --allow-run=binary` | `which path == "/usr/bin/binary"` at startup | `which path != "/usr/bin/binary"` at startup | +// |---------------------------------------|----------------------------------------------|--------------------------------------------| +// | **`Deno.Command("binary")`** | :white_check_mark: | :white_check_mark: | +// | **`Deno.Command("/usr/bin/binary")`** | :white_check_mark: | :x: | +// | `deno run --allow-run=/usr/bin/binary | `which path == "/usr/bin/binary"` at runtime | `which path != "/usr/bin/binary"` at runtime | +// |---------------------------------------|----------------------------------------------|--------------------------------------------| +// | **`Deno.Command("binary")`** | :white_check_mark: | :x: | +// | **`Deno.Command("/usr/bin/binary")`** | :white_check_mark: | :white_check_mark: | -// | `deno run --allow-run=/usr/bin/echo | `which path == "/usr/bin/echo"` at runtime | `which path != "/usr/bin/echo"` at runtime | -// |-------------------------------------|--------------------------------------------|--------------------------------------------| -// | **`Deno.Command("echo")`** | ✅ | ❌ | -// | **`Deno.Command("/usr/bin/echo")`** | ✅ | ✅ | +const binaryName = Deno.build.os === "windows" ? "binary.exe" : "binary"; +const pathSep = Deno.build.os === "windows" ? "\\" : "/"; +const cwd = Deno.cwd(); +const execPathParent = `${Deno.cwd()}${pathSep}sub`; +const execPath = `${execPathParent}${pathSep}${binaryName}`; -const execPath = Deno.execPath(); -const execPathParent = execPath.replace(/[/\\][^/\\]+$/, ""); +Deno.mkdirSync(execPathParent); +Deno.copyFileSync(Deno.execPath(), execPath); const testUrl = `data:application/typescript;base64,${ btoa(` - console.error(await Deno.permissions.query({ name: "run", command: "deno" })); + console.error(await Deno.permissions.query({ name: "run", command: "binary" })); console.error(await Deno.permissions.query({ name: "run", command: "${ execPath.replaceAll("\\", "\\\\") }" })); Deno.env.set("PATH", ""); - console.error(await Deno.permissions.query({ name: "run", command: "deno" })); + console.error(await Deno.permissions.query({ name: "run", command: "binary" })); console.error(await Deno.permissions.query({ name: "run", command: "${ execPath.replaceAll("\\", "\\\\") }" })); `) }`; -const process1 = await new Deno.Command(Deno.execPath(), { +await new Deno.Command(Deno.execPath(), { args: [ "run", "--allow-env", - "--allow-run=deno", + "--allow-run=binary", testUrl, ], stdout: "inherit", @@ -44,7 +49,7 @@ await new Deno.Command(Deno.execPath(), { args: [ "run", "--allow-env", - "--allow-run=deno", + "--allow-run=binary", testUrl, ], stderr: "inherit", diff --git a/tests/specs/run/ld_preload/__test__.jsonc b/tests/specs/run/ld_preload/__test__.jsonc index 882f157e9e..16ae697a74 100644 --- a/tests/specs/run/ld_preload/__test__.jsonc +++ b/tests/specs/run/ld_preload/__test__.jsonc @@ -6,11 +6,11 @@ }, "tests": { "env_arg": { - "args": "run --allow-run=echo env_arg.ts", + "args": "run --allow-run=curl env_arg.ts", "output": "env_arg.out" }, "set_with_allow_env": { - "args": "run --allow-run=echo --allow-env set_with_allow_env.ts", + "args": "run --allow-run=curl --allow-env set_with_allow_env.ts", "output": "set_with_allow_env.out" } } diff --git a/tests/specs/run/ld_preload/set_with_allow_env.ts b/tests/specs/run/ld_preload/set_with_allow_env.ts index 79004aa165..a3e8dd3976 100644 --- a/tests/specs/run/ld_preload/set_with_allow_env.ts +++ b/tests/specs/run/ld_preload/set_with_allow_env.ts @@ -1,7 +1,7 @@ Deno.env.set("LD_PRELOAD", "./libpreload.so"); try { - new Deno.Command("echo").spawn(); + new Deno.Command("curl").spawn(); } catch (err) { console.log(err); } @@ -9,7 +9,7 @@ try { Deno.env.set("DYLD_FALLBACK_LIBRARY_PATH", "./libpreload.so"); try { - Deno.run({ cmd: ["echo"] }).spawnSync(); + Deno.run({ cmd: ["curl"] }).spawnSync(); } catch (err) { console.log(err); } diff --git a/tests/testdata/run/056_make_temp_file_write_perm.out b/tests/testdata/run/056_make_temp_file_write_perm.out deleted file mode 100644 index c56aae43f6..0000000000 --- a/tests/testdata/run/056_make_temp_file_write_perm.out +++ /dev/null @@ -1 +0,0 @@ -good [WILDCARD]subdir[WILDCARD] diff --git a/tests/testdata/run/056_make_temp_file_write_perm.ts b/tests/testdata/run/056_make_temp_file_write_perm.ts deleted file mode 100644 index c0deda8a25..0000000000 --- a/tests/testdata/run/056_make_temp_file_write_perm.ts +++ /dev/null @@ -1,9 +0,0 @@ -const path = await Deno.makeTempFile({ dir: `subdir` }); -try { - if (!path.match(/^subdir[/\\][^/\\]+/)) { - throw Error("bad " + path); - } - console.log("good", path); -} finally { - await Deno.remove(path); -} diff --git a/tests/testdata/workers/read_check_granular_worker.js b/tests/testdata/workers/read_check_granular_worker.js index 7f2d0f7178..01df8ca97d 100644 --- a/tests/testdata/workers/read_check_granular_worker.js +++ b/tests/testdata/workers/read_check_granular_worker.js @@ -1,4 +1,16 @@ // deno-fmt-ignore-file +import { toFileUrl } from "@std/path/to-file-url"; + +function tryGetCwd() { + // will throw in one test but not the other + try { + return Deno.cwd() + } catch { + return import.meta.dirname; + } +} + +const fooExePath = tryGetCwd() + "/foo" + (Deno.build.os === "windows" ? ".exe" : ""); postMessage({ envGlobal: (await Deno.permissions.query({ name: "env" })).state, envFoo: (await Deno.permissions.query({ name: "env", variable: "foo" })).state, @@ -15,11 +27,13 @@ postMessage({ readGlobal: (await Deno.permissions.query({ name: "read" })).state, readFoo: (await Deno.permissions.query({ name: "read", path: new URL("foo", import.meta.url) })).state, readBar: (await Deno.permissions.query({ name: "read", path: "bar" })).state, - readAbsent: (await Deno.permissions.query({ name: "read", path: "absent" })).state, + readAbsent: (await Deno.permissions.query({ name: "read", path: "../absent" })).state, runGlobal: (await Deno.permissions.query({ name: "run" })).state, - runFoo: (await Deno.permissions.query({ name: "run", command: new URL("foo", import.meta.url) })).state, + runFoo: (await Deno.permissions.query({ name: "run", command: toFileUrl(fooExePath) })).state, + runFooPath: (await Deno.permissions.query({ name: "run", command: fooExePath })).state, runBar: (await Deno.permissions.query({ name: "run", command: "bar" })).state, runBaz: (await Deno.permissions.query({ name: "run", command: "./baz" })).state, + runUnresolved: (await Deno.permissions.query({ name: "run", command: "unresolved-exec" })).state, runAbsent: (await Deno.permissions.query({ name: "run", command: "absent" })).state, writeGlobal: (await Deno.permissions.query({ name: "write" })).state, writeFoo: (await Deno.permissions.query({ name: "write", path: new URL("foo", import.meta.url) })).state, diff --git a/tests/unit/os_test.ts b/tests/unit/os_test.ts index 4f760ecf8d..a70796505f 100644 --- a/tests/unit/os_test.ts +++ b/tests/unit/os_test.ts @@ -79,7 +79,9 @@ Deno.test( ) => { const src = ` console.log( - ${JSON.stringify(Object.keys(expectedEnv))}.map(k => Deno.env.get(k)) + ${ + JSON.stringify(Object.keys(expectedEnv)) + }.map(k => Deno.env.get(k) ?? null) )`; const { success, stdout } = await new Deno.Command(Deno.execPath(), { args: ["eval", src], diff --git a/tests/unit/worker_test.ts b/tests/unit/worker_test.ts index 88c6ca4c6f..42c257282c 100644 --- a/tests/unit/worker_test.ts +++ b/tests/unit/worker_test.ts @@ -5,6 +5,7 @@ // Requires to be run with `--allow-net` flag import { assert, assertEquals, assertMatch, assertThrows } from "@std/assert"; +import { toFileUrl } from "@std/path/to-file-url"; function resolveWorker(worker: string): string { return import.meta.resolve(`../testdata/workers/${worker}`); @@ -442,7 +443,31 @@ Deno.test("Worker limit children permissions", async function () { worker.terminate(); }); +function setupReadCheckGranularWorkerTest() { + const tempDir = Deno.realPathSync(Deno.makeTempDirSync()); + const initialPath = Deno.env.get("PATH")!; + const initialCwd = Deno.cwd(); + Deno.chdir(tempDir); + const envSep = Deno.build.os === "windows" ? ";" : ":"; + Deno.env.set("PATH", initialPath + envSep + tempDir); + + // create executables that will be resolved when doing `which` + const ext = Deno.build.os === "windows" ? ".exe" : ""; + Deno.copyFileSync(Deno.execPath(), tempDir + "/bar" + ext); + + return { + tempDir, + runFooFilePath: tempDir + "/foo" + ext, + [Symbol.dispose]() { + Deno.removeSync(tempDir, { recursive: true }); + Deno.env.set("PATH", initialPath); + Deno.chdir(initialCwd); + }, + }; +} + Deno.test("Worker limit children permissions granularly", async function () { + const ctx = setupReadCheckGranularWorkerTest(); const workerUrl = resolveWorker("read_check_granular_worker.js"); const worker = new Worker( workerUrl, @@ -453,8 +478,13 @@ Deno.test("Worker limit children permissions granularly", async function () { env: ["foo"], net: ["foo", "bar:8000"], ffi: [new URL("foo", workerUrl), "bar"], - read: [new URL("foo", workerUrl), "bar"], - run: [new URL("foo", workerUrl), "bar", "./baz"], + read: [new URL("foo", workerUrl), "bar", ctx.tempDir], + run: [ + toFileUrl(ctx.runFooFilePath), + "bar", + "./baz", + "unresolved-exec", + ], write: [new URL("foo", workerUrl), "bar"], }, }, @@ -482,8 +512,10 @@ Deno.test("Worker limit children permissions granularly", async function () { readAbsent: "prompt", runGlobal: "prompt", runFoo: "granted", + runFooPath: "granted", runBar: "granted", runBaz: "granted", + runUnresolved: "prompt", // unresolved binaries remain as "prompt" runAbsent: "prompt", writeGlobal: "prompt", writeFoo: "granted", @@ -494,6 +526,7 @@ Deno.test("Worker limit children permissions granularly", async function () { }); Deno.test("Nested worker limit children permissions", async function () { + const _cleanup = setupReadCheckGranularWorkerTest(); /** This worker has permissions but doesn't grant them to its children */ const worker = new Worker( resolveWorker("parent_read_check_worker.js"), @@ -521,8 +554,10 @@ Deno.test("Nested worker limit children permissions", async function () { readAbsent: "prompt", runGlobal: "prompt", runFoo: "prompt", + runFooPath: "prompt", runBar: "prompt", runBaz: "prompt", + runUnresolved: "prompt", runAbsent: "prompt", writeGlobal: "prompt", writeFoo: "prompt", diff --git a/tools/lint.js b/tools/lint.js index ae7b8470b0..a9f9400061 100755 --- a/tools/lint.js +++ b/tools/lint.js @@ -218,7 +218,7 @@ async function ensureNoNewITests() { "pm_tests.rs": 0, "publish_tests.rs": 0, "repl_tests.rs": 0, - "run_tests.rs": 334, + "run_tests.rs": 333, "shared_library_tests.rs": 0, "task_tests.rs": 4, "test_tests.rs": 0,