diff --git a/Cargo.toml b/Cargo.toml index c35a590..4652fd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,13 +15,13 @@ repository = "https://github.com/blocklessnetwork/bls-permissions" [workspace.dependencies] anyhow = "1.0.57" -deno_core = { version = "0.299.0" } -deno_path_util = "=0.2.0" +deno_core = { version = "0.314.1" } +deno_path_util = "=0.2.1" deno_terminal = "0.2.0" libc = "0.2.126" log = "0.4.20" fqdn = "0.3.4" -serde_json = "1.0.128" +serde_json = "1.0.85" serde = { version = "1.0.149", features = ["derive"] } url = { version = "< 2.5.0", features = ["serde", "expose_internals"] } once_cell = "1.17.1" @@ -29,4 +29,6 @@ which = "4.2.5" bls-permissions = {path = "crates/bls-permissions"} parking_lot = "0.12.0" percent-encoding = "2.3.0" -winapi = "=0.3.9" \ No newline at end of file +winapi = "=0.3.9" +thiserror = "1.0.61" + diff --git a/crates/bls-permissions/Cargo.toml b/crates/bls-permissions/Cargo.toml index 5cbf614..8aaae7d 100644 --- a/crates/bls-permissions/Cargo.toml +++ b/crates/bls-permissions/Cargo.toml @@ -19,9 +19,16 @@ parking_lot.workspace = true libc.workspace = true serde_json.workspace = true percent-encoding.workspace = true +thiserror.workspace = true +termcolor = { version = "1.1.3"} -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +[target.'cfg(not(target_family="wasm"))'.dependencies] which.workspace = true -[dev.dependencies] +[dev-dependencies] serde.workspace = true + +[features] +default = ["deno"] +deno=[] + diff --git a/crates/bls-permissions/src/error.rs b/crates/bls-permissions/src/error.rs index f0abe9a..1a09b42 100644 --- a/crates/bls-permissions/src/error.rs +++ b/crates/bls-permissions/src/error.rs @@ -87,5 +87,7 @@ pub fn get_custom_error_class(error: &Error) -> Option<&'static str> { #[inline(always)] pub fn is_yield_error_class(error: &Error) -> bool { - get_custom_error_class(error).map(|s| s == YIELD_CLASS).unwrap_or(false) + get_custom_error_class(error) + .map(|s| s == YIELD_CLASS) + .unwrap_or(false) } diff --git a/crates/bls-permissions/src/lib.rs b/crates/bls-permissions/src/lib.rs index 464170b..c83130f 100644 --- a/crates/bls-permissions/src/lib.rs +++ b/crates/bls-permissions/src/lib.rs @@ -1,17 +1,17 @@ -use parking_lot::lock_api::MutexGuard; -use parking_lot::RawMutex; -use serde::de; -use std::fmt; -use std::sync::Once; -use fqdn::FQDN; +use anyhow::bail; use anyhow::Context; +use fqdn::FQDN; use once_cell::sync::Lazy; use parking_lot::Mutex; +use path_utils::url_to_file_path; +use serde::de; use serde::Deserialize; use serde::Deserializer; use serde::Serialize; use std::borrow::Cow; use std::collections::HashSet; +use std::ffi::OsStr; +use std::fmt; use std::fmt::Debug; use std::hash::Hash; use std::net::IpAddr; @@ -19,31 +19,42 @@ use std::net::Ipv6Addr; use std::path::Component; use std::path::Path; use std::path::PathBuf; -use std::str::FromStr; -use std::sync::Arc; -use std::ffi::OsStr; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; -#[cfg(not(target_arch = "wasm32"))] -use which::which; +use std::sync::Arc; pub use url::Url; +#[cfg(not(target_family = "wasm"))] +use which::which; mod error; +mod path_utils; +mod terminal; use error::custom_error; +pub use error::is_yield_error_class; use error::type_error; use error::uri_error; -pub use error::is_yield_error_class; -#[cfg(target_arch = "wasm32")] +#[cfg(target_family = "wasm")] use error::yield_error; +use terminal::colors; mod prompter; -pub use prompter::*; use prompter::bls_permission_prompt as permission_prompt; +pub use prompter::*; pub type AnyError = anyhow::Error; pub type ModuleSpecifier = Url; +#[cfg(feature = "deno")] +const API: &'static str = "deno"; +#[cfg(feature = "deno")] +const UAPI: &'static str = "Deno"; + +#[cfg(not(feature = "deno"))] +const API: &'static str = "bls-runtime"; +#[cfg(not(feature = "deno"))] +const UAPI: &'static str = "Bls-runtime"; + /// Quadri-state value for storing permission state #[derive(Eq, PartialEq, Default, Debug, Clone, Copy, Deserialize, PartialOrd)] pub enum PermissionState { @@ -52,20 +63,51 @@ pub enum PermissionState { #[default] Prompt = 2, Denied = 3, - #[cfg(target_arch = "wasm32")] + #[cfg(target_family = "wasm")] Yield = 4, } static DEBUG_LOG_ENABLED: Lazy = Lazy::new(|| log::log_enabled!(log::Level::Debug)); -static DEBUG_LOG_MSG_FUNC: Mutex String + 'static + Send + Sync>>> = Mutex::new(None); +/// 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_net_descriptor_from_url(&self, url: &Url) -> Result { + NetDescriptor::from_url(url) + } + + fn parse_import_descriptor(&self, text: &str) -> Result; + + fn parse_import_descriptor_from_url(&self, url: &Url) -> Result { + ImportDescriptor::from_url(url) + } + + 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; -/// ensure init only once. -pub fn init_debug_log_msg_func(fun: impl Fn(&str) -> String + 'static + Send + Sync) { - static INIT_ONCE: Once = Once::new(); - INIT_ONCE.call_once(|| { - *DEBUG_LOG_MSG_FUNC.lock() = Some(Box::new(fun)); - }); + // queries + + fn parse_path_query(&self, path: &str) -> Result; + + fn parse_run_query(&self, requested: &str) -> Result; } static IS_STANDALONE: AtomicBool = AtomicBool::new(false); @@ -85,13 +127,37 @@ impl fmt::Display for PermissionState { PermissionState::GrantedPartial => f.pad("granted-partial"), PermissionState::Prompt => f.pad("prompt"), PermissionState::Denied => f.pad("denied"), - #[cfg(target_arch = "wasm32")] + #[cfg(target_family = "wasm")] PermissionState::Yield => f.pad("yield"), } } } impl PermissionState { + #[inline(always)] + fn log_perm_access(name: &str, info: impl FnOnce() -> Option) { + if *DEBUG_LOG_ENABLED { + log::debug!( + "{}", + colors::bold(&format!( + "{}️ Granted {}", + PERMISSION_EMOJI, + Self::fmt_access(name, info) + )) + ); + } + } + + fn fmt_access(name: &str, info: impl FnOnce() -> Option) -> String { + format!( + "{} access{}", + name, + info() + .map(|info| { format!(" to {info}") }) + .unwrap_or_default(), + ) + } + fn error(name: &str, info: impl FnOnce() -> Option) -> AnyError { let msg = if is_standalone() { format!( @@ -109,29 +175,6 @@ impl PermissionState { custom_error("PermissionDenied", msg) } - fn fmt_access(name: &str, info: impl FnOnce() -> Option) -> String { - format!( - "{} access{}", - name, - info() - .map(|info| { format!(" to {info}") }) - .unwrap_or_default(), - ) - } - - #[inline(always)] - fn log_perm_access(name: &str, info: impl FnOnce() -> Option) { - if *DEBUG_LOG_ENABLED { - let msg = Self::fmt_access(name, info); - let msg = if let Some(f) = DEBUG_LOG_MSG_FUNC.lock().as_ref() { - (f)(&msg) - } else { - msg - }; - log::debug!("{} Granted {msg}", PERMISSION_EMOJI); - } - } - /// Check the permission state. bool is whether a prompt was issued. #[inline] pub fn check( @@ -175,11 +218,11 @@ impl PermissionState { (Ok(()), true, true) } PromptResponse::Deny => (Err(Self::error(name, info)), true, false), - #[cfg(target_arch = "wasm32")] + #[cfg(target_family = "wasm")] PromptResponse::Yield => (Err(yield_error("yield.")), false, false), } } - + _ => (Err(Self::error(name, info)), false, false), } } @@ -220,49 +263,6 @@ macro_rules! skip_check_if_is_permission_fully_granted { }; } -fn parse_run_list(list: &Option>) -> Result, AnyError> { - let mut result = HashSet::new(); - if let Some(v) = list { - for s in v { - if s.is_empty() { - return Err(AnyError::msg("Empty path is not allowed")); - } else { - let desc = RunDescriptor::from(s.to_string()); - let aliases = desc.aliases(); - result.insert(desc); - result.extend(aliases); - } - } - } - Ok(result) -} - -fn parse_net_list(list: &Option>) -> Result, AnyError> { - if let Some(v) = list { - v.iter() - .map(|x| NetDescriptor::from_str(x)) - .collect::, AnyError>>() - } else { - Ok(HashSet::new()) - } -} - -fn parse_env_list(list: &Option>) -> 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()) - } -} - #[inline] pub fn normalize_path>(path: P) -> PathBuf { let mut components = path.as_ref().components().peekable(); @@ -291,56 +291,32 @@ pub fn normalize_path>(path: P) -> PathBuf { ret } -fn parse_path_list( - list: &Option>, - 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()) - } -} - #[inline] pub fn resolve_from_cwd(path: &Path) -> Result { if path.is_absolute() { Ok(normalize_path(path)) } else { #[allow(clippy::disallowed_methods)] - #[cfg(not(target_arch = "wasm32"))] - let cwd: PathBuf = std::env::current_dir().context("Failed to get current working directory")?; - #[cfg(target_arch = "wasm32")] + #[cfg(not(target_family = "wasm"))] + let cwd: PathBuf = + std::env::current_dir().context("Failed to get current working directory")?; + #[cfg(target_family = "wasm")] let cwd: PathBuf = "/".into(); Ok(normalize_path(cwd.join(path))) } } -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct UnaryPermission { - pub granted_global: bool, - pub granted_list: HashSet, - pub flag_denied_global: bool, - pub flag_denied_list: HashSet, - pub prompt_denied_global: bool, - pub prompt_denied_list: HashSet, - pub prompt: bool, -} +pub trait QueryDescriptor: Debug { + type AllowDesc: Debug + Eq + Clone + Hash; + type DenyDesc: Debug + Eq + Clone + Hash; -pub trait Descriptor: Eq + Clone + Hash { - type Arg: From; + fn flag_name() -> &'static str; + fn display_name(&self) -> Cow; + + fn from_allow(allow: &Self::AllowDesc) -> Self; - /// Parse this descriptor from a list of Self::Arg, which may have been converted from - /// command-line strings. - fn parse(list: &Option>) -> Result, AnyError>; + 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( @@ -349,19 +325,63 @@ 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; +} + +#[derive(Debug, Eq, PartialEq)] +pub struct UnaryPermission { + pub granted_global: bool, + pub granted_list: HashSet, + pub flag_denied_global: bool, + pub flag_denied_list: HashSet, + pub prompt_denied_global: bool, + pub prompt_denied_list: HashSet, + pub prompt: bool, +} + +impl Default for UnaryPermission { + fn default() -> Self { + UnaryPermission { + granted_global: Default::default(), + granted_list: Default::default(), + flag_denied_global: Default::default(), + flag_denied_list: Default::default(), + prompt_denied_global: Default::default(), + prompt_denied_list: Default::default(), + prompt: Default::default(), + } + } +} + +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) } - fn aliases(&self) -> Vec { - vec![] +} + +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 { +impl UnaryPermission { pub fn allow_all() -> Self { Self { granted_global: true, @@ -377,26 +397,21 @@ impl UnaryPermission { pub fn check_all_api(&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) } 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,110 +419,89 @@ 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 } - fn query_desc(&self, desc: Option<&T>, allow_partial: AllowPartial) -> PermissionState { - let aliases = desc.map_or(vec![], T::aliases); - for desc in [desc] - .into_iter() - .chain(aliases.iter().map(Some).collect::>()) - { - let state = if self.is_flag_denied(desc) || self.is_prompt_denied(desc) { - PermissionState::Denied - } else if self.is_granted(desc) { - match allow_partial { - AllowPartial::TreatAsGranted => PermissionState::Granted, - AllowPartial::TreatAsDenied => { - if self.is_partial_flag_denied(desc) { - PermissionState::Denied - } else { - PermissionState::Granted - } + fn query_desc(&self, desc: Option<&TQuery>, allow_partial: AllowPartial) -> PermissionState { + if self.is_flag_denied(desc) || self.is_prompt_denied(desc) { + PermissionState::Denied + } else if self.is_granted(desc) { + match allow_partial { + AllowPartial::TreatAsGranted => PermissionState::Granted, + AllowPartial::TreatAsDenied => { + if self.is_partial_flag_denied(desc) { + PermissionState::Denied + } else { + PermissionState::Granted } - AllowPartial::TreatAsPartialGranted => { - if self.is_partial_flag_denied(desc) { - PermissionState::GrantedPartial - } else { - PermissionState::Granted - } + } + AllowPartial::TreatAsPartialGranted => { + if self.is_partial_flag_denied(desc) { + PermissionState::GrantedPartial + } else { + PermissionState::Granted } } - } else if matches!(allow_partial, AllowPartial::TreatAsDenied) - && self.is_partial_flag_denied(desc) - { - PermissionState::Denied - } else { - PermissionState::Prompt - }; - if state != PermissionState::Prompt { - return state; } + } else if matches!(allow_partial, AllowPartial::TreatAsDenied) + && self.is_partial_flag_denied(desc) + { + PermissionState::Denied + } else { + PermissionState::Prompt } - PermissionState::Prompt } - 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; } + if !self.prompt { + return PermissionState::Denied; + } 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(), - Some("Deno.permissions.request()"), + TQuery::flag_name(), + Some(&format!("{UAPI}.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 => { self.insert_granted(None); PermissionState::Granted } - #[cfg(target_arch = "wasm32")] - PromptResponse::Yield => { - PermissionState::Yield - } + #[cfg(target_family = "wasm")] + PromptResponse::Yield => PermissionState::Yield, } } - 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)); - for alias in desc.aliases() { - self.granted_list.retain(|v| !v.stronger_than(&alias)); - } + self.granted_list.retain(|v| !desc.revokes(v)); } None => { self.granted_global = false; @@ -520,43 +514,58 @@ 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)), - } - } - - 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, + Some(query) => self.flag_denied_list.iter().any(|v| query.overlaps_deny(v)), } } - 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, @@ -564,23 +573,20 @@ impl UnaryPermission { ); } - fn list_insert(desc: Option, list_global: &mut bool, list: &mut HashSet) { + fn list_insert(desc: Option, list_global: &mut bool, list: &mut HashSet) { match desc { Some(desc) => { - let aliases = desc.aliases(); list.insert(desc); - for alias in aliases { - list.insert(alias); - } } None => *list_global = true, } } - pub fn create_child_permissions( + 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 { @@ -595,13 +601,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()); } } @@ -683,10 +691,55 @@ impl<'de> Deserialize<'de> for ChildUnaryPermissionArg { } #[derive(Clone, Eq, PartialEq, Hash, Debug)] -pub struct ReadDescriptor(pub PathBuf); +pub struct PathQueryDescriptor { + pub requested: String, + pub resolved: PathBuf, +} + +impl PathQueryDescriptor { + pub fn into_ffi(self) -> FfiQueryDescriptor { + FfiQueryDescriptor(self) + } + + 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 display_name(&self) -> Cow { + Cow::Borrowed(self.0.requested.as_str()) + } + + fn from_allow(allow: &Self::AllowDesc) -> Self { + PathQueryDescriptor { + requested: allow.0.to_string_lossy().into_owned(), + resolved: allow.0.clone(), + } + .into_read() + } + + fn as_allow(&self) -> Option { + Some(ReadDescriptor(self.0.resolved.clone())) + } -impl Descriptor for ReadDescriptor { - type Arg = PathBuf; + fn as_deny(&self) -> Self::DenyDesc { + ReadDescriptor(self.0.resolved.clone()) + } fn check_in_permission( &self, @@ -694,31 +747,62 @@ impl Descriptor for ReadDescriptor { 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>) -> Result, AnyError> { - parse_path_list(args, ReadDescriptor) + fn matches_allow(&self, other: &Self::AllowDesc) -> bool { + self.0.resolved.starts_with(&other.0) } - fn flag_name() -> &'static str { - "read" + 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 name(&self) -> Cow { - Cow::from(self.0.display().to_string()) + fn stronger_than_deny(&self, other: &Self::DenyDesc) -> bool { + other.0.starts_with(&self.0.resolved) } - fn stronger_than(&self, other: &Self) -> bool { - other.0.starts_with(&self.0) + fn overlaps_deny(&self, other: &Self::DenyDesc) -> bool { + self.stronger_than_deny(other) } } #[derive(Clone, Eq, PartialEq, Hash, Debug)] -pub struct WriteDescriptor(pub PathBuf); +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 display_name(&self) -> Cow { + Cow::Borrowed(&self.0.requested) + } + + 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())) + } -impl Descriptor for WriteDescriptor { - type Arg = PathBuf; + fn as_deny(&self) -> Self::DenyDesc { + WriteDescriptor(self.0.resolved.clone()) + } fn check_in_permission( &self, @@ -726,36 +810,42 @@ 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>) -> Result, AnyError> { - parse_path_list(args, WriteDescriptor) + fn matches_allow(&self, other: &Self::AllowDesc) -> bool { + self.0.resolved.starts_with(&other.0) } - fn flag_name() -> &'static str { - "write" + 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 name(&self) -> Cow { - Cow::from(self.0.display().to_string()) + fn stronger_than_deny(&self, other: &Self::DenyDesc) -> bool { + other.0.starts_with(&self.0.resolved) } - fn stronger_than(&self, other: &Self) -> bool { - other.0.starts_with(&self.0) + 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), Ip(IpAddr), } -impl FromStr for Host { - type Err = AnyError; - - fn from_str(s: &str) -> Result { +impl Host { + // TODO(bartlomieju): rewrite to not use `AnyError` but a specific error implementations + pub fn parse(s: &str) -> Result { if s.starts_with('[') && s.ends_with(']') { let ip = s[1..s.len() - 1] .parse::() @@ -775,20 +865,49 @@ impl FromStr for Host { } else { Cow::Owned(s.to_ascii_lowercase()) }; - let fqdn = FQDN::from_str(&lower).with_context(|| format!("invalid host: '{s}'"))?; + let fqdn = { + use std::str::FromStr; + FQDN::from_str(&lower).with_context(|| format!("invalid host: '{s}'"))? + }; if fqdn.is_root() { return Err(uri_error(format!("invalid empty host: '{s}'"))); } Ok(Host::Fqdn(fqdn)) } } + + #[track_caller] + pub fn must_parse(s: &str) -> Self { + Self::parse(s).unwrap() + } } #[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, @@ -796,30 +915,39 @@ 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>) -> 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 } } -impl FromStr for NetDescriptor { - type Err = AnyError; +// TODO(bartlomieju): rewrite to not use `AnyError` but a specific error implementations +impl NetDescriptor { + pub fn parse(hostname: &str) -> Result { + if hostname.starts_with("http://") || hostname.starts_with("https://") { + return Err(uri_error(format!( + "invalid value '{hostname}': URLs are not supported, only domains and ips" + ))); + } - fn from_str(hostname: &str) -> Result { // If this is a IPv6 address enclosed in square brackets, parse it as such. if hostname.starts_with('[') { if let Some((ip, after)) = hostname.split_once(']') { @@ -850,7 +978,7 @@ impl FromStr for NetDescriptor { Some((host, port)) => (host, port), None => (hostname, ""), }; - let host = host.parse::()?; + let host = Host::parse(host)?; let port = if port.is_empty() { None @@ -872,6 +1000,15 @@ impl FromStr for NetDescriptor { Ok(NetDescriptor(host, port)) } + + pub fn from_url(url: &Url) -> Result { + let host = url + .host_str() + .ok_or_else(|| type_error(format!("Missing host in url: '{}'", url)))?; + let host = Host::parse(host)?; + let port = url.port_or_known_default(); + Ok(NetDescriptor(host, port)) + } } impl fmt::Display for NetDescriptor { @@ -889,16 +1026,31 @@ impl fmt::Display for NetDescriptor { } #[derive(Clone, Eq, PartialEq, Hash, Debug)] -pub struct EnvDescriptor(EnvVarName); +pub struct ImportDescriptor(NetDescriptor); -impl EnvDescriptor { - pub fn new(env: impl AsRef) -> Self { - Self(EnvVarName::new(env)) +impl QueryDescriptor for ImportDescriptor { + type AllowDesc = ImportDescriptor; + type DenyDesc = ImportDescriptor; + + fn flag_name() -> &'static str { + "import" + } + + fn display_name(&self) -> Cow { + self.0.display_name() + } + + fn from_allow(allow: &Self::AllowDesc) -> Self { + Self(NetDescriptor::from_allow(&allow.0)) + } + + fn as_allow(&self) -> Option { + self.0.as_allow().map(ImportDescriptor) } -} -impl Descriptor for EnvDescriptor { - type Arg = String; + fn as_deny(&self) -> Self::DenyDesc { + Self(self.0.as_deny()) + } fn check_in_permission( &self, @@ -906,20 +1058,101 @@ 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 matches_allow(&self, other: &Self::AllowDesc) -> bool { + self.0.matches_allow(&other.0) } - fn parse(list: &Option>) -> Result, AnyError> { - parse_env_list(list) + fn matches_deny(&self, other: &Self::DenyDesc) -> bool { + self.0.matches_deny(&other.0) } + fn revokes(&self, other: &Self::AllowDesc) -> bool { + self.0.revokes(&other.0) + } + + fn stronger_than_deny(&self, other: &Self::DenyDesc) -> bool { + self.0.stronger_than_deny(&other.0) + } + + fn overlaps_deny(&self, other: &Self::DenyDesc) -> bool { + self.0.overlaps_deny(&other.0) + } +} + +impl ImportDescriptor { + pub fn parse(specifier: &str) -> Result { + Ok(ImportDescriptor(NetDescriptor::parse(specifier)?)) + } + + pub fn from_url(url: &Url) -> Result { + Ok(ImportDescriptor(NetDescriptor::from_url(url)?)) + } +} + +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub struct EnvDescriptor(EnvVarName); + +impl EnvDescriptor { + pub fn new(env: impl AsRef) -> Self { + Self(EnvVarName::new(env)) + } +} + +impl QueryDescriptor for EnvDescriptor { + type AllowDesc = EnvDescriptor; + type DenyDesc = EnvDescriptor; + fn flag_name() -> &'static str { "env" } - fn name(&self) -> Cow { + 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, + 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 { + self == other + } + + fn matches_deny(&self, other: &Self::DenyDesc) -> bool { + self == other + } + + 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 + } } impl AsRef for EnvDescriptor { @@ -928,18 +1161,94 @@ impl AsRef for EnvDescriptor { } } -#[derive(Clone, Eq, PartialEq, Hash, Debug)] -pub enum RunDescriptor { - /// Warning: You may want to construct with `RunDescriptor::from()` for case - /// handling. +#[derive(Clone, Eq, PartialEq, Hash, Debug, Serialize, Deserialize)] +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), - /// Warning: You may want to construct with `RunDescriptor::from()` for case - /// handling. - Path(PathBuf), } -impl Descriptor for RunDescriptor { - type Arg = 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 { + #[cfg(not(target_family = "wasm"))] + match which(requested) { + Ok(resolved) => Ok(RunQueryDescriptor::Path { + requested: requested.to_string(), + resolved, + }), + Err(_) => Ok(RunQueryDescriptor::Name(requested.to_string())), + } + #[cfg(target_family = "wasm")] + 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, @@ -947,85 +1256,214 @@ impl Descriptor for RunDescriptor { 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>) -> Result, AnyError> { - parse_run_list(args) + perm.check_desc(Some(self), false, api_name) } - fn flag_name() -> &'static str { - "run" + fn matches_allow(&self, other: &Self::AllowDesc) -> bool { + match self { + RunQueryDescriptor::Path { resolved, .. } => *resolved == other.0, + RunQueryDescriptor::Name(_) => false, + } } - fn name(&self) -> Cow { - Cow::from(self.to_string()) + 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 aliases(&self) -> Vec { + fn revokes(&self, other: &Self::AllowDesc) -> bool { match self { - #[cfg(not(target_arch = "wasm32"))] - RunDescriptor::Name(name) => match which(name) { - Ok(path) => vec![RunDescriptor::Path(path)], - Err(_) => vec![], - }, - #[cfg(target_arch = "wasm32")] - RunDescriptor::Name(_) => vec![], - RunDescriptor::Path(_) => vec![], + 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 { + Name(String), + 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. + #[cfg(not(target_family = "wasm"))] + Unresolved(Box), + #[cfg(target_family = "wasm")] + Unresolved(Box), + Descriptor(AllowRunDescriptor), +} + +#[inline] +fn resolve_from_known_cwd(path: &Path, cwd: &Path) -> PathBuf { + if path.is_absolute() { + normalize_path(path) + } else { + normalize_path(cwd.join(path)) + } } -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()) +#[derive(Debug, Clone, Hash, Eq, PartialEq)] +pub struct AllowRunDescriptor(pub PathBuf); + +impl AllowRunDescriptor { + #[cfg(not(target_family = "wasm"))] + 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 { - Self::Name(s) - } + 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), + )) + } + + #[cfg(target_family = "wasm")] + 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 { + resolve_from_known_cwd(Path::new(&format!("/{text}")), cwd) + }; + Ok(AllowRunDescriptorParseResult::Descriptor( + AllowRunDescriptor(path), + )) } } -impl From for RunDescriptor { - fn from(p: PathBuf) -> Self { - #[cfg(windows)] - let p = PathBuf::from(p.to_string_lossy().to_string().to_lowercase()); - if p.is_absolute() { - Self::Path(p) +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub enum DenyRunDescriptor { + /// Warning: You may want to construct with `RunDescriptor::from()` for case + /// handling. + Name(String), + /// Warning: You may want to construct with `RunDescriptor::from()` for case + /// handling. + Path(PathBuf), +} + +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 { - Self::Path(resolve_from_cwd(&p).unwrap()) + DenyRunDescriptor::Name(text.to_string()) } } } -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()), - } +fn is_path(text: &str) -> bool { + if cfg!(windows) { + text.contains('/') || text.contains('\\') || Path::new(text).is_absolute() + } else { + text.contains('/') } } -impl AsRef for RunDescriptor { - fn as_ref(&self) -> &Path { - match self { - RunDescriptor::Name(s) => s.as_ref(), - RunDescriptor::Path(s) => s.as_ref(), +pub 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; + } + 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(String); + +impl SysDescriptor { + pub fn parse(kind: String) -> Result { + match kind.as_str() { + "hostname" | "osRelease" | "osUptime" | "loadavg" | "networkInterfaces" + | "systemMemoryInfo" | "uid" | "gid" | "cpus" | "homedir" | "getegid" | "username" + | "statfs" | "getPriority" | "setPriority" => Ok(Self(kind)), + _ => Err(type_error(format!("unknown system info kind \"{kind}\""))), } } + + pub fn into_string(self) -> String { + self.0 + } } -#[derive(Clone, Eq, PartialEq, Hash, Debug)] -pub struct SysDescriptor(pub String); +impl QueryDescriptor for SysDescriptor { + type AllowDesc = SysDescriptor; + type DenyDesc = SysDescriptor; + + fn flag_name() -> &'static str { + "sys" + } -impl Descriptor for SysDescriptor { - type Arg = String; + 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, @@ -1033,36 +1471,60 @@ 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>) -> 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 } -} -pub fn parse_sys_kind(kind: &str) -> Result<&str, AnyError> { - match kind { - "hostname" | "osRelease" | "osUptime" | "loadavg" | "networkInterfaces" - | "systemMemoryInfo" | "uid" | "gid" | "cpus" | "homedir" | "getegid" | "username" - | "statfs" | "getPriority" | "setPriority" => Ok(kind), - _ => Err(type_error(format!("unknown system info kind \"{kind}\""))), + fn overlaps_deny(&self, _other: &Self::DenyDesc) -> bool { + false } } #[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, @@ -1070,153 +1532,106 @@ 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>) -> 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) } -} -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, - ) + fn overlaps_deny(&self, other: &Self::DenyDesc) -> bool { + self.stronger_than_deny(other) } +} - 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()), - ) +#[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 revoke(&mut self, path: Option<&Path>) -> PermissionState { - self.revoke_desc( - path.map(|p| ReadDescriptor(resolve_from_cwd(p).unwrap())) - .as_ref(), - ) + pub fn request(&mut self, path: Option<&ReadQueryDescriptor>) -> PermissionState { + self.request_desc(path) } - pub fn check(&mut self, path: &Path, 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())), - ) + pub fn revoke(&mut self, desc: Option<&ReadQueryDescriptor>) -> PermissionState { + self.revoke_desc(desc) } - #[inline] - pub fn check_partial(&mut self, path: &Path, api_name: Option<&str>) -> Result<(), AnyError> { + pub fn check( + &mut self, + 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())) - }) + self.check_desc(Some(desc), true, api_name) } - /// As `check()`, but permission error messages will anonymize the path - /// by replacing it with the given `display`. - pub fn check_blind( + #[inline] + pub fn check_partial( &mut self, - path: &Path, - display: &str, - api_name: &str, + 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, 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, api_name: Option<&str>) -> Result<(), AnyError> { + pub fn check( + &mut self, + 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, 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( + pub fn check_partial( &mut self, - path: &Path, - display: &str, - api_name: &str, + path: &WriteQueryDescriptor, + api_name: Option<&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) } } @@ -1226,7 +1641,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 { @@ -1235,25 +1650,40 @@ impl UnaryPermission { pub fn check(&mut self, host: &NetDescriptor, 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(&mut self, url: &url::Url, api_name: Option<&str>) -> Result<(), AnyError> { + pub fn check_all(&mut self) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - let host = url - .host_str() - .ok_or_else(|| type_error(format!("Missing host in url: '{}'", url)))?; - let host = host.parse::()?; - 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(None, false, None) + } +} + +impl UnaryPermission { + pub fn query(&self, host: Option<&ImportDescriptor>) -> PermissionState { + self.query_desc(host, AllowPartial::TreatAsPartialGranted) + } + + pub fn request(&mut self, host: Option<&ImportDescriptor>) -> PermissionState { + self.request_desc(host) + } + + pub fn revoke(&mut self, host: Option<&ImportDescriptor>) -> PermissionState { + self.revoke_desc(host) + } + + pub fn check( + &mut self, + host: &ImportDescriptor, + api_name: Option<&str>, + ) -> Result<(), AnyError> { + skip_check_if_is_permission_fully_granted!(self); + self.check_desc(Some(host), 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) } } @@ -1266,7 +1696,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 { @@ -1275,144 +1705,124 @@ impl UnaryPermission { pub fn check(&mut self, env: &str, 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) } } impl UnaryPermission { - pub fn query(&self, kind: Option<&str>) -> PermissionState { - self.query_desc( - kind.map(|k| SysDescriptor(k.to_string())).as_ref(), - AllowPartial::TreatAsPartialGranted, - ) + pub fn query(&self, kind: Option<&SysDescriptor>) -> PermissionState { + self.query_desc(kind, AllowPartial::TreatAsPartialGranted) } - pub fn request(&mut self, kind: Option<&str>) -> PermissionState { - self.request_desc(kind.map(|k| SysDescriptor(k.to_string())).as_ref(), || None) + pub fn request(&mut self, kind: Option<&SysDescriptor>) -> PermissionState { + self.request_desc(kind) } - pub fn revoke(&mut self, kind: Option<&str>) -> PermissionState { - self.revoke_desc(kind.map(|k| SysDescriptor(k.to_string())).as_ref()) + pub fn revoke(&mut self, kind: Option<&SysDescriptor>) -> PermissionState { + self.revoke_desc(kind) } - pub fn check(&mut self, kind: &str, api_name: Option<&str>) -> Result<(), AnyError> { + pub fn check(&mut self, kind: &SysDescriptor, 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(kind), 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: &str, api_name: Option<&str>) -> Result<(), AnyError> { - skip_check_if_is_permission_fully_granted!(self); - self.check_desc( - Some(&RunDescriptor::from(cmd.to_string())), - false, - api_name, - || Some(format!("\"{}\"", cmd)), - ) + pub fn check( + &mut self, + cmd: &RunQueryDescriptor, + api_name: Option<&str>, + ) -> Result<(), AnyError> { + 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 + pub fn query_all(&mut self, api_name: Option<&str>) -> bool { + if self.is_allow_all() { + return true; + } + let (result, _prompted, _is_allow_all) = + self.query_desc(None, AllowPartial::TreatAsDenied).check2( + RunQueryDescriptor::flag_name(), + api_name, + || None, + /* prompt */ false, + ); + result.is_ok() } } -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, api_name: Option<&str>) -> Result<(), AnyError> { + pub fn check( + &mut self, + 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 import: UnaryPermission, pub all: UnitPermission, - pub hrtime: UnitPermission, } #[derive(Clone, Debug, Eq, PartialEq, Default, Serialize, Deserialize)] @@ -1420,66 +1830,41 @@ pub struct PermissionsOptions { pub allow_all: bool, pub allow_env: Option>, pub deny_env: Option>, - pub allow_hrtime: bool, - pub deny_hrtime: bool, 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_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 allow_import: Option>, pub prompt: bool, } -impl Default for UnaryPermission { - fn default() -> Self { - UnaryPermission { - granted_global: Default::default(), - granted_list: Default::default(), - flag_denied_global: Default::default(), - flag_denied_list: Default::default(), - prompt_denied_global: Default::default(), - prompt_denied_list: Default::default(), - prompt: Default::default(), - } - } -} - impl Permissions { - pub fn new_unary( - allow_list: &Option>, - deny_list: &Option>, + 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() }) } - pub const fn new_hrtime(allow_state: bool, deny_state: bool) -> UnitPermission { - unit_permission_from_flag_bools( - allow_state, - deny_state, - "hrtime", - "high precision time", - false, // never prompt for hrtime - ) - } - pub const fn new_all(allow_state: bool) -> UnitPermission { unit_permission_from_flag_bools( allow_state, @@ -1490,17 +1875,150 @@ 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, &opts.deny_read, opts.prompt)?, - write: Permissions::new_unary(&opts.allow_write, &opts.deny_write, opts.prompt)?, - net: Permissions::new_unary(&opts.allow_net, &opts.deny_net, opts.prompt)?, - env: Permissions::new_unary(&opts.allow_env, &opts.deny_env, opts.prompt)?, - sys: Permissions::new_unary(&opts.allow_sys, &opts.deny_sys, opts.prompt)?, - run: Permissions::new_unary(&opts.allow_run, &opts.deny_run, opts.prompt)?, - ffi: Permissions::new_unary(&opts.allow_ffi, &opts.deny_ffi, opts.prompt)?, + read: Permissions::new_unary( + 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( + parse_maybe_vec(opts.allow_write.as_deref(), |item| { + parser.parse_write_descriptor(item) + })?, + deny_write, + opts.prompt, + )?, + net: Permissions::new_unary( + 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( + 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( + 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( + allow_run, + parse_maybe_vec(opts.deny_run.as_deref(), |text| { + parser.parse_deny_run_descriptor(text) + })?, + opts.prompt, + )?, + ffi: Permissions::new_unary( + 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, + )?, + import: Permissions::new_unary( + parse_maybe_vec(opts.allow_import.as_deref(), |item| { + parser.parse_import_descriptor(item) + })?, + None, + opts.prompt, + )?, all: Permissions::new_all(opts.allow_all), - hrtime: Permissions::new_hrtime(opts.allow_hrtime, opts.deny_hrtime), }) } @@ -1514,8 +2032,8 @@ impl Permissions { sys: UnaryPermission::allow_all(), run: UnaryPermission::allow_all(), ffi: UnaryPermission::allow_all(), + import: UnaryPermission::allow_all(), all: Permissions::new_all(true), - hrtime: Permissions::new_hrtime(true, false), } } @@ -1531,55 +2049,29 @@ impl Permissions { fn none(prompt: bool) -> Self { Self { - read: Permissions::new_unary(&None, &None, prompt).unwrap(), - write: Permissions::new_unary(&None, &None, prompt).unwrap(), - net: Permissions::new_unary(&None, &None, prompt).unwrap(), - env: Permissions::new_unary(&None, &None, prompt).unwrap(), - sys: Permissions::new_unary(&None, &None, prompt).unwrap(), - run: Permissions::new_unary(&None, &None, prompt).unwrap(), - ffi: Permissions::new_unary(&None, &None, prompt).unwrap(), + read: Permissions::new_unary(None, None, prompt).unwrap(), + write: Permissions::new_unary(None, None, prompt).unwrap(), + net: Permissions::new_unary(None, None, prompt).unwrap(), + env: Permissions::new_unary(None, None, prompt).unwrap(), + sys: Permissions::new_unary(None, None, prompt).unwrap(), + run: Permissions::new_unary(None, None, prompt).unwrap(), + ffi: Permissions::new_unary(None, None, prompt).unwrap(), + import: Permissions::new_unary(None, None, prompt).unwrap(), all: Permissions::new_all(false), - hrtime: Permissions::new_hrtime(false, false), - } - } - - /// A helper function that determines if the module specifier is a local or - /// remote, and performs a read or net check for the specifier. - pub fn check_specifier(&mut self, specifier: &ModuleSpecifier) -> Result<(), AnyError> { - #[cfg(target_arch="wasm32")] - let filepath = to_file_path(specifier); - #[cfg(not(target_arch="wasm32"))] - let filepath = specifier.to_file_path(); - match specifier.scheme() { - "file" => match filepath { - Ok(path) => self.read.check(&path, Some("import()")), - Err(_) => Err(uri_error(format!( - "Invalid file path.\n Specifier: {specifier}" - ))), - }, - "data" => Ok(()), - "blob" => Ok(()), - _ => self.net.check_url(specifier, Some("import()")), } } } -#[cfg(target_arch="wasm32")] -fn to_file_path(url: &Url) -> Result { - if let Some(segments) = url.path_segments() { - let host = match url.host() { - None | Some(url::Host::Domain("localhost")) => None, - _ => return Err(()), - }; - - return file_url_segments_to_pathbuf(host, segments); - } - Err(()) +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum CheckSpecifierKind { + Static, + Dynamic, } /// the file_url_segments_to_pathbuf is come from url crate -/// which is not for wasm32 -#[cfg(target_arch = "wasm32")] +/// which is not for wasm +#[allow(dead_code)] +#[cfg(target_family = "wasm")] fn file_url_segments_to_pathbuf( host: Option<&str>, segments: std::str::Split<'_, char>, @@ -1603,9 +2095,8 @@ fn file_url_segments_to_pathbuf( bytes.push(b'/'); } - let path_str = unsafe { - String::from_raw_parts(bytes.as_mut_ptr(), bytes.len(), bytes.capacity()) - }; + let path_str = + unsafe { String::from_raw_parts(bytes.as_mut_ptr(), bytes.len(), bytes.capacity()) }; let path = PathBuf::from(path_str); debug_assert!( @@ -1634,14 +2125,14 @@ impl UnitPermission { let resp = permission_prompt( &format!("access to {}", self.description), self.name, - Some("Deno.permissions.query()"), + Some("{UAPI}.permissions.query()"), false, ); if PromptResponse::Allow == resp { self.state = PermissionState::Granted; } else { self.state = PermissionState::Denied; - #[cfg(target_arch = "wasm32")] + #[cfg(target_family = "wasm")] if PromptResponse::Yield == resp { self.state = PermissionState::Yield; } @@ -1670,7 +2161,10 @@ impl UnitPermission { result } - pub fn create_child_permissions(&mut self, flag: ChildUnitPermissionArg) -> Result { + pub fn create_child_permissions( + &mut self, + flag: ChildUnitPermissionArg, + ) -> Result { let mut perm = self.clone(); match flag { ChildUnitPermissionArg::Inherit => { @@ -1745,7 +2239,7 @@ impl<'de> Deserialize<'de> for ChildUnitPermissionArg { } } -fn global_from_option(flag: &Option>) -> bool { +fn global_from_option(flag: Option<&HashSet>) -> bool { matches!(flag, Some(v) if v.is_empty()) } @@ -1770,22 +2264,6 @@ const fn unit_permission_from_flag_bools( } } -fn parse_sys_list(list: &Option>) -> 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 escalation_error() -> AnyError { custom_error( "PermissionDenied", @@ -1825,82 +2303,293 @@ impl From for AllowPartial { /// 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 BlsPermissionsContainer(pub Arc>); +pub struct BlsPermissionsContainer { + descriptor_parser: Arc, + pub inner: Arc>, +} impl BlsPermissionsContainer { - - #[inline(always)] - pub fn lock(&self) -> MutexGuard<'_, RawMutex, Permissions> { - self.0.lock() + pub fn new(descriptor_parser: Arc, perms: Permissions) -> Self { + Self { + descriptor_parser, + inner: Arc::new(Mutex::new(perms)), + } } - pub fn new(perms: Permissions) -> Self { - // init_debug_log_msg_func(|msg: &str| format!("{}", colors::bold(msg))); - Self(Arc::new(Mutex::new(perms))) + pub fn allow_all(descriptor_parser: Arc) -> Self { + Self::new(descriptor_parser, Permissions::allow_all()) } - #[inline(always)] - pub fn allow_hrtime(&self) -> bool { - self.0.lock().hrtime.check().is_ok() + pub fn lock(&self) -> parking_lot::lock_api::MutexGuard { + self.inner.lock() } - pub fn allow_all() -> Self { - Self::new(Permissions::allow_all()) + #[inline(always)] + pub fn create_child_permissions( + &self, + child_permissions_arg: ChildPermissionsArg, + ) -> Result { + fn is_granted_unary(arg: &ChildUnaryPermissionArg) -> bool { + match arg { + ChildUnaryPermissionArg::Inherit | ChildUnaryPermissionArg::Granted => true, + ChildUnaryPermissionArg::NotGranted | ChildUnaryPermissionArg::GrantedList(_) => { + false + } + } + } + + let mut worker_perms = Permissions::none_without_prompt(); + + let mut inner = self.inner.lock(); + worker_perms.all = inner + .all + .create_child_permissions(ChildUnitPermissionArg::Inherit)?; + + // downgrade the `worker_perms.all` based on the other values + if worker_perms.all.query() == PermissionState::Granted { + let unary_perms = [ + &child_permissions_arg.read, + &child_permissions_arg.write, + &child_permissions_arg.net, + &child_permissions_arg.import, + &child_permissions_arg.env, + &child_permissions_arg.sys, + &child_permissions_arg.run, + &child_permissions_arg.ffi, + ]; + let allow_all = unary_perms.into_iter().all(is_granted_unary); + if !allow_all { + worker_perms.all.revoke(); + } + } + + // WARNING: When adding a permission here, ensure it is handled + // in the worker_perms.all block above + worker_perms.read = inner + .read + .create_child_permissions(child_permissions_arg.read, |text| { + Ok(Some(self.descriptor_parser.parse_read_descriptor(text)?)) + })?; + worker_perms.write = inner + .write + .create_child_permissions(child_permissions_arg.write, |text| { + Ok(Some(self.descriptor_parser.parse_write_descriptor(text)?)) + })?; + worker_perms.import = inner + .import + .create_child_permissions(child_permissions_arg.import, |text| { + Ok(Some(self.descriptor_parser.parse_import_descriptor(text)?)) + })?; + worker_perms.net = inner + .net + .create_child_permissions(child_permissions_arg.net, |text| { + Ok(Some(self.descriptor_parser.parse_net_descriptor(text)?)) + })?; + worker_perms.env = inner + .env + .create_child_permissions(child_permissions_arg.env, |text| { + Ok(Some(self.descriptor_parser.parse_env_descriptor(text)?)) + })?; + worker_perms.sys = inner + .sys + .create_child_permissions(child_permissions_arg.sys, |text| { + Ok(Some(self.descriptor_parser.parse_sys_descriptor(text)?)) + })?; + worker_perms.run = + inner + .run + .create_child_permissions(child_permissions_arg.run, |text| { + match self.descriptor_parser.parse_allow_run_descriptor(text)? { + AllowRunDescriptorParseResult::Unresolved(_) => Ok(None), + AllowRunDescriptorParseResult::Descriptor(desc) => Ok(Some(desc)), + } + })?; + worker_perms.ffi = inner + .ffi + .create_child_permissions(child_permissions_arg.ffi, |text| { + Ok(Some(self.descriptor_parser.parse_ffi_descriptor(text)?)) + })?; + + Ok(BlsPermissionsContainer::new( + self.descriptor_parser.clone(), + worker_perms, + )) } #[inline(always)] - pub fn check_specifier(&self, specifier: &ModuleSpecifier) -> Result<(), AnyError> { - self.0.lock().check_specifier(specifier) + pub fn check_specifier( + &self, + specifier: &ModuleSpecifier, + kind: CheckSpecifierKind, + ) -> Result<(), AnyError> { + let mut inner = self.inner.lock(); + match specifier.scheme() { + "file" => { + if inner.read.is_allow_all() || kind == CheckSpecifierKind::Static { + return Ok(()); + } + + match url_to_file_path(specifier) { + Ok(path) => inner.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}" + ))), + } + } + "data" => Ok(()), + "blob" => Ok(()), + _ => { + if inner.import.is_allow_all() { + return Ok(()); // avoid allocation below + } + + let desc = self + .descriptor_parser + .parse_import_descriptor_from_url(specifier)?; + inner.import.check(&desc, Some("import()"))?; + Ok(()) + } + } } + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] #[inline(always)] - pub fn check_read(&self, path: &Path, api_name: &str) -> Result<(), AnyError> { - self.0.lock().read.check(path, Some(api_name)) + pub fn check_read(&self, path: &str, api_name: &str) -> 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_blind( + 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, path: &Path, 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(&self, api_name: &str) -> Result<(), AnyError> { - self.0.lock().read.check_all(Some(api_name)) + self.inner.lock().read.check_all(Some(api_name)) } #[inline(always)] - pub fn check_write(&self, path: &Path, api_name: &str) -> Result<(), AnyError> { - self.0.lock().write.check(path, Some(api_name)) + pub fn query_read_all(&self) -> bool { + self.inner.lock().read.query(None) == PermissionState::Granted } + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] + #[inline(always)] + pub fn check_write(&self, path: &str, api_name: &str) -> 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(&self, api_name: &str) -> Result<(), AnyError> { - self.0.lock().write.check_all(Some(api_name)) + 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( &self, @@ -1908,59 +2597,85 @@ impl BlsPermissionsContainer { 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: &str, api_name: &str) -> 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_write_partial(&self, path: &Path, api_name: &str) -> Result<(), AnyError> { - self.0.lock().write.check_partial(path, Some(api_name)) + pub fn check_run(&mut self, cmd: &RunQueryDescriptor, api_name: &str) -> Result<(), AnyError> { + self.inner.lock().run.check(cmd, Some(api_name)) } #[inline(always)] - pub fn check_run(&self, cmd: &str, api_name: &str) -> Result<(), AnyError> { - self.0.lock().run.check(cmd, Some(api_name)) + pub fn check_run_all(&mut self, api_name: &str) -> Result<(), AnyError> { + self.inner.lock().run.check_all(Some(api_name)) } #[inline(always)] - pub fn check_run_all(&self, api_name: &str) -> Result<(), AnyError> { - self.0.lock().run.check_all(Some(api_name)) + pub fn query_run_all(&mut self, api_name: &str) -> bool { + 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( + &self.descriptor_parser.parse_sys_descriptor(kind)?, + Some(api_name), + ) } #[inline(always)] pub fn check_env(&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(&self) -> Result<(), AnyError> { - self.0.lock().env.check_all() + pub fn check_env_all(&mut self) -> Result<(), AnyError> { + self.inner.lock().env.check_all() } #[inline(always)] - pub fn check_sys_all(&self) -> Result<(), AnyError> { - self.0.lock().sys.check_all() + pub fn check_sys_all(&mut self) -> Result<(), AnyError> { + self.inner.lock().sys.check_all() } #[inline(always)] - pub fn check_ffi_all(& self) -> Result<(), AnyError> { - self.0.lock().ffi.check_all() + pub fn check_ffi_all(&mut self) -> Result<(), AnyError> { + 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(&self) -> Result<(), AnyError> { - self.0.lock().all.check() + pub fn check_was_allow_all_flag_passed(&mut self) -> Result<(), AnyError> { + self.inner.lock().all.check() } /// Checks special file access, returning the failed permission type if /// not successful. - pub fn check_special_file(&self, path: &Path, _api_name: &str) -> Result<(), &'static str> { + pub fn check_special_file(&mut self, path: &Path, _api_name: &str) -> Result<(), &'static str> { let error_all = |_| "all"; // Safe files with no major additional side-effects. While there's a small risk of someone @@ -2057,7 +2772,12 @@ impl BlsPermissionsContainer { #[inline(always)] pub fn check_net_url(&self, url: &Url, api_name: &str) -> Result<(), AnyError> { - self.0.lock().net.check_url(url, Some(api_name)) + let mut inner = self.inner.lock(); + if inner.net.is_allow_all() { + return Ok(()); + } + let desc = self.descriptor_parser.parse_net_descriptor_from_url(url)?; + inner.net.check(&desc, Some(api_name)) } #[inline(always)] @@ -2066,19 +2786,435 @@ impl BlsPermissionsContainer { host: &(T, Option), api_name: &str, ) -> Result<(), AnyError> { - let hostname = host.0.as_ref().parse::()?; + 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(&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_no_path(&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(&self, path: Option<&Path>) -> Result<(), AnyError> { - self.0.lock().ffi.check(path.unwrap(), None) + pub fn check_ffi_partial_with_path(&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_partial(Some(&desc))?; + Ok(desc.0.resolved) + } + } + + // query + + #[inline(always)] + pub fn query_read(&self, path: Option<&str>) -> Result { + let inner = self.inner.lock(); + let permission = &inner.read; + if permission.is_allow_all() { + return Ok(PermissionState::Granted); + } + Ok(permission.query( + path.map(|path| { + Result::<_, AnyError>::Ok( + self.descriptor_parser.parse_path_query(path)?.into_read(), + ) + }) + .transpose()? + .as_ref(), + )) + } + + #[inline(always)] + pub fn query_write(&self, path: Option<&str>) -> Result { + let inner = self.inner.lock(); + let permission = &inner.write; + if permission.is_allow_all() { + return Ok(PermissionState::Granted); + } + Ok(permission.query( + path.map(|path| { + Result::<_, AnyError>::Ok( + self.descriptor_parser.parse_path_query(path)?.into_write(), + ) + }) + .transpose()? + .as_ref(), + )) + } + + #[inline(always)] + pub fn query_net(&self, host: Option<&str>) -> Result { + let inner = self.inner.lock(); + let permission = &inner.net; + if permission.is_allow_all() { + return Ok(PermissionState::Granted); + } + Ok(permission.query( + match host { + None => None, + Some(h) => Some(self.descriptor_parser.parse_net_descriptor(h)?), + } + .as_ref(), + )) + } + + #[inline(always)] + pub fn query_env(&self, var: Option<&str>) -> PermissionState { + let inner = self.inner.lock(); + let permission = &inner.env; + if permission.is_allow_all() { + return PermissionState::Granted; + } + permission.query(var) + } + + #[inline(always)] + pub fn query_sys(&self, kind: Option<&str>) -> Result { + let inner = self.inner.lock(); + let permission = &inner.sys; + if permission.is_allow_all() { + return Ok(PermissionState::Granted); + } + Ok(permission.query( + kind.map(|kind| self.descriptor_parser.parse_sys_descriptor(kind)) + .transpose()? + .as_ref(), + )) + } + + #[inline(always)] + pub fn query_run(&self, cmd: Option<&str>) -> Result { + let inner = self.inner.lock(); + let permission = &inner.run; + if permission.is_allow_all() { + return Ok(PermissionState::Granted); + } + Ok(permission.query( + cmd.map(|request| self.descriptor_parser.parse_run_query(request)) + .transpose()? + .as_ref(), + )) + } + + #[inline(always)] + pub fn query_ffi(&self, path: Option<&str>) -> Result { + let inner = self.inner.lock(); + let permission = &inner.ffi; + if permission.is_allow_all() { + return Ok(PermissionState::Granted); + } + Ok(permission.query( + path.map(|path| { + Result::<_, AnyError>::Ok(self.descriptor_parser.parse_path_query(path)?.into_ffi()) + }) + .transpose()? + .as_ref(), + )) + } + + // revoke + + #[inline(always)] + pub fn revoke_read(&self, path: Option<&str>) -> Result { + Ok(self.inner.lock().read.revoke( + path.map(|path| { + Result::<_, AnyError>::Ok( + self.descriptor_parser.parse_path_query(path)?.into_read(), + ) + }) + .transpose()? + .as_ref(), + )) + } + + #[inline(always)] + pub fn revoke_write(&self, path: Option<&str>) -> Result { + Ok(self.inner.lock().write.revoke( + path.map(|path| { + Result::<_, AnyError>::Ok( + self.descriptor_parser.parse_path_query(path)?.into_write(), + ) + }) + .transpose()? + .as_ref(), + )) + } + + #[inline(always)] + pub fn revoke_net(&self, host: Option<&str>) -> Result { + Ok(self.inner.lock().net.revoke( + match host { + None => None, + Some(h) => Some(self.descriptor_parser.parse_net_descriptor(h)?), + } + .as_ref(), + )) + } + + #[inline(always)] + pub fn revoke_env(&self, var: Option<&str>) -> PermissionState { + self.inner.lock().env.revoke(var) + } + + #[inline(always)] + pub fn revoke_sys(&self, kind: Option<&str>) -> Result { + Ok(self.inner.lock().sys.revoke( + kind.map(|kind| self.descriptor_parser.parse_sys_descriptor(kind)) + .transpose()? + .as_ref(), + )) + } + + #[inline(always)] + pub fn revoke_run(&self, cmd: Option<&str>) -> Result { + Ok(self.inner.lock().run.revoke( + cmd.map(|request| self.descriptor_parser.parse_run_query(request)) + .transpose()? + .as_ref(), + )) + } + + #[inline(always)] + pub fn revoke_ffi(&self, path: Option<&str>) -> Result { + Ok(self.inner.lock().ffi.revoke( + path.map(|path| { + Result::<_, AnyError>::Ok(self.descriptor_parser.parse_path_query(path)?.into_ffi()) + }) + .transpose()? + .as_ref(), + )) + } + + // request + + #[inline(always)] + pub fn request_read(&self, path: Option<&str>) -> Result { + Ok(self.inner.lock().read.request( + path.map(|path| { + Result::<_, AnyError>::Ok( + self.descriptor_parser.parse_path_query(path)?.into_read(), + ) + }) + .transpose()? + .as_ref(), + )) + } + + #[inline(always)] + pub fn request_write(&self, path: Option<&str>) -> Result { + Ok(self.inner.lock().write.request( + path.map(|path| { + Result::<_, AnyError>::Ok( + self.descriptor_parser.parse_path_query(path)?.into_write(), + ) + }) + .transpose()? + .as_ref(), + )) + } + + #[inline(always)] + pub fn request_net(&self, host: Option<&str>) -> Result { + Ok(self.inner.lock().net.request( + match host { + None => None, + Some(h) => Some(self.descriptor_parser.parse_net_descriptor(h)?), + } + .as_ref(), + )) + } + + #[inline(always)] + pub fn request_env(&self, var: Option<&str>) -> PermissionState { + self.inner.lock().env.request(var) + } + + #[inline(always)] + pub fn request_sys(&self, kind: Option<&str>) -> Result { + Ok(self.inner.lock().sys.request( + kind.map(|kind| self.descriptor_parser.parse_sys_descriptor(kind)) + .transpose()? + .as_ref(), + )) + } + + #[inline(always)] + pub fn request_run(&self, cmd: Option<&str>) -> Result { + Ok(self.inner.lock().run.request( + cmd.map(|request| self.descriptor_parser.parse_run_query(request)) + .transpose()? + .as_ref(), + )) } #[inline(always)] - pub fn check_ffi_partial(&self, path: Option<&Path>) -> Result<(), AnyError> { - self.0.lock().ffi.check_partial(path) + pub fn request_ffi(&self, path: Option<&str>) -> Result { + Ok(self.inner.lock().ffi.request( + path.map(|path| { + Result::<_, AnyError>::Ok(self.descriptor_parser.parse_path_query(path)?.into_ffi()) + }) + .transpose()? + .as_ref(), + )) + } +} + +/// Directly deserializable from JS worker and test permission options. +#[derive(Debug, Eq, PartialEq)] +pub struct ChildPermissionsArg { + pub env: ChildUnaryPermissionArg, + pub net: ChildUnaryPermissionArg, + pub ffi: ChildUnaryPermissionArg, + pub import: ChildUnaryPermissionArg, + pub read: ChildUnaryPermissionArg, + pub run: ChildUnaryPermissionArg, + pub sys: ChildUnaryPermissionArg, + pub write: ChildUnaryPermissionArg, +} + +impl ChildPermissionsArg { + pub fn inherit() -> Self { + ChildPermissionsArg { + env: ChildUnaryPermissionArg::Inherit, + net: ChildUnaryPermissionArg::Inherit, + ffi: ChildUnaryPermissionArg::Inherit, + import: ChildUnaryPermissionArg::Inherit, + read: ChildUnaryPermissionArg::Inherit, + run: ChildUnaryPermissionArg::Inherit, + sys: ChildUnaryPermissionArg::Inherit, + write: ChildUnaryPermissionArg::Inherit, + } + } + + pub fn none() -> Self { + ChildPermissionsArg { + env: ChildUnaryPermissionArg::NotGranted, + net: ChildUnaryPermissionArg::NotGranted, + ffi: ChildUnaryPermissionArg::NotGranted, + import: ChildUnaryPermissionArg::NotGranted, + read: ChildUnaryPermissionArg::NotGranted, + run: ChildUnaryPermissionArg::NotGranted, + sys: ChildUnaryPermissionArg::NotGranted, + write: ChildUnaryPermissionArg::NotGranted, + } } } +impl<'de> Deserialize<'de> for ChildPermissionsArg { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct ChildPermissionsArgVisitor; + impl<'de> de::Visitor<'de> for ChildPermissionsArgVisitor { + type Value = ChildPermissionsArg; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("\"inherit\" or \"none\" or object") + } + + fn visit_unit(self) -> Result + where + E: de::Error, + { + Ok(ChildPermissionsArg::inherit()) + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + if v == "inherit" { + Ok(ChildPermissionsArg::inherit()) + } else if v == "none" { + Ok(ChildPermissionsArg::none()) + } else { + Err(de::Error::invalid_value(de::Unexpected::Str(v), &self)) + } + } + + fn visit_map(self, mut v: V) -> Result + where + V: de::MapAccess<'de>, + { + let mut child_permissions_arg = ChildPermissionsArg::none(); + while let Some((key, value)) = v.next_entry::()? { + if key == "env" { + let arg = serde_json::from_value::(value); + child_permissions_arg.env = arg.map_err(|e| { + de::Error::custom(format!("({API}.permissions.env) {e}")) + })?; + } else if key == "net" { + let arg = serde_json::from_value::(value); + child_permissions_arg.net = arg.map_err(|e| { + de::Error::custom(format!("({API}.permissions.net) {e}")) + })?; + } else if key == "ffi" { + let arg = serde_json::from_value::(value); + child_permissions_arg.ffi = arg.map_err(|e| { + de::Error::custom(format!("({API}.permissions.ffi) {e}")) + })?; + } else if key == "import" { + let arg = serde_json::from_value::(value); + child_permissions_arg.import = arg.map_err(|e| { + de::Error::custom(format!("({API}.permissions.import) {e}")) + })?; + } else if key == "read" { + let arg = serde_json::from_value::(value); + child_permissions_arg.read = arg.map_err(|e| { + de::Error::custom(format!("({API}.permissions.read) {e}")) + })?; + } else if key == "run" { + let arg = serde_json::from_value::(value); + child_permissions_arg.run = arg.map_err(|e| { + de::Error::custom(format!("({API}.permissions.run) {e}")) + })?; + } else if key == "sys" { + let arg = serde_json::from_value::(value); + child_permissions_arg.sys = arg.map_err(|e| { + de::Error::custom(format!("({API}.permissions.sys) {e}")) + })?; + } else if key == "write" { + let arg = serde_json::from_value::(value); + child_permissions_arg.write = arg.map_err(|e| { + de::Error::custom(format!("({API}.permissions.write) {e}")) + })?; + } else { + return Err(de::Error::custom("unknown permission name")); + } + } + Ok(child_permissions_arg) + } + } + deserializer.deserialize_any(ChildPermissionsArgVisitor) + } +} diff --git a/crates/bls-permissions/src/path_utils.rs b/crates/bls-permissions/src/path_utils.rs new file mode 100644 index 0000000..1084f93 --- /dev/null +++ b/crates/bls-permissions/src/path_utils.rs @@ -0,0 +1,506 @@ +#![allow(dead_code)] +#![deny(clippy::print_stderr)] +#![deny(clippy::print_stdout)] +#![deny(clippy::unused_async)] + +use std::io::ErrorKind; +use std::path::Component; +use std::path::Path; +use std::path::PathBuf; +use thiserror::Error; +use url::Url; + +/// Gets the parent of this url. +pub fn url_parent(url: &Url) -> Url { + let mut url = url.clone(); + // don't use url.segments() because it will strip the leading slash + let mut segments = url.path().split('/').collect::>(); + if segments.iter().all(|s| s.is_empty()) { + return url; + } + if let Some(last) = segments.last() { + if last.is_empty() { + segments.pop(); + } + segments.pop(); + let new_path = format!("{}/", segments.join("/")); + url.set_path(&new_path); + } + url +} + +#[derive(Debug, Error)] +#[error("Could not convert URL to file path.\n URL: {0}")] +pub struct UrlToFilePathError(pub Url); + +/// Attempts to convert a url to a file path. By default, uses the Url +/// crate's `to_file_path()` method, but falls back to try and resolve unix-style +/// paths on Windows. +pub fn url_to_file_path(url: &Url) -> Result { + let result = if url.scheme() != "file" { + Err(()) + } else { + url_to_file_path_inner(url) + }; + match result { + Ok(path) => Ok(path), + Err(()) => Err(UrlToFilePathError(url.clone())), + } +} + +fn url_to_file_path_inner(url: &Url) -> Result { + #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] + return url_to_file_path_real(url); + #[cfg(not(any(unix, windows, target_os = "redox", target_os = "wasi")))] + url_to_file_path_wasm(url) +} + +#[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] +fn url_to_file_path_real(url: &Url) -> Result { + if cfg!(windows) { + match url.to_file_path() { + Ok(path) => Ok(path), + Err(()) => { + // This might be a unix-style path which is used in the tests even on Windows. + // Attempt to see if we can convert it to a `PathBuf`. This code should be removed + // once/if https://github.com/servo/rust-url/issues/730 is implemented. + if url.scheme() == "file" + && url.host().is_none() + && url.port().is_none() + && url.path_segments().is_some() + { + let path_str = url.path(); + match String::from_utf8( + percent_encoding::percent_decode(path_str.as_bytes()).collect(), + ) { + Ok(path_str) => Ok(PathBuf::from(path_str)), + Err(_) => Err(()), + } + } else { + Err(()) + } + } + } + } else { + url.to_file_path() + } +} + +#[cfg(any(test, not(any(unix, windows, target_os = "redox", target_os = "wasi"))))] +fn url_to_file_path_wasm(url: &Url) -> Result { + fn is_windows_path_segment(url: &str) -> bool { + let mut chars = url.chars(); + + let first_char = chars.next(); + if first_char.is_none() || !first_char.unwrap().is_ascii_alphabetic() { + return false; + } + + if chars.next() != Some(':') { + return false; + } + + chars.next().is_none() + } + + let path_segments = url.path_segments().unwrap().collect::>(); + let mut final_text = String::new(); + let mut is_windows_share = false; + if let Some(host) = url.host_str() { + final_text.push_str("\\\\"); + final_text.push_str(host); + is_windows_share = true; + } + for segment in path_segments.iter() { + if is_windows_share { + final_text.push('\\'); + } else if !final_text.is_empty() { + final_text.push('/'); + } + final_text.push_str(&percent_encoding::percent_decode_str(segment).decode_utf8_lossy()); + } + if !is_windows_share && !is_windows_path_segment(path_segments[0]) { + final_text = format!("/{}", final_text); + } + Ok(PathBuf::from(final_text)) +} + +/// Normalize all intermediate components of the path (ie. remove "./" and "../" components). +/// Similar to `fs::canonicalize()` but doesn't resolve symlinks. +/// +/// Taken from Cargo +/// +#[inline] +pub fn normalize_path>(path: P) -> PathBuf { + fn inner(path: &Path) -> PathBuf { + let mut components = path.components().peekable(); + let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { + components.next(); + PathBuf::from(c.as_os_str()) + } else { + PathBuf::new() + }; + + for component in components { + match component { + Component::Prefix(..) => unreachable!(), + Component::RootDir => { + ret.push(component.as_os_str()); + } + Component::CurDir => {} + Component::ParentDir => { + ret.pop(); + } + Component::Normal(c) => { + ret.push(c); + } + } + } + ret + } + + inner(path.as_ref()) +} + +#[derive(Debug, Error)] +#[error("Could not convert path to URL.\n Path: {0}")] +pub struct PathToUrlError(pub PathBuf); + +#[allow(clippy::result_unit_err)] +pub fn url_from_file_path(path: &Path) -> Result { + #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] + return Url::from_file_path(path).map_err(|()| PathToUrlError(path.to_path_buf())); + #[cfg(not(any(unix, windows, target_os = "redox", target_os = "wasi")))] + url_from_file_path_wasm(path).map_err(|()| PathToUrlError(path.to_path_buf())) +} + +#[allow(clippy::result_unit_err)] +pub fn url_from_directory_path(path: &Path) -> Result { + #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] + return Url::from_directory_path(path).map_err(|()| PathToUrlError(path.to_path_buf())); + #[cfg(not(any(unix, windows, target_os = "redox", target_os = "wasi")))] + url_from_directory_path_wasm(path).map_err(|()| PathToUrlError(path.to_path_buf())) +} + +#[cfg(any(test, not(any(unix, windows, target_os = "redox", target_os = "wasi"))))] +fn url_from_directory_path_wasm(path: &Path) -> Result { + let mut url = url_from_file_path_wasm(path)?; + url.path_segments_mut().unwrap().push(""); + Ok(url) +} + +#[cfg(any(test, not(any(unix, windows, target_os = "redox", target_os = "wasi"))))] +fn url_from_file_path_wasm(path: &Path) -> Result { + use std::path::Component; + + let original_path = path.to_string_lossy(); + let mut path_str = original_path; + // assume paths containing backslashes are windows paths + if path_str.contains('\\') { + let mut url = Url::parse("file://").unwrap(); + if let Some(next) = path_str.strip_prefix(r#"\\?\UNC\"#) { + if let Some((host, rest)) = next.split_once('\\') { + if url.set_host(Some(host)).is_ok() { + path_str = rest.to_string().into(); + } + } + } else if let Some(next) = path_str.strip_prefix(r#"\\?\"#) { + path_str = next.to_string().into(); + } else if let Some(next) = path_str.strip_prefix(r#"\\"#) { + if let Some((host, rest)) = next.split_once('\\') { + if url.set_host(Some(host)).is_ok() { + path_str = rest.to_string().into(); + } + } + } + + for component in path_str.split('\\') { + url.path_segments_mut().unwrap().push(component); + } + + Ok(url) + } else { + let mut url = Url::parse("file://").unwrap(); + for component in path.components() { + match component { + Component::RootDir => { + url.path_segments_mut().unwrap().push(""); + } + Component::Normal(segment) => { + url.path_segments_mut() + .unwrap() + .push(&segment.to_string_lossy()); + } + Component::Prefix(_) | Component::CurDir | Component::ParentDir => { + return Err(()); + } + } + } + + Ok(url) + } +} + +#[cfg(not(windows))] +#[inline] +pub fn strip_unc_prefix(path: PathBuf) -> PathBuf { + path +} + +/// Strips the unc prefix (ex. \\?\) from Windows paths. +#[cfg(windows)] +pub fn strip_unc_prefix(path: PathBuf) -> PathBuf { + use std::path::Component; + use std::path::Prefix; + + let mut components = path.components(); + match components.next() { + Some(Component::Prefix(prefix)) => { + match prefix.kind() { + // \\?\device + Prefix::Verbatim(device) => { + let mut path = PathBuf::new(); + path.push(format!(r"\\{}\", device.to_string_lossy())); + path.extend(components.filter(|c| !matches!(c, Component::RootDir))); + path + } + // \\?\c:\path + Prefix::VerbatimDisk(_) => { + let mut path = PathBuf::new(); + path.push(prefix.as_os_str().to_string_lossy().replace(r"\\?\", "")); + path.extend(components); + path + } + // \\?\UNC\hostname\share_name\path + Prefix::VerbatimUNC(hostname, share_name) => { + let mut path = PathBuf::new(); + path.push(format!( + r"\\{}\{}\", + hostname.to_string_lossy(), + share_name.to_string_lossy() + )); + path.extend(components.filter(|c| !matches!(c, Component::RootDir))); + path + } + _ => path, + } + } + _ => path, + } +} + +/// Canonicalizes a path which might be non-existent by going up the +/// ancestors until it finds a directory that exists, canonicalizes +/// that path, then adds back the remaining path components. +/// +/// Note: When using this, you should be aware that a symlink may +/// subsequently be created along this path by some other code. +pub fn canonicalize_path_maybe_not_exists( + path: &Path, + canonicalize: &impl Fn(&Path) -> std::io::Result, +) -> std::io::Result { + let path = normalize_path(path); + let mut path = path.as_path(); + let mut names_stack = Vec::new(); + loop { + match canonicalize(path) { + Ok(mut canonicalized_path) => { + for name in names_stack.into_iter().rev() { + canonicalized_path = canonicalized_path.join(name); + } + return Ok(canonicalized_path); + } + Err(err) if err.kind() == ErrorKind::NotFound => { + names_stack.push(match path.file_name() { + Some(name) => name.to_owned(), + None => return Err(err), + }); + path = match path.parent() { + Some(parent) => parent, + None => return Err(err), + }; + } + Err(err) => return Err(err), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_url_parent() { + run_test("file:///", "file:///"); + run_test("file:///test", "file:///"); + run_test("file:///test/", "file:///"); + run_test("file:///test/other", "file:///test/"); + run_test("file:///test/other.txt", "file:///test/"); + run_test("file:///test/other/", "file:///test/"); + + fn run_test(url: &str, expected: &str) { + let result = url_parent(&Url::parse(url).unwrap()); + assert_eq!(result.to_string(), expected); + } + } + + #[test] + fn test_url_to_file_path() { + run_success_test("file:///", "/"); + run_success_test("file:///test", "/test"); + run_success_test("file:///dir/test/test.txt", "/dir/test/test.txt"); + run_success_test( + "file:///dir/test%20test/test.txt", + "/dir/test test/test.txt", + ); + + assert_no_panic_url_to_file_path("file:/"); + assert_no_panic_url_to_file_path("file://"); + assert_no_panic_url_to_file_path("file://asdf/"); + assert_no_panic_url_to_file_path("file://asdf/66666/a.ts"); + + fn run_success_test(url: &str, expected_path: &str) { + let result = url_to_file_path(&Url::parse(url).unwrap()).unwrap(); + assert_eq!(result, PathBuf::from(expected_path)); + } + + fn assert_no_panic_url_to_file_path(url: &str) { + let _result = url_to_file_path(&Url::parse(url).unwrap()); + } + } + + #[test] + fn test_url_to_file_path_wasm() { + #[track_caller] + fn convert(path: &str) -> String { + url_to_file_path_wasm(&Url::parse(path).unwrap()) + .unwrap() + .to_string_lossy() + .into_owned() + } + + assert_eq!(convert("file:///a/b/c.json"), "/a/b/c.json"); + assert_eq!(convert("file:///D:/test/other.json"), "D:/test/other.json"); + assert_eq!( + convert("file:///path%20with%20spaces/and%23special%25chars!.json"), + "/path with spaces/and#special%chars!.json", + ); + assert_eq!( + convert("file:///C:/My%20Documents/file.txt"), + "C:/My Documents/file.txt" + ); + assert_eq!( + convert("file:///a/b/%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80.txt"), + "/a/b/пример.txt" + ); + assert_eq!( + convert("file://server/share/folder/file.txt"), + "\\\\server\\share\\folder\\file.txt" + ); + } + + #[test] + fn test_url_from_file_path_wasm() { + #[track_caller] + fn convert(path: &str) -> String { + url_from_file_path_wasm(Path::new(path)) + .unwrap() + .to_string() + } + + assert_eq!(convert("/a/b/c.json"), "file:///a/b/c.json"); + assert_eq!( + convert("D:\\test\\other.json"), + "file:///D:/test/other.json" + ); + assert_eq!( + convert("/path with spaces/and#special%chars!.json"), + "file:///path%20with%20spaces/and%23special%25chars!.json" + ); + assert_eq!( + convert("C:\\My Documents\\file.txt"), + "file:///C:/My%20Documents/file.txt" + ); + assert_eq!( + convert("/a/b/пример.txt"), + "file:///a/b/%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80.txt" + ); + assert_eq!( + convert("\\\\server\\share\\folder\\file.txt"), + "file://server/share/folder/file.txt" + ); + assert_eq!(convert(r#"\\?\UNC\server\share"#), "file://server/share"); + assert_eq!( + convert(r"\\?\cat_pics\subfolder\file.jpg"), + "file:///cat_pics/subfolder/file.jpg" + ); + assert_eq!(convert(r"\\?\cat_pics"), "file:///cat_pics"); + } + + #[test] + fn test_url_from_directory_path_wasm() { + #[track_caller] + fn convert(path: &str) -> String { + url_from_directory_path_wasm(Path::new(path)) + .unwrap() + .to_string() + } + + assert_eq!(convert("/a/b/c"), "file:///a/b/c/"); + assert_eq!(convert("D:\\test\\other"), "file:///D:/test/other/"); + } + + #[cfg(windows)] + #[test] + fn test_strip_unc_prefix() { + use std::path::PathBuf; + + run_test(r"C:\", r"C:\"); + run_test(r"C:\test\file.txt", r"C:\test\file.txt"); + + run_test(r"\\?\C:\", r"C:\"); + run_test(r"\\?\C:\test\file.txt", r"C:\test\file.txt"); + + run_test(r"\\.\C:\", r"\\.\C:\"); + run_test(r"\\.\C:\Test\file.txt", r"\\.\C:\Test\file.txt"); + + run_test(r"\\?\UNC\localhost\", r"\\localhost"); + run_test(r"\\?\UNC\localhost\c$\", r"\\localhost\c$"); + run_test( + r"\\?\UNC\localhost\c$\Windows\file.txt", + r"\\localhost\c$\Windows\file.txt", + ); + run_test(r"\\?\UNC\wsl$\deno.json", r"\\wsl$\deno.json"); + + run_test(r"\\?\server1", r"\\server1"); + run_test(r"\\?\server1\e$\", r"\\server1\e$\"); + run_test( + r"\\?\server1\e$\test\file.txt", + r"\\server1\e$\test\file.txt", + ); + + fn run_test(input: &str, expected: &str) { + assert_eq!( + super::strip_unc_prefix(PathBuf::from(input)), + PathBuf::from(expected) + ); + } + } + + #[cfg(windows)] + #[test] + fn test_normalize_path() { + use super::*; + + run_test("C:\\test\\./file.txt", "C:\\test\\file.txt"); + run_test("C:\\test\\../other/file.txt", "C:\\other\\file.txt"); + run_test("C:\\test\\../other\\file.txt", "C:\\other\\file.txt"); + + fn run_test(input: &str, expected: &str) { + assert_eq!( + normalize_path(PathBuf::from(input)), + PathBuf::from(expected) + ); + } + } +} diff --git a/crates/bls-permissions/src/prompter.rs b/crates/bls-permissions/src/prompter.rs index 87ccd3d..55887d2 100644 --- a/crates/bls-permissions/src/prompter.rs +++ b/crates/bls-permissions/src/prompter.rs @@ -6,7 +6,7 @@ pub enum PromptResponse { Allow, Deny, AllowAll, - #[cfg(target_arch = "wasm32")] + #[cfg(target_family = "wasm")] Yield, } diff --git a/crates/bls-permissions/src/terminal/colors.rs b/crates/bls-permissions/src/terminal/colors.rs new file mode 100644 index 0000000..a1fc728 --- /dev/null +++ b/crates/bls-permissions/src/terminal/colors.rs @@ -0,0 +1,332 @@ +#![allow(dead_code)] + +use once_cell::sync::Lazy; +use std::fmt; +use std::fmt::Write as _; +use std::sync::atomic::AtomicBool; +use termcolor::Ansi; +use termcolor::Color::Ansi256; +use termcolor::Color::Black; +use termcolor::Color::Blue; +use termcolor::Color::Cyan; +use termcolor::Color::Green; +use termcolor::Color::Magenta; +use termcolor::Color::Red; +use termcolor::Color::White; +use termcolor::Color::Yellow; +use termcolor::ColorSpec; +use termcolor::WriteColor; + +#[cfg(windows)] +use termcolor::BufferWriter; +#[cfg(windows)] +use termcolor::ColorChoice; + +static USE_COLOR: Lazy = Lazy::new(|| { + #[cfg(target_family = "wasm")] + { + // Don't use color by default on Wasm targets because + // it's not always possible to read the `NO_COLOR` env var. + // + // Instead the user can opt-in via `set_use_color`. + AtomicBool::new(false) + } + #[cfg(not(target_family = "wasm"))] + { + let no_color = std::env::var_os("NO_COLOR") + .map(|v| !v.is_empty()) + .unwrap_or(false); + AtomicBool::new(!no_color) + } +}); + +#[derive(Clone, Copy, Debug)] +pub enum ColorLevel { + None, + Ansi, + Ansi256, + TrueColor, +} + +static COLOR_LEVEL: Lazy = Lazy::new(|| { + #[cfg(target_family = "wasm")] + { + // Don't use color by default on Wasm targets because + // it's not always possible to read env vars. + ColorLevel::None + } + #[cfg(not(target_family = "wasm"))] + { + let no_color = std::env::var_os("NO_COLOR") + .map(|v| !v.is_empty()) + .unwrap_or(false); + + if no_color { + return ColorLevel::None; + } + + let mut term = "dumb".to_string(); + if let Some(term_value) = std::env::var_os("TERM") { + if term_value == "dumb" { + return ColorLevel::None; + } + + if let Some(term_value) = term_value.to_str() { + term = term_value.to_string(); + } + } + + #[cfg(windows)] + { + // TODO: Older versions of windows only support ansi colors, + // starting with Windows 10 build 10586 ansi256 is supported + + // TrueColor support landed with Windows 10 build 14931, + // see https://devblogs.microsoft.com/commandline/24-bit-color-in-the-windows-console/ + return ColorLevel::TrueColor; + } + + if std::env::var_os("TMUX").is_some() { + return ColorLevel::Ansi; + } + + if let Some(ci) = std::env::var_os("CI") { + if let Some(ci_value) = ci.to_str() { + if matches!( + ci_value, + "TRAVIS" + | "CIRCLECI" + | "APPVEYOR" + | "GITLAB_CI" + | "GITHUB_ACTIONS" + | "BUILDKITE" + | "DRONE" + ) { + return ColorLevel::Ansi256; + } + } else { + return ColorLevel::Ansi; + } + } + + if let Some(color_term) = std::env::var_os("COLORTERM") { + if color_term == "truecolor" || color_term == "24bit" { + return ColorLevel::TrueColor; + } + } + + if term.ends_with("-256color") || term.ends_with("256") { + return ColorLevel::Ansi256; + } + + ColorLevel::Ansi + } +}); + +pub fn get_color_level() -> ColorLevel { + *COLOR_LEVEL +} + +/// Gets whether color should be used in the output. +/// +/// This is informed via the `USE_COLOR` environment variable +/// or if `set_use_color` has been set to true. +/// +/// On Wasm targets, use `set_use_color(true)` to enable color output. +pub fn use_color() -> bool { + USE_COLOR.load(std::sync::atomic::Ordering::Relaxed) +} + +/// Sets whether color should be used in the output. +/// +/// This overrides the default value set via the `NO_COLOR` env var. +pub fn set_use_color(use_color: bool) { + USE_COLOR.store(use_color, std::sync::atomic::Ordering::Relaxed); +} + +/// Enables ANSI color output on Windows. This is a no-op on other platforms. +pub fn enable_ansi() { + #[cfg(windows)] + { + BufferWriter::stdout(ColorChoice::AlwaysAnsi); + } +} + +/// A struct that can adapt a `fmt::Write` to a `std::io::Write`. If anything +/// that can not be represented as UTF-8 is written to this writer, it will +/// return an error. +struct StdFmtStdIoWriter<'a>(&'a mut dyn fmt::Write); + +impl std::io::Write for StdFmtStdIoWriter<'_> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let str = std::str::from_utf8(buf).map_err(|_| { + std::io::Error::new(std::io::ErrorKind::Other, "failed to convert bytes to str") + })?; + match self.0.write_str(str) { + Ok(_) => Ok(buf.len()), + Err(_) => Err(std::io::Error::new( + std::io::ErrorKind::Other, + "failed to write to fmt::Write", + )), + } + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +/// A struct that can adapt a `std::io::Write` to a `fmt::Write`. +struct StdIoStdFmtWriter<'a>(&'a mut dyn std::io::Write); + +impl fmt::Write for StdIoStdFmtWriter<'_> { + fn write_str(&mut self, s: &str) -> fmt::Result { + self.0.write_all(s.as_bytes()).map_err(|_| fmt::Error)?; + Ok(()) + } +} + +pub struct Style { + colorspec: ColorSpec, + inner: I, +} + +impl fmt::Display for Style { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !use_color() { + return fmt::Display::fmt(&self.inner, f); + } + let mut ansi_writer = Ansi::new(StdFmtStdIoWriter(f)); + ansi_writer + .set_color(&self.colorspec) + .map_err(|_| fmt::Error)?; + write!(StdIoStdFmtWriter(&mut ansi_writer), "{}", self.inner)?; + ansi_writer.reset().map_err(|_| fmt::Error)?; + Ok(()) + } +} + +#[inline] +fn style<'a, S: fmt::Display + 'a>(s: S, colorspec: ColorSpec) -> Style { + Style { + colorspec, + inner: s, + } +} + +pub fn red_bold<'a, S: fmt::Display + 'a>(s: S) -> Style { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Red)).set_bold(true); + style(s, style_spec) +} + +pub fn green_bold<'a, S: fmt::Display + 'a>(s: S) -> Style { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Green)).set_bold(true); + style(s, style_spec) +} + +pub fn yellow_bold<'a, S: fmt::Display + 'a>(s: S) -> Style { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Yellow)).set_bold(true); + style(s, style_spec) +} + +pub fn italic<'a, S: fmt::Display + 'a>(s: S) -> Style { + let mut style_spec = ColorSpec::new(); + style_spec.set_italic(true); + style(s, style_spec) +} + +pub fn italic_gray<'a, S: fmt::Display + 'a>(s: S) -> Style { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Ansi256(8))).set_italic(true); + style(s, style_spec) +} + +pub fn italic_bold<'a, S: fmt::Display + 'a>(s: S) -> Style { + let mut style_spec = ColorSpec::new(); + style_spec.set_bold(true).set_italic(true); + style(s, style_spec) +} + +pub fn white_on_red<'a, S: fmt::Display + 'a>(s: S) -> Style { + let mut style_spec = ColorSpec::new(); + style_spec.set_bg(Some(Red)).set_fg(Some(White)); + style(s, style_spec) +} + +pub fn black_on_green<'a, S: fmt::Display + 'a>(s: S) -> Style { + let mut style_spec = ColorSpec::new(); + style_spec.set_bg(Some(Green)).set_fg(Some(Black)); + style(s, style_spec) +} + +pub fn yellow<'a, S: fmt::Display + 'a>(s: S) -> Style { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Yellow)); + style(s, style_spec) +} + +pub fn cyan<'a, S: fmt::Display + 'a>(s: S) -> Style { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Cyan)); + style(s, style_spec) +} + +pub fn cyan_with_underline<'a>(s: impl fmt::Display + 'a) -> impl fmt::Display + 'a { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Cyan)).set_underline(true); + style(s, style_spec) +} + +pub fn cyan_bold<'a, S: fmt::Display + 'a>(s: S) -> Style { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Cyan)).set_bold(true); + style(s, style_spec) +} + +pub fn magenta<'a, S: fmt::Display + 'a>(s: S) -> Style { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Magenta)); + style(s, style_spec) +} + +pub fn red<'a, S: fmt::Display + 'a>(s: S) -> Style { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Red)); + style(s, style_spec) +} + +pub fn green<'a, S: fmt::Display + 'a>(s: S) -> Style { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Green)); + style(s, style_spec) +} + +pub fn bold<'a, S: fmt::Display + 'a>(s: S) -> Style { + let mut style_spec = ColorSpec::new(); + style_spec.set_bold(true); + style(s, style_spec) +} + +pub fn gray<'a, S: fmt::Display + 'a>(s: S) -> Style { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Ansi256(245))); + style(s, style_spec) +} + +pub fn intense_blue<'a, S: fmt::Display + 'a>(s: S) -> Style { + let mut style_spec = ColorSpec::new(); + style_spec.set_fg(Some(Blue)).set_intense(true); + style(s, style_spec) +} + +pub fn white_bold_on_red<'a>(s: impl fmt::Display + 'a) -> impl fmt::Display + 'a { + let mut style_spec = ColorSpec::new(); + style_spec + .set_bold(true) + .set_bg(Some(Red)) + .set_fg(Some(White)); + style(s, style_spec) +} diff --git a/crates/bls-permissions/src/terminal/mod.rs b/crates/bls-permissions/src/terminal/mod.rs new file mode 100644 index 0000000..c788cae --- /dev/null +++ b/crates/bls-permissions/src/terminal/mod.rs @@ -0,0 +1,18 @@ +#![allow(dead_code)] + +use std::io::IsTerminal; + +use once_cell::sync::Lazy; + +pub mod colors; + +static IS_STDOUT_TTY: Lazy = Lazy::new(|| std::io::stdout().is_terminal()); +static IS_STDERR_TTY: Lazy = Lazy::new(|| std::io::stderr().is_terminal()); + +pub fn is_stdout_tty() -> bool { + *IS_STDOUT_TTY +} + +pub fn is_stderr_tty() -> bool { + *IS_STDERR_TTY +} diff --git a/crates/browser/index.js b/crates/browser/index.js index 5bab683..225b107 100644 --- a/crates/browser/index.js +++ b/crates/browser/index.js @@ -46,7 +46,7 @@ class MyDialog { style.innerHTML = ` dialog.promptDlg { font-size:15px; - color:red; + color:green; } dialog.promptDlg .buttons { margin-top: 5px; @@ -107,4 +107,11 @@ blsCheckRead("/test.o", "permission").then((rs) =>{ console.log(rs); let {code, msg} = rs; console.log(`code:${code} msg:${msg}` ); -}); \ No newline at end of file + + blsCheckRead("test.o", "permission").then((rs) =>{ + console.log(rs); + let {code, msg} = rs; + console.log(`code:${code} msg:${msg}` ); + }); +}); + diff --git a/crates/browser/module.js b/crates/browser/module.js index 9c8d61b..12ba067 100644 --- a/crates/browser/module.js +++ b/crates/browser/module.js @@ -43,6 +43,7 @@ class DefaultDialog { style.innerHTML = ` dialog.promptDlg { font-size:15px; + color:red; } dialog.promptDlg .buttons button { margin-left: 3px; diff --git a/crates/browser/package.json b/crates/browser/package.json index b84a802..c63dc8d 100644 --- a/crates/browser/package.json +++ b/crates/browser/package.json @@ -4,7 +4,7 @@ "serve": "webpack serve" }, "devDependencies": { - "@wasm-tool/wasm-pack-plugin": "1.5.0", + "@wasm-tool/wasm-pack-plugin": "1.6.0", "html-webpack-plugin": "^5.6.0", "typescript": "^5.6.3", "webpack": "^5.93.0", diff --git a/crates/browser/src/html.rs b/crates/browser/src/html.rs index b00ffc8..799d4fc 100644 --- a/crates/browser/src/html.rs +++ b/crates/browser/src/html.rs @@ -17,5 +17,4 @@ impl Html { pub fn italic(msg: &str) -> String { format!("{msg}") } - -} \ No newline at end of file +} diff --git a/crates/browser/src/lib.rs b/crates/browser/src/lib.rs index 4de4515..2457a54 100644 --- a/crates/browser/src/lib.rs +++ b/crates/browser/src/lib.rs @@ -1,15 +1,33 @@ -use std::path::Path; -use std::path::PathBuf; -use bls_permissions::AnyError; use bls_permissions::is_yield_error_class; +use bls_permissions::AllowRunDescriptor; +use bls_permissions::AllowRunDescriptorParseResult; +use bls_permissions::AnyError; use bls_permissions::BlsPermissionsContainer; +use bls_permissions::CheckSpecifierKind; +use bls_permissions::ChildPermissionsArg; +use bls_permissions::DenyRunDescriptor; +use bls_permissions::EnvDescriptor; +use bls_permissions::FfiDescriptor; +use bls_permissions::ImportDescriptor; use bls_permissions::ModuleSpecifier; +use bls_permissions::NetDescriptor; +use bls_permissions::PathQueryDescriptor; +use bls_permissions::PermissionDescriptorParser; +use bls_permissions::PermissionState; use bls_permissions::Permissions; +use bls_permissions::ReadDescriptor; +use bls_permissions::RunQueryDescriptor; +use bls_permissions::SysDescriptor; use bls_permissions::Url; +use bls_permissions::WriteDescriptor; +pub use macros::*; use once_cell::sync::Lazy; use prompter::init_browser_prompter; +use std::borrow::Cow; +use std::path::Path; +use std::path::PathBuf; +use std::sync::Arc; use wasm_bindgen::prelude::wasm_bindgen; -pub use macros::*; #[macro_use] mod macros; @@ -17,45 +35,62 @@ mod html; mod prompter; #[derive(Clone, Debug)] -pub struct PermissionsContainer(BlsPermissionsContainer); +pub struct PermissionsContainer(pub bls_permissions::BlsPermissionsContainer); impl PermissionsContainer { - pub fn new(perms: Permissions) -> Self { + pub fn new(descriptor_parser: Arc, perms: Permissions) -> Self { init_browser_prompter(); - Self(BlsPermissionsContainer::new(perms)) + Self(BlsPermissionsContainer::new(descriptor_parser, perms)) } - #[inline(always)] - pub fn allow_hrtime(&self) -> bool { - self.0.allow_hrtime() + pub fn create_child_permissions( + &self, + child_permissions_arg: ChildPermissionsArg, + ) -> Result { + Ok(PermissionsContainer(self.0.create_child_permissions(child_permissions_arg)?)) } - 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)] - pub fn check_specifier(&self, specifier: &ModuleSpecifier) -> Result<(), AnyError> { - self.0.check_specifier(specifier) + pub fn check_specifier( + &self, + specifier: &ModuleSpecifier, + kind: CheckSpecifierKind, + ) -> Result<(), AnyError> { + self.0.check_specifier(specifier, kind) } #[inline(always)] - pub fn check_read(&self, path: &Path, api_name: &str) -> Result<(), AnyError> { + pub fn check_read(&self, path: &str, api_name: &str) -> Result { self.0.check_read(path, api_name) } #[inline(always)] pub fn check_read_with_api_name( &self, - path: &Path, + path: &str, api_name: Option<&str>, - ) -> Result<(), AnyError> { + ) -> Result { self.0.check_read_with_api_name(path, api_name) } #[inline(always)] - pub fn check_read_blind( + pub fn check_read_path<'a>( &self, + path: &'a Path, + api_name: Option<&str>, + ) -> Result, AnyError> { + self.0.check_read_path(path, api_name) + } + + /// 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, path: &Path, display: &str, api_name: &str, @@ -69,24 +104,40 @@ impl PermissionsContainer { } #[inline(always)] - pub fn check_write(&self, path: &Path, api_name: &str) -> Result<(), AnyError> { + pub fn query_read_all(&self) -> bool { + self.0.query_read_all() + } + + #[inline(always)] + pub fn check_write(&self, path: &str, api_name: &str) -> Result { self.0.check_write(path, api_name) } #[inline(always)] pub fn check_write_with_api_name( &self, - path: &Path, + path: &str, api_name: Option<&str>, - ) -> Result<(), AnyError> { + ) -> Result { self.0.check_write_with_api_name(path, api_name) } + #[inline(always)] + pub fn check_write_path<'a>( + &self, + path: &'a Path, + api_name: &str, + ) -> Result, AnyError> { + self.0.check_write_path(path, api_name) + } + #[inline(always)] pub fn check_write_all(&self, api_name: &str) -> Result<(), AnyError> { self.0.check_write_all(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( &self, @@ -98,20 +149,25 @@ impl PermissionsContainer { } #[inline(always)] - pub fn check_write_partial(&self, path: &Path, api_name: &str) -> Result<(), AnyError> { + pub fn check_write_partial(&mut self, path: &str, api_name: &str) -> Result { self.0.check_write_partial(path, api_name) } #[inline(always)] - pub fn check_run(&self, cmd: &str, api_name: &str) -> Result<(), AnyError> { + pub fn check_run(&mut self, cmd: &RunQueryDescriptor, api_name: &str) -> Result<(), AnyError> { self.0.check_run(cmd, api_name) } #[inline(always)] - pub fn check_run_all(&self, api_name: &str) -> Result<(), AnyError> { + pub fn check_run_all(&mut self, api_name: &str) -> Result<(), AnyError> { self.0.check_run_all(api_name) } + #[inline(always)] + pub fn query_run_all(&mut self, api_name: &str) -> bool { + self.0.query_run_all(api_name) + } + #[inline(always)] pub fn check_sys(&self, kind: &str, api_name: &str) -> Result<(), AnyError> { self.0.check_sys(kind, api_name) @@ -123,28 +179,30 @@ impl PermissionsContainer { } #[inline(always)] - pub fn check_env_all(&self) -> Result<(), AnyError> { + pub fn check_env_all(&mut self) -> Result<(), AnyError> { self.0.check_env_all() } #[inline(always)] - pub fn check_sys_all(&self) -> Result<(), AnyError> { + pub fn check_sys_all(&mut self) -> Result<(), AnyError> { self.0.check_sys_all() } #[inline(always)] - pub fn check_ffi_all(&self) -> Result<(), AnyError> { + pub fn check_ffi_all(&mut self) -> Result<(), AnyError> { self.0.check_ffi_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(&self) -> Result<(), AnyError> { + pub fn check_was_allow_all_flag_passed(&mut self) -> Result<(), AnyError> { self.0.check_was_allow_all_flag_passed() } - pub fn check_special_file(&self, path: &Path, api_name: &str) -> Result<(), &'static str> { + /// Checks special file access, returning the failed permission type if + /// not successful. + pub fn check_special_file(&mut self, path: &Path, api_name: &str) -> Result<(), &'static str> { self.0.check_special_file(path, api_name) } @@ -163,24 +221,210 @@ impl PermissionsContainer { } #[inline(always)] - pub fn check_ffi(&self, path: Option<&Path>) -> Result<(), AnyError> { + pub fn check_ffi(&mut self, path: &str) -> Result { self.0.check_ffi(path) } #[inline(always)] - pub fn check_ffi_partial(&self, path: Option<&Path>) -> Result<(), AnyError> { - self.0.check_ffi_partial(path) + pub fn check_ffi_partial_no_path(&mut self) -> Result<(), AnyError> { + self.0.check_ffi_partial_no_path() + } + + #[inline(always)] + pub fn check_ffi_partial_with_path(&mut self, path: &str) -> Result { + self.0.check_ffi_partial_with_path(path) + } + + // query + + #[inline(always)] + pub fn query_read(&self, path: Option<&str>) -> Result { + self.0.query_read(path) + } + + #[inline(always)] + pub fn query_write(&self, path: Option<&str>) -> Result { + self.0.query_write(path) + } + + #[inline(always)] + pub fn query_net(&self, host: Option<&str>) -> Result { + self.0.query_net(host) + } + + #[inline(always)] + pub fn query_env(&self, var: Option<&str>) -> PermissionState { + self.0.query_env(var) + } + + #[inline(always)] + pub fn query_sys(&self, kind: Option<&str>) -> Result { + self.0.query_sys(kind) + } + + #[inline(always)] + pub fn query_run(&self, cmd: Option<&str>) -> Result { + self.0.query_run(cmd) + } + + #[inline(always)] + pub fn query_ffi(&self, path: Option<&str>) -> Result { + self.0.query_ffi(path) + } + + // revoke + + #[inline(always)] + pub fn revoke_read(&self, path: Option<&str>) -> Result { + self.0.revoke_read(path) + } + + #[inline(always)] + pub fn revoke_write(&self, path: Option<&str>) -> Result { + self.0.revoke_write(path) + } + + #[inline(always)] + pub fn revoke_net(&self, host: Option<&str>) -> Result { + self.0.revoke_net(host) + } + + #[inline(always)] + pub fn revoke_env(&self, var: Option<&str>) -> PermissionState { + self.0.revoke_env(var) + } + + #[inline(always)] + pub fn revoke_sys(&self, kind: Option<&str>) -> Result { + self.0.revoke_sys(kind) + } + + #[inline(always)] + pub fn revoke_run(&self, cmd: Option<&str>) -> Result { + self.0.revoke_run(cmd) + } + + #[inline(always)] + pub fn revoke_ffi(&self, path: Option<&str>) -> Result { + self.0.revoke_ffi(path) + } + + // request + + #[inline(always)] + pub fn request_read(&self, path: Option<&str>) -> Result { + self.0.request_read(path) + } + + #[inline(always)] + pub fn request_write(&self, path: Option<&str>) -> Result { + self.0.revoke_write(path) + } + + #[inline(always)] + pub fn request_net(&self, host: Option<&str>) -> Result { + self.0.request_net(host) + } + + #[inline(always)] + pub fn request_env(&self, var: Option<&str>) -> PermissionState { + self.0.request_env(var) + } + + #[inline(always)] + pub fn request_sys(&self, kind: Option<&str>) -> Result { + self.0.request_sys(kind) + } + + #[inline(always)] + pub fn request_run(&self, cmd: Option<&str>) -> Result { + self.0.request_run(cmd) + } + + #[inline(always)] + pub fn request_ffi(&self, path: Option<&str>) -> Result { + self.0.request_ffi(path) } } -static PERMSSIONSCONTAINER: Lazy = Lazy::new(|| { - PermissionsContainer::allow_all() -}); +#[derive(Debug, Clone)] +struct BrowserPermissionDescriptorParser; + +impl BrowserPermissionDescriptorParser { + 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 BrowserPermissionDescriptorParser { + 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_import_descriptor(&self, text: &str) -> Result { + ImportDescriptor::parse(text) + } + + fn parse_env_descriptor(&self, text: &str) -> Result { + Ok(EnvDescriptor::new(text)) + } + + fn parse_sys_descriptor(&self, text: &str) -> Result { + SysDescriptor::parse(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) + } +} + +static PERMSSIONSCONTAINER: Lazy = + Lazy::new(|| PermissionsContainer::allow_all(Arc::new(BrowserPermissionDescriptorParser))); #[wasm_bindgen] pub fn init_permissions_prompt(b: bool) { info!("init_permissions_prompt: {b}"); - *PERMSSIONSCONTAINER.0.0.lock() = if b { + *PERMSSIONSCONTAINER.0.lock() = if b { Permissions::none_with_prompt() } else { Permissions::none_without_prompt() @@ -207,7 +451,6 @@ pub struct JsCode { msg: Option, } - #[wasm_bindgen] impl JsCode { #[wasm_bindgen(getter)] @@ -245,21 +488,19 @@ impl JsCode { fn error>(code: Code, msg: T) -> Self { Self { code, - msg: Some(msg.into()) + msg: Some(msg.into()), } } } #[wasm_bindgen] pub fn check_read(path: &str, api_name: &str) -> JsCode { - let path = PathBuf::from(path); permission_check!(PERMSSIONSCONTAINER.check_read(&path, api_name)) } #[wasm_bindgen] pub fn check_write(path: &str, api_name: &str) -> JsCode { info!("check write: {path}"); - let path = PathBuf::from(path); permission_check!(PERMSSIONSCONTAINER.check_write(&path, api_name)) } @@ -272,15 +513,20 @@ pub fn check_env(env: &str) -> JsCode { #[wasm_bindgen] pub fn check_net(net: &str, api_name: &str) -> JsCode { info!("check net: {net}"); - let net= match net.rsplit_once(":") { + let net = match net.rsplit_once(":") { Some((a, p)) => { let port: Result = p.parse(); let port = match port { Ok(port) => Some(port), - Err(_) => return JsCode::error(Code::ParameterError, &format!("{net} parameter is error.")), + Err(_) => { + return JsCode::error( + Code::ParameterError, + &format!("{net} parameter is error."), + ) + } }; (a, port) - }, + } None => (net, None), }; permission_check!(PERMSSIONSCONTAINER.check_net(&net, api_name)) @@ -291,7 +537,9 @@ pub fn check_net_url(url: &str, api_name: &str) -> JsCode { info!("check net url: {url}"); let url = match url.parse() { Ok(url) => url, - Err(_) => return JsCode::error(Code::ParameterError, &format!("{url} parameter is error.")), + Err(_) => { + return JsCode::error(Code::ParameterError, &format!("{url} parameter is error.")) + } }; permission_check!(PERMSSIONSCONTAINER.check_net_url(&url, api_name)) } diff --git a/crates/browser/src/macros.rs b/crates/browser/src/macros.rs index 1167d62..4af3951 100644 --- a/crates/browser/src/macros.rs +++ b/crates/browser/src/macros.rs @@ -1,6 +1,5 @@ use wasm_bindgen::prelude::wasm_bindgen; - #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace = console, js_name = log)] @@ -61,12 +60,10 @@ macro_rules! error2jscode { JsCode::error(Code::Failed, $msg) } }; - ($e: expr) => { - { - let msg = format!("{}", $e); - error2jscode!($e, msg) - } - } + ($e: expr) => {{ + let msg = format!("{}", $e); + error2jscode!($e, msg) + }}; } macro_rules! permission_check { @@ -77,4 +74,4 @@ macro_rules! permission_check { JsCode::success() } }; -} \ No newline at end of file +} diff --git a/crates/browser/src/prompter.rs b/crates/browser/src/prompter.rs index 3062811..15a2621 100644 --- a/crates/browser/src/prompter.rs +++ b/crates/browser/src/prompter.rs @@ -1,6 +1,8 @@ use std::fmt::Write; use std::sync::Once; +use super::html::Html; +use crate::blsrt_get_input as get_input; use bls_permissions::bls_set_prompter; use bls_permissions::is_standalone; use bls_permissions::PermissionPrompter; @@ -8,10 +10,8 @@ use bls_permissions::PromptResponse; use bls_permissions::MAX_PERMISSION_PROMPT_LENGTH; use bls_permissions::PERMISSION_EMOJI; use serde::Serialize; -use crate::blsrt_get_input as get_input; -use super::html::Html; -#[cfg(target_arch = "wasm32")] +#[cfg(target_family = "wasm")] const YIELD: &str = "cmd:yield"; pub fn init_browser_prompter() { @@ -64,22 +64,29 @@ impl PermissionPrompter for BrowserPrompter { }; { let mut output = String::new(); - write!(&mut output, "
").unwrap(); - write!(&mut output, "┏ {} ", Html::span_color("yellow", PERMISSION_EMOJI)).unwrap(); + write!( + &mut output, + "
" + ) + .unwrap(); + write!( + &mut output, + "┏ {} ", + Html::span_color("yellow", PERMISSION_EMOJI) + ) + .unwrap(); write!(&mut output, "{}", Html::bold("bls-runtime requests ")).unwrap(); write!(&mut output, "{}", Html::bold(message)).unwrap(); writeln!(&mut output, "{}", ".").unwrap(); if let Some(api_name) = api_name.clone() { - writeln!( - &mut output, - "
┠─ Requested by `{}` API.", - api_name - ) - .unwrap(); + writeln!(&mut output, "
┠─ Requested by `{}` API.", api_name).unwrap(); } let msg = format!( "Learn more at: {}", - Html::color_with_underline("cyan", &format!("https://blockless.network/docs/go--allow-{}", name)) + Html::color_with_underline( + "cyan", + &format!("https://blockless.network/docs/go--allow-{}", name) + ) ); writeln!(&mut output, "
┠─ {}", Html::italic(&msg)).unwrap(); let msg = if is_standalone() { @@ -91,19 +98,20 @@ impl PermissionPrompter for BrowserPrompter { write!(&mut output, "
┗ {}", Html::bold("Allow?")).unwrap(); write!(&mut output, " {opts} ").unwrap(); write!(&mut output, "
").unwrap(); - let prompt_msg = serde_json::to_string(&PromptMsg{ + let prompt_msg = serde_json::to_string(&PromptMsg { api_name: api_name, message, dlg_html: &output, name, opts: &opts, is_unary, - }).unwrap(); + }) + .unwrap(); bls_prompt_dlg_info!("{prompt_msg}"); } let resp = loop { let input = blsrt_get_input(); - #[cfg(target_arch = "wasm32")] + #[cfg(target_family = "wasm")] if input == YIELD { return PromptResponse::Yield; } @@ -119,18 +127,18 @@ impl PermissionPrompter for BrowserPrompter { blsrt_show_tips!(fail: "❌ {msg}"); break PromptResponse::Deny; } - 'A' | 'a' if is_unary => { + 'A' | 'a' if is_unary => { let msg = format!("Granted all {name} access."); blsrt_show_tips!(success: "✅ {msg}"); break PromptResponse::AllowAll; } _ => { blsrt_show_tips!(fail:"┗ Unrecognized option. Allow? {opts} > "); - #[cfg(target_arch = "wasm32")] + #[cfg(target_family = "wasm")] break PromptResponse::Yield; } } }; return resp; } -} \ No newline at end of file +} diff --git a/crates/deno/Cargo.toml b/crates/deno/Cargo.toml index b3ec1e8..fc5da45 100644 --- a/crates/deno/Cargo.toml +++ b/crates/deno/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deno_permissions" -version = "0.24.0" +version = "0.33.0" authors.workspace = true edition.workspace = true license.workspace = true diff --git a/crates/deno/src/lib.rs b/crates/deno/src/lib.rs index 10053d7..45cf8cb 100644 --- a/crates/deno/src/lib.rs +++ b/crates/deno/src/lib.rs @@ -1,16 +1,10 @@ -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -use deno_core::serde::de; -use deno_core::serde::Deserialize; -use deno_core::serde::Deserializer; -use deno_core::serde_json; -use deno_core::unsync::sync::AtomicFlag; -use deno_terminal::colors; use prompter::init_tty_prompter; -use std::fmt; +use std::borrow::Cow; use std::fmt::Debug; use std::path::Path; +use std::path::PathBuf; +use std::sync::Arc; pub mod prompter; pub use prompter::set_prompt_callbacks; @@ -18,46 +12,60 @@ pub use prompter::PromptCallback; pub use bls_permissions::*; - #[derive(Clone, Debug)] pub struct PermissionsContainer(pub bls_permissions::BlsPermissionsContainer); - impl PermissionsContainer { - pub fn new(perms: Permissions) -> Self { + pub fn new(descriptor_parser: Arc, perms: Permissions) -> Self { init_tty_prompter(); - init_debug_log_msg_func(|msg: &str| format!("{}", colors::bold(msg))); - Self(BlsPermissionsContainer::new(perms)) + Self(BlsPermissionsContainer::new(descriptor_parser, perms)) } - #[inline(always)] - pub fn allow_hrtime(&mut self) -> bool { - self.0.allow_hrtime() + pub fn create_child_permissions( + &self, + child_permissions_arg: ChildPermissionsArg, + ) -> Result { + Ok(PermissionsContainer(self.0.create_child_permissions(child_permissions_arg)?)) } - 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)] - pub fn check_specifier(&self, specifier: &ModuleSpecifier) -> Result<(), AnyError> { - self.0.check_specifier(specifier) + pub fn check_specifier( + &self, + specifier: &ModuleSpecifier, + kind: CheckSpecifierKind, + ) -> Result<(), AnyError> { + self.0.check_specifier(specifier, kind) } #[inline(always)] - pub fn check_read(&mut self, path: &Path, api_name: &str) -> Result<(), AnyError> { + pub fn check_read(&self, path: &str, api_name: &str) -> Result { self.0.check_read(path, api_name) } #[inline(always)] pub fn check_read_with_api_name( &self, - path: &Path, + path: &str, api_name: Option<&str>, - ) -> Result<(), AnyError> { + ) -> Result { self.0.check_read_with_api_name(path, api_name) } + #[inline(always)] + pub fn check_read_path<'a>( + &self, + path: &'a Path, + api_name: Option<&str>, + ) -> Result, AnyError> { + self.0.check_read_path(path, api_name) + } + + /// 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, @@ -69,32 +77,48 @@ impl PermissionsContainer { } #[inline(always)] - pub fn check_read_all(&mut self, api_name: &str) -> Result<(), AnyError> { + pub fn check_read_all(&self, api_name: &str) -> Result<(), AnyError> { self.0.check_read_all(api_name) } #[inline(always)] - pub fn check_write(&mut self, path: &Path, api_name: &str) -> Result<(), AnyError> { + pub fn query_read_all(&self) -> bool { + self.0.query_read_all() + } + + #[inline(always)] + pub fn check_write(&self, path: &str, api_name: &str) -> Result { self.0.check_write(path, api_name) } #[inline(always)] pub fn check_write_with_api_name( &self, - path: &Path, + path: &str, api_name: Option<&str>, - ) -> Result<(), AnyError> { + ) -> Result { self.0.check_write_with_api_name(path, api_name) } #[inline(always)] - pub fn check_write_all(&mut self, api_name: &str) -> Result<(), AnyError> { + pub fn check_write_path<'a>( + &self, + path: &'a Path, + api_name: &str, + ) -> Result, AnyError> { + self.0.check_write_path(path, api_name) + } + + #[inline(always)] + pub fn check_write_all(&self, api_name: &str) -> Result<(), AnyError> { self.0.check_write_all(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, @@ -103,12 +127,12 @@ impl PermissionsContainer { } #[inline(always)] - pub fn check_write_partial(&mut self, path: &Path, api_name: &str) -> Result<(), AnyError> { + pub fn check_write_partial(&mut self, path: &str, api_name: &str) -> Result { self.0.check_write_partial(path, api_name) } #[inline(always)] - pub fn check_run(&mut self, cmd: &str, api_name: &str) -> Result<(), AnyError> { + pub fn check_run(&mut self, cmd: &RunQueryDescriptor, api_name: &str) -> Result<(), AnyError> { self.0.check_run(cmd, api_name) } @@ -117,6 +141,11 @@ impl PermissionsContainer { self.0.check_run_all(api_name) } + #[inline(always)] + pub fn query_run_all(&mut self, api_name: &str) -> bool { + self.0.query_run_all(api_name) + } + #[inline(always)] pub fn check_sys(&self, kind: &str, api_name: &str) -> Result<(), AnyError> { self.0.check_sys(kind, api_name) @@ -149,6 +178,8 @@ impl PermissionsContainer { self.0.check_was_allow_all_flag_passed() } + /// Checks special file access, returning the failed permission type if + /// not successful. pub fn check_special_file(&mut self, path: &Path, api_name: &str) -> Result<(), &'static str> { self.0.check_special_file(path, api_name) } @@ -168,368 +199,290 @@ impl PermissionsContainer { } #[inline(always)] - pub fn check_ffi(&mut self, path: Option<&Path>) -> Result<(), AnyError> { + pub fn check_ffi(&mut self, path: &str) -> Result { self.0.check_ffi(path) } #[inline(always)] - pub fn check_ffi_partial(&mut self, path: Option<&Path>) -> Result<(), AnyError> { - self.0.check_ffi_partial(path) + pub fn check_ffi_partial_no_path(&mut self) -> Result<(), AnyError> { + self.0.check_ffi_partial_no_path() } -} + #[inline(always)] + pub fn check_ffi_partial_with_path(&mut self, path: &str) -> Result { + self.0.check_ffi_partial_with_path(path) + } -/// Directly deserializable from JS worker and test permission options. -#[derive(Debug, Eq, PartialEq)] -pub struct ChildPermissionsArg { - env: ChildUnaryPermissionArg, - hrtime: ChildUnitPermissionArg, - net: ChildUnaryPermissionArg, - ffi: ChildUnaryPermissionArg, - read: ChildUnaryPermissionArg, - run: ChildUnaryPermissionArg, - sys: ChildUnaryPermissionArg, - write: ChildUnaryPermissionArg, -} + // query -impl ChildPermissionsArg { - pub fn inherit() -> Self { - ChildPermissionsArg { - env: ChildUnaryPermissionArg::Inherit, - hrtime: ChildUnitPermissionArg::Inherit, - net: ChildUnaryPermissionArg::Inherit, - ffi: ChildUnaryPermissionArg::Inherit, - read: ChildUnaryPermissionArg::Inherit, - run: ChildUnaryPermissionArg::Inherit, - sys: ChildUnaryPermissionArg::Inherit, - write: ChildUnaryPermissionArg::Inherit, - } + #[inline(always)] + pub fn query_read(&self, path: Option<&str>) -> Result { + self.0.query_read(path) } - pub fn none() -> Self { - ChildPermissionsArg { - env: ChildUnaryPermissionArg::NotGranted, - hrtime: ChildUnitPermissionArg::NotGranted, - net: ChildUnaryPermissionArg::NotGranted, - ffi: ChildUnaryPermissionArg::NotGranted, - read: ChildUnaryPermissionArg::NotGranted, - run: ChildUnaryPermissionArg::NotGranted, - sys: ChildUnaryPermissionArg::NotGranted, - write: ChildUnaryPermissionArg::NotGranted, - } + #[inline(always)] + pub fn query_write(&self, path: Option<&str>) -> Result { + self.0.query_write(path) } -} -impl<'de> Deserialize<'de> for ChildPermissionsArg { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct ChildPermissionsArgVisitor; - impl<'de> de::Visitor<'de> for ChildPermissionsArgVisitor { - type Value = ChildPermissionsArg; + #[inline(always)] + pub fn query_net(&self, host: Option<&str>) -> Result { + self.0.query_net(host) + } - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("\"inherit\" or \"none\" or object") - } + #[inline(always)] + pub fn query_env(&self, var: Option<&str>) -> PermissionState { + self.0.query_env(var) + } - fn visit_unit(self) -> Result - where - E: de::Error, - { - Ok(ChildPermissionsArg::inherit()) - } + #[inline(always)] + pub fn query_sys(&self, kind: Option<&str>) -> Result { + self.0.query_sys(kind) + } - fn visit_str(self, v: &str) -> Result - where - E: de::Error, - { - if v == "inherit" { - Ok(ChildPermissionsArg::inherit()) - } else if v == "none" { - Ok(ChildPermissionsArg::none()) - } else { - Err(de::Error::invalid_value(de::Unexpected::Str(v), &self)) - } - } + #[inline(always)] + pub fn query_run(&self, cmd: Option<&str>) -> Result { + self.0.query_run(cmd) + } - fn visit_map(self, mut v: V) -> Result - where - V: de::MapAccess<'de>, - { - let mut child_permissions_arg = ChildPermissionsArg::none(); - while let Some((key, value)) = v.next_entry::()? { - if key == "env" { - let arg = serde_json::from_value::(value); - child_permissions_arg.env = arg.map_err(|e| { - de::Error::custom(format!("(deno.permissions.env) {e}")) - })?; - } else if key == "hrtime" { - let arg = serde_json::from_value::(value); - child_permissions_arg.hrtime = arg.map_err(|e| { - de::Error::custom(format!("(deno.permissions.hrtime) {e}")) - })?; - } else if key == "net" { - let arg = serde_json::from_value::(value); - child_permissions_arg.net = arg.map_err(|e| { - de::Error::custom(format!("(deno.permissions.net) {e}")) - })?; - } else if key == "ffi" { - let arg = serde_json::from_value::(value); - child_permissions_arg.ffi = arg.map_err(|e| { - de::Error::custom(format!("(deno.permissions.ffi) {e}")) - })?; - } else if key == "read" { - let arg = serde_json::from_value::(value); - child_permissions_arg.read = arg.map_err(|e| { - de::Error::custom(format!("(deno.permissions.read) {e}")) - })?; - } else if key == "run" { - let arg = serde_json::from_value::(value); - child_permissions_arg.run = arg.map_err(|e| { - de::Error::custom(format!("(deno.permissions.run) {e}")) - })?; - } else if key == "sys" { - let arg = serde_json::from_value::(value); - child_permissions_arg.sys = arg.map_err(|e| { - de::Error::custom(format!("(deno.permissions.sys) {e}")) - })?; - } else if key == "write" { - let arg = serde_json::from_value::(value); - child_permissions_arg.write = arg.map_err(|e| { - de::Error::custom(format!("(deno.permissions.write) {e}")) - })?; - } else { - return Err(de::Error::custom("unknown permission name")); - } - } - Ok(child_permissions_arg) - } - } - deserializer.deserialize_any(ChildPermissionsArgVisitor) + #[inline(always)] + pub fn query_ffi(&self, path: Option<&str>) -> Result { + self.0.query_ffi(path) } -} -pub fn create_child_permissions( - main_perms: &mut Permissions, - child_permissions_arg: ChildPermissionsArg, -) -> Result { - fn is_granted_unary(arg: &ChildUnaryPermissionArg) -> bool { - match arg { - ChildUnaryPermissionArg::Inherit | ChildUnaryPermissionArg::Granted => true, - ChildUnaryPermissionArg::NotGranted | ChildUnaryPermissionArg::GrantedList(_) => false, - } + // revoke + + #[inline(always)] + pub fn revoke_read(&self, path: Option<&str>) -> Result { + self.0.revoke_read(path) } - fn is_granted_unit(arg: &ChildUnitPermissionArg) -> bool { - match arg { - ChildUnitPermissionArg::Inherit | ChildUnitPermissionArg::Granted => true, - ChildUnitPermissionArg::NotGranted => false, - } + #[inline(always)] + pub fn revoke_write(&self, path: Option<&str>) -> Result { + self.0.revoke_write(path) } - let mut worker_perms = Permissions::none_without_prompt(); + #[inline(always)] + pub fn revoke_net(&self, host: Option<&str>) -> Result { + self.0.revoke_net(host) + } - worker_perms.all = main_perms - .all - .create_child_permissions(ChildUnitPermissionArg::Inherit)?; + #[inline(always)] + pub fn revoke_env(&self, var: Option<&str>) -> PermissionState { + self.0.revoke_env(var) + } - // downgrade the `worker_perms.all` based on the other values - if worker_perms.all.query() == PermissionState::Granted { - let unary_perms = [ - &child_permissions_arg.read, - &child_permissions_arg.write, - &child_permissions_arg.net, - &child_permissions_arg.env, - &child_permissions_arg.sys, - &child_permissions_arg.run, - &child_permissions_arg.ffi, - ]; - let unit_perms = [&child_permissions_arg.hrtime]; - let allow_all = unary_perms.into_iter().all(is_granted_unary) - && unit_perms.into_iter().all(is_granted_unit); - if !allow_all { - worker_perms.all.revoke(); - } + #[inline(always)] + pub fn revoke_sys(&self, kind: Option<&str>) -> Result { + self.0.revoke_sys(kind) } - // WARNING: When adding a permission here, ensure it is handled - // in the worker_perms.all block above - worker_perms.read = main_perms - .read - .create_child_permissions(child_permissions_arg.read)?; - worker_perms.write = main_perms - .write - .create_child_permissions(child_permissions_arg.write)?; - worker_perms.net = main_perms - .net - .create_child_permissions(child_permissions_arg.net)?; - worker_perms.env = main_perms - .env - .create_child_permissions(child_permissions_arg.env)?; - 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)?; - worker_perms.ffi = main_perms - .ffi - .create_child_permissions(child_permissions_arg.ffi)?; - worker_perms.hrtime = main_perms - .hrtime - .create_child_permissions(child_permissions_arg.hrtime)?; - - Ok(worker_perms) -} + #[inline(always)] + pub fn revoke_run(&self, cmd: Option<&str>) -> Result { + self.0.revoke_run(cmd) + } + + #[inline(always)] + pub fn revoke_ffi(&self, path: Option<&str>) -> Result { + self.0.revoke_ffi(path) + } -static IS_STANDALONE: AtomicFlag = AtomicFlag::lowered(); + // request -pub fn mark_standalone() { - IS_STANDALONE.raise(); -} + #[inline(always)] + pub fn request_read(&self, path: Option<&str>) -> Result { + self.0.request_read(path) + } -pub fn is_standalone() -> bool { - IS_STANDALONE.is_raised() + #[inline(always)] + pub fn request_write(&self, path: Option<&str>) -> Result { + self.0.revoke_write(path) + } + + #[inline(always)] + pub fn request_net(&self, host: Option<&str>) -> Result { + self.0.request_net(host) + } + + #[inline(always)] + pub fn request_env(&self, var: Option<&str>) -> PermissionState { + self.0.request_env(var) + } + + #[inline(always)] + pub fn request_sys(&self, kind: Option<&str>) -> Result { + self.0.request_sys(kind) + } + + #[inline(always)] + pub fn request_run(&self, cmd: Option<&str>) -> Result { + self.0.request_run(cmd) + } + + #[inline(always)] + pub fn request_ffi(&self, path: Option<&str>) -> Result { + self.0.request_ffi(path) + } } #[cfg(test)] mod tests { use super::*; use deno_core::serde_json::json; + use deno_core::{parking_lot::Mutex, serde_json}; use fqdn::fqdn; - use prompter::tests::*; - use std::net::Ipv4Addr; - use deno_core::url; - use std::net::IpAddr; - use std::net::Ipv6Addr; - use std::path::PathBuf; - use std::collections::HashSet; - use std::string::ToString; + use prompter::{set_prompter, tests::*}; + use std::{ + collections::HashSet, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + }; // Creates vector of strings, Vec macro_rules! svec { ($($x:expr),*) => (vec![$($x.to_string()),*]); - } + } + // make the test thread serial process, make set prompter safe(it's global variable). + static TESTMUTEX: Mutex<()> = Mutex::new(()); + + #[derive(Debug, Clone)] + 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) + } + } + } - #[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"), - ]; + impl PermissionDescriptorParser for TestPermissionDescriptorParser { + fn parse_read_descriptor(&self, text: &str) -> Result { + Ok(ReadDescriptor(self.join_path_with_root(text))) + } - let mut perms = Permissions::from_options(&PermissionsOptions { - allow_read: Some(allowlist.clone()), - allow_write: Some(allowlist.clone()), - allow_ffi: Some(allowlist), - ..Default::default() - }) - .unwrap(); + fn parse_write_descriptor(&self, text: &str) -> Result { + Ok(WriteDescriptor(self.join_path_with_root(text))) + } - // 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()); + fn parse_net_descriptor(&self, text: &str) -> Result { + NetDescriptor::parse(text) + } - // 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()); + fn parse_import_descriptor(&self, text: &str) -> Result { + ImportDescriptor::parse(text) + } - // 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()); + fn parse_env_descriptor(&self, text: &str) -> Result { + Ok(EnvDescriptor::new(text)) + } - // 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()); + fn parse_sys_descriptor(&self, text: &str) -> Result { + SysDescriptor::parse(text.to_string()) + } - // 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()); + fn parse_allow_run_descriptor( + &self, + text: &str, + ) -> Result { + Ok(AllowRunDescriptorParseResult::Descriptor( + AllowRunDescriptor(self.join_path_with_root(text)), + )) + } - // 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()); + 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())) + } + } - // 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()); + 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() { + let _locked = TESTMUTEX.lock(); + 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); + set_prompter(Box::new(TestPrompter)); - // 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()); + 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), + ]; - // 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() { + let _locked = TESTMUTEX.lock(); 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![ @@ -559,7 +512,7 @@ mod tests { ]; for (host, port, is_ok) in domain_tests { - let host = host.parse().unwrap(); + let host = Host::parse(host).unwrap(); let descriptor = NetDescriptor(host, Some(port)); assert_eq!( is_ok, @@ -571,11 +524,16 @@ mod tests { #[test] fn test_check_net_only_flag() { + let _locked = TESTMUTEX.lock(); 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![ @@ -601,7 +559,7 @@ mod tests { ]; for (host_str, port) in domain_tests { - let host = host_str.parse().unwrap(); + let host = Host::parse(host_str).unwrap(); let descriptor = NetDescriptor(host, Some(port)); assert!( perms.net.check(&descriptor, None).is_ok(), @@ -612,11 +570,16 @@ mod tests { #[test] fn test_check_net_no_flag() { + let _locked = TESTMUTEX.lock(); 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![ @@ -642,7 +605,7 @@ mod tests { ]; for (host_str, port) in domain_tests { - let host = host_str.parse().unwrap(); + let host = Host::parse(host_str).unwrap(); let descriptor = NetDescriptor(host, Some(port)); assert!( perms.net.check(&descriptor, None).is_err(), @@ -653,18 +616,24 @@ 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 _locked = TESTMUTEX.lock(); + let parser = TestPermissionDescriptorParser; + let 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" + ]), + ..Default::default() + }, + ) .unwrap(); + let mut perms = PermissionsContainer::new(Arc::new(parser), perms); let url_tests = vec![ // Any protocol + port for localhost should be ok, since we don't specify @@ -706,55 +675,91 @@ mod tests { ]; for (url_str, is_ok) in url_tests { - let u = url::Url::parse(url_str).unwrap(); - assert_eq!(is_ok, perms.net.check_url(&u, None).is_ok(), "{}", u); + let u = Url::parse(url_str).unwrap(); + assert_eq!(is_ok, perms.check_net_url(&u, "api()").is_ok(), "{}", u); } } #[test] fn check_specifiers() { - set_prompter(Box::new(TestPrompter)); + let _locked = TESTMUTEX.lock(); 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 parser = TestPermissionDescriptorParser; + let perms = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_read: Some(read_allowlist), + allow_import: Some(svec!["localhost"]), + ..Default::default() + }, + ) .unwrap(); - + let perms = PermissionsContainer::new(Arc::new(parser), perms); + set_prompter(Box::new(TestPrompter)); let mut fixtures = vec![ ( ModuleSpecifier::parse("http://localhost:4545/mod.ts").unwrap(), + CheckSpecifierKind::Static, + true, + ), + ( + ModuleSpecifier::parse("http://localhost:4545/mod.ts").unwrap(), + CheckSpecifierKind::Dynamic, true, ), ( ModuleSpecifier::parse("http://deno.land/x/mod.ts").unwrap(), + CheckSpecifierKind::Dynamic, false, ), ( ModuleSpecifier::parse("data:text/plain,Hello%2C%20Deno!").unwrap(), + CheckSpecifierKind::Dynamic, true, ), ]; if cfg!(target_os = "windows") { - fixtures.push((ModuleSpecifier::parse("file:///C:/a/mod.ts").unwrap(), true)); + fixtures.push(( + ModuleSpecifier::parse("file:///C:/a/mod.ts").unwrap(), + CheckSpecifierKind::Dynamic, + true, + )); + fixtures.push(( + ModuleSpecifier::parse("file:///C:/b/mod.ts").unwrap(), + CheckSpecifierKind::Static, + true, + )); fixtures.push(( ModuleSpecifier::parse("file:///C:/b/mod.ts").unwrap(), + CheckSpecifierKind::Dynamic, false, )); } else { - fixtures.push((ModuleSpecifier::parse("file:///a/mod.ts").unwrap(), true)); - fixtures.push((ModuleSpecifier::parse("file:///b/mod.ts").unwrap(), false)); + fixtures.push(( + ModuleSpecifier::parse("file:///a/mod.ts").unwrap(), + CheckSpecifierKind::Dynamic, + true, + )); + fixtures.push(( + ModuleSpecifier::parse("file:///b/mod.ts").unwrap(), + CheckSpecifierKind::Static, + true, + )); + fixtures.push(( + ModuleSpecifier::parse("file:///b/mod.ts").unwrap(), + CheckSpecifierKind::Dynamic, + false, + )); } - for (specifier, expected) in fixtures { + for (specifier, kind, expected) in fixtures { assert_eq!( - perms.check_specifier(&specifier).is_ok(), + perms.check_specifier(&specifier, kind).is_ok(), expected, "{}", specifier, @@ -762,279 +767,287 @@ mod tests { } } - #[test] - fn check_invalid_specifiers() { - set_prompter(Box::new(TestPrompter)); - let mut perms = Permissions::allow_all(); - - let mut test_cases = vec![]; - - if cfg!(target_os = "windows") { - test_cases.push("file://"); - test_cases.push("file:///"); - } else { - test_cases.push("file://remotehost/"); - } - - for url in test_cases { - assert!(perms - .check_specifier(&ModuleSpecifier::parse(url).unwrap()) - .is_err()); - } - } - #[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(vec![PathBuf::from("/foo")]), &None, false).unwrap(), - write: Permissions::new_unary(&Some(vec![PathBuf::from("/foo")]), &None, false) - .unwrap(), - ffi: Permissions::new_unary(&Some(vec![PathBuf::from("/foo")]), &None, false).unwrap(), - net: Permissions::new_unary(&Some(svec!["127.0.0.1:8000"]), &None, false).unwrap(), - env: Permissions::new_unary(&Some(svec!["HOME"]), &None, false).unwrap(), - sys: Permissions::new_unary(&Some(svec!["hostname"]), &None, false).unwrap(), - run: Permissions::new_unary(&Some(svec!["deno"]), &None, false).unwrap(), - all: Permissions::new_all(false), - hrtime: Permissions::new_hrtime(false, false), - }; - let perms3 = Permissions { - read: Permissions::new_unary(&None, &Some(vec![PathBuf::from("/foo")]), false).unwrap(), - write: Permissions::new_unary(&None, &Some(vec![PathBuf::from("/foo")]), false) - .unwrap(), - ffi: Permissions::new_unary(&None, &Some(vec![PathBuf::from("/foo")]), false).unwrap(), - net: Permissions::new_unary(&None, &Some(svec!["127.0.0.1:8000"]), false).unwrap(), - env: Permissions::new_unary(&None, &Some(svec!["HOME"]), false).unwrap(), - sys: Permissions::new_unary(&None, &Some(svec!["hostname"]), false).unwrap(), - run: Permissions::new_unary(&None, &Some(svec!["deno"]), false).unwrap(), - all: Permissions::new_all(false), - hrtime: Permissions::new_hrtime(false, true), - }; - let perms4 = Permissions { - read: Permissions::new_unary(&Some(vec![]), &Some(vec![PathBuf::from("/foo")]), false) - .unwrap(), - write: Permissions::new_unary(&Some(vec![]), &Some(vec![PathBuf::from("/foo")]), false) - .unwrap(), - ffi: Permissions::new_unary(&Some(vec![]), &Some(vec![PathBuf::from("/foo")]), false) - .unwrap(), - net: Permissions::new_unary(&Some(vec![]), &Some(svec!["127.0.0.1:8000"]), false) - .unwrap(), - env: Permissions::new_unary(&Some(vec![]), &Some(svec!["HOME"]), false).unwrap(), - sys: Permissions::new_unary(&Some(vec![]), &Some(svec!["hostname"]), false).unwrap(), - run: Permissions::new_unary(&Some(vec![]), &Some(svec!["deno"]), false).unwrap(), - all: Permissions::new_all(false), - hrtime: Permissions::new_hrtime(true, true), - }; + 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] - { - assert_eq!(perms1.read.query(None), PermissionState::Granted); - assert_eq!(perms1.read.query(Some(Path::new("/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!(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!(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!(perms1.write.query(None), PermissionState::Granted); - assert_eq!(perms1.write.query(Some(Path::new("/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!(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!(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!(perms1.ffi.query(None), PermissionState::Granted); - assert_eq!(perms1.ffi.query(Some(Path::new("/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!(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!(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!(perms1.net.query(None), PermissionState::Granted); - assert_eq!(perms1.net.query(Some(&NetDescriptor("127.0.0.1".parse().unwrap(), None))), PermissionState::Granted); - assert_eq!(perms2.net.query(None), PermissionState::Prompt); - assert_eq!(perms2.net.query(Some(&NetDescriptor("127.0.0.1".parse().unwrap(), Some(8000)))), PermissionState::Granted); - assert_eq!(perms3.net.query(None), PermissionState::Prompt); - assert_eq!(perms3.net.query(Some(&NetDescriptor("127.0.0.1".parse().unwrap(), Some(8000)))), PermissionState::Denied); - assert_eq!(perms4.net.query(None), PermissionState::GrantedPartial); - assert_eq!(perms4.net.query(Some(&NetDescriptor("127.0.0.1".parse().unwrap(), Some(8000)))), PermissionState::Denied); - assert_eq!(perms4.net.query(Some(&NetDescriptor("192.168.0.1".parse().unwrap(), Some(8000)))), PermissionState::Granted); - assert_eq!(perms1.env.query(None), PermissionState::Granted); - assert_eq!(perms1.env.query(Some("HOME")), PermissionState::Granted); - assert_eq!(perms2.env.query(None), PermissionState::Prompt); - assert_eq!(perms2.env.query(Some("HOME")), PermissionState::Granted); - assert_eq!(perms3.env.query(None), PermissionState::Prompt); - assert_eq!(perms3.env.query(Some("HOME")), PermissionState::Denied); - assert_eq!(perms4.env.query(None), PermissionState::GrantedPartial); - assert_eq!(perms4.env.query(Some("HOME")), PermissionState::Denied); - assert_eq!(perms4.env.query(Some("AWAY")), PermissionState::Granted); - assert_eq!(perms1.sys.query(None), PermissionState::Granted); - assert_eq!(perms1.sys.query(Some("HOME")), PermissionState::Granted); - assert_eq!(perms2.sys.query(None), PermissionState::Prompt); - assert_eq!(perms2.sys.query(Some("hostname")), PermissionState::Granted); - assert_eq!(perms3.sys.query(None), PermissionState::Prompt); - assert_eq!(perms3.sys.query(Some("hostname")), PermissionState::Denied); - assert_eq!(perms4.sys.query(None), PermissionState::GrantedPartial); - 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); - assert_eq!(perms2.run.query(None), PermissionState::Prompt); - assert_eq!(perms2.run.query(Some("deno")), PermissionState::Granted); - assert_eq!(perms3.run.query(None), PermissionState::Prompt); - assert_eq!(perms3.run.query(Some("deno")), 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!(perms1.hrtime.query(), PermissionState::Granted); - assert_eq!(perms2.hrtime.query(), PermissionState::Prompt); - assert_eq!(perms3.hrtime.query(), PermissionState::Denied); - assert_eq!(perms4.hrtime.query(), PermissionState::Denied); - }; + { + 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(&read_query("/foo"))), PermissionState::Granted); + assert_eq!(perms2.read.query(None), PermissionState::Prompt); + 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(&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(&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(&write_query("/foo"))), PermissionState::Granted); + assert_eq!(perms2.write.query(None), PermissionState::Prompt); + 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(&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(&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(&ffi_query("/foo"))), PermissionState::Granted); + assert_eq!(perms2.ffi.query(None), PermissionState::Prompt); + 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(&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(&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); + assert_eq!(perms2.net.query(Some(&NetDescriptor(Host::must_parse("127.0.0.1"), Some(8000)))), PermissionState::Granted); + assert_eq!(perms3.net.query(None), PermissionState::Prompt); + assert_eq!(perms3.net.query(Some(&NetDescriptor(Host::must_parse("127.0.0.1"), Some(8000)))), PermissionState::Denied); + assert_eq!(perms4.net.query(None), PermissionState::GrantedPartial); + assert_eq!(perms4.net.query(Some(&NetDescriptor(Host::must_parse("127.0.0.1"), Some(8000)))), PermissionState::Denied); + assert_eq!(perms4.net.query(Some(&NetDescriptor(Host::must_parse("192.168.0.1"), Some(8000)))), PermissionState::Granted); + assert_eq!(perms1.env.query(None), PermissionState::Granted); + assert_eq!(perms1.env.query(Some("HOME")), PermissionState::Granted); + assert_eq!(perms2.env.query(None), PermissionState::Prompt); + assert_eq!(perms2.env.query(Some("HOME")), PermissionState::Granted); + assert_eq!(perms3.env.query(None), PermissionState::Prompt); + assert_eq!(perms3.env.query(Some("HOME")), PermissionState::Denied); + assert_eq!(perms4.env.query(None), PermissionState::GrantedPartial); + assert_eq!(perms4.env.query(Some("HOME")), PermissionState::Denied); + assert_eq!(perms4.env.query(Some("AWAY")), PermissionState::Granted); + let sys_desc = |name: &str| SysDescriptor::parse(name.to_string()).unwrap(); + assert_eq!(perms1.sys.query(None), PermissionState::Granted); + assert_eq!(perms1.sys.query(Some(&sys_desc("osRelease"))), PermissionState::Granted); + assert_eq!(perms2.sys.query(None), PermissionState::Prompt); + assert_eq!(perms2.sys.query(Some(&sys_desc("hostname"))), PermissionState::Granted); + assert_eq!(perms3.sys.query(None), PermissionState::Prompt); + assert_eq!(perms3.sys.query(Some(&sys_desc("hostname"))), PermissionState::Denied); + assert_eq!(perms4.sys.query(None), PermissionState::GrantedPartial); + assert_eq!(perms4.sys.query(Some(&sys_desc("hostname"))), PermissionState::Denied); + assert_eq!(perms4.sys.query(Some(&sys_desc("uid"))), PermissionState::Granted); + assert_eq!(perms1.run.query(None), 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_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_run_query)), PermissionState::Denied); + assert_eq!(perms4.run.query(None), PermissionState::GrantedPartial); + 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 mut perms: Permissions = Permissions::none_without_prompt(); + let parser = TestPermissionDescriptorParser; + let mut perms: Permissions = Permissions::none_with_prompt(); + let mut perms_no_prompt: 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.query(None), PermissionState::Prompt); - prompt_value.set(false); - assert_eq!(perms.read.request(Some(Path::new("/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); - 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); - prompt_value.set(true); - assert_eq!(perms.ffi.request(None), PermissionState::Denied); - prompt_value.set(true); - assert_eq!(perms.net.request(Some(&NetDescriptor("127.0.0.1".parse().unwrap(), None))), PermissionState::Granted); - prompt_value.set(false); - assert_eq!(perms.net.request(Some(&NetDescriptor("127.0.0.1".parse().unwrap(), Some(8000)))), PermissionState::Granted); - prompt_value.set(true); - assert_eq!(perms.env.request(Some("HOME")), PermissionState::Granted); - assert_eq!(perms.env.query(None), PermissionState::Prompt); - prompt_value.set(false); - assert_eq!(perms.env.request(Some("HOME")), PermissionState::Granted); - prompt_value.set(true); - assert_eq!(perms.sys.request(Some("hostname")), PermissionState::Granted); - assert_eq!(perms.sys.query(None), PermissionState::Prompt); - 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); - assert_eq!(perms.run.query(None), PermissionState::Prompt); - prompt_value.set(false); - assert_eq!(perms.run.request(Some("deno")), PermissionState::Granted); - prompt_value.set(false); - assert_eq!(perms.hrtime.request(), PermissionState::Denied); - prompt_value.set(true); - assert_eq!(perms.hrtime.request(), PermissionState::Denied); - }; + { + let _locked = TESTMUTEX.lock(); + let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock(); + prompt_value.set(true); + 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(&read_query("/foo/bar"))), PermissionState::Granted); + prompt_value.set(false); + 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(&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); + assert_eq!(perms.net.request(Some(&NetDescriptor(Host::must_parse("127.0.0.1"), None))), PermissionState::Granted); + prompt_value.set(false); + assert_eq!(perms.net.request(Some(&NetDescriptor(Host::must_parse("127.0.0.1"), Some(8000)))), PermissionState::Granted); + prompt_value.set(true); + assert_eq!(perms.env.request(Some("HOME")), PermissionState::Granted); + assert_eq!(perms.env.query(None), PermissionState::Prompt); + prompt_value.set(false); + assert_eq!(perms.env.request(Some("HOME")), PermissionState::Granted); + prompt_value.set(true); + let sys_desc = |name: &str| SysDescriptor::parse(name.to_string()).unwrap(); + assert_eq!(perms.sys.request(Some(&sys_desc("hostname"))), PermissionState::Granted); + assert_eq!(perms.sys.query(None), PermissionState::Prompt); + prompt_value.set(false); + assert_eq!(perms.sys.request(Some(&sys_desc("hostname"))), PermissionState::Granted); + prompt_value.set(true); + 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(&run_query)), PermissionState::Granted); + assert_eq!(perms_no_prompt.read.request(Some(&read_query("/foo"))), PermissionState::Denied); + }; } #[test] fn test_revoke() { + let _locked = TESTMUTEX.lock(); set_prompter(Box::new(TestPrompter)); - let mut perms = Permissions { - read: Permissions::new_unary( - &Some(vec![PathBuf::from("/foo"), PathBuf::from("/foo/baz")]), - &None, - false, - ) - .unwrap(), - write: Permissions::new_unary( - &Some(vec![PathBuf::from("/foo"), PathBuf::from("/foo/baz")]), - &None, - false, - ) - .unwrap(), - ffi: Permissions::new_unary( - &Some(vec![PathBuf::from("/foo"), PathBuf::from("/foo/baz")]), - &None, - false, - ) - .unwrap(), - net: Permissions::new_unary(&Some(svec!["127.0.0.1", "127.0.0.1:8000"]), &None, false) - .unwrap(), - env: Permissions::new_unary(&Some(svec!["HOME"]), &None, false).unwrap(), - sys: Permissions::new_unary(&Some(svec!["hostname"]), &None, false).unwrap(), - run: Permissions::new_unary(&Some(svec!["deno"]), &None, false).unwrap(), - all: Permissions::new_all(false), - hrtime: Permissions::new_hrtime(false, true), - }; + 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.net.revoke(Some(&NetDescriptor("127.0.0.1".parse().unwrap(), Some(9000)))), PermissionState::Prompt); - assert_eq!(perms.net.query(Some(&NetDescriptor("127.0.0.1".parse().unwrap(), None))), PermissionState::Prompt); - assert_eq!(perms.net.query(Some(&NetDescriptor("127.0.0.1".parse().unwrap(), 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); - assert_eq!(perms.hrtime.revoke(), PermissionState::Denied); + 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); + let run_query = RunQueryDescriptor::Path { + requested: "deno".to_string(), + resolved: PathBuf::from("/deno"), + }; + assert_eq!(perms.run.revoke(Some(&run_query)), PermissionState::Prompt); }; } #[test] fn test_check() { + let _locked = TESTMUTEX.lock(); 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 .net .check( - &NetDescriptor("127.0.0.1".parse().unwrap(), Some(8000)), + &NetDescriptor(Host::must_parse("127.0.0.1"), Some(8000)), None ) .is_ok()); @@ -1042,38 +1055,67 @@ mod tests { assert!(perms .net .check( - &NetDescriptor("127.0.0.1".parse().unwrap(), Some(8000)), + &NetDescriptor(Host::must_parse("127.0.0.1"), Some(8000)), None ) .is_ok()); assert!(perms .net .check( - &NetDescriptor("127.0.0.1".parse().unwrap(), Some(8001)), + &NetDescriptor(Host::must_parse("127.0.0.1"), Some(8001)), None ) .is_err()); assert!(perms .net - .check(&NetDescriptor("127.0.0.1".parse().unwrap(), None), None) + .check(&NetDescriptor(Host::must_parse("127.0.0.1"), None), None) .is_err()); assert!(perms .net .check( - &NetDescriptor("deno.land".parse().unwrap(), Some(8000)), + &NetDescriptor(Host::must_parse("deno.land"), Some(8000)), None ) .is_err()); assert!(perms .net - .check(&NetDescriptor("deno.land".parse().unwrap(), None), None) + .check(&NetDescriptor(Host::must_parse("deno.land"), None), None) .is_err()); + #[allow(clippy::disallowed_methods)] + let cwd = std::env::current_dir().unwrap(); prompt_value.set(true); - assert!(perms.run.check("cat", None).is_ok()); + assert!(perms + .run + .check( + &RunQueryDescriptor::Path { + requested: "cat".to_string(), + resolved: cwd.join("cat") + }, + None + ) + .is_ok()); prompt_value.set(false); - assert!(perms.run.check("cat", None).is_ok()); - assert!(perms.run.check("ls", None).is_err()); + assert!(perms + .run + .check( + &RunQueryDescriptor::Path { + requested: "cat".to_string(), + resolved: cwd.join("cat") + }, + None + ) + .is_ok()); + assert!(perms + .run + .check( + &RunQueryDescriptor::Path { + requested: "ls".to_string(), + resolved: cwd.join("ls") + }, + None + ) + .is_err()); prompt_value.set(true); assert!(perms.env.check("HOME", None).is_ok()); @@ -1086,45 +1128,48 @@ mod tests { prompt_value.set(false); assert!(perms.env.check("hostname", None).is_ok()); assert!(perms.env.check("osRelease", None).is_err()); - - assert!(perms.hrtime.check().is_err()); } #[test] fn test_check_fail() { + let _locked = TESTMUTEX.lock(); 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 .net .check( - &NetDescriptor("127.0.0.1".parse().unwrap(), Some(8000)), + &NetDescriptor(Host::must_parse("127.0.0.1"), Some(8000)), None ) .is_err()); @@ -1132,21 +1177,21 @@ mod tests { assert!(perms .net .check( - &NetDescriptor("127.0.0.1".parse().unwrap(), Some(8000)), + &NetDescriptor(Host::must_parse("127.0.0.1"), Some(8000)), None ) .is_err()); assert!(perms .net .check( - &NetDescriptor("127.0.0.1".parse().unwrap(), Some(8001)), + &NetDescriptor(Host::must_parse("127.0.0.1"), Some(8001)), None ) .is_ok()); assert!(perms .net .check( - &NetDescriptor("deno.land".parse().unwrap(), Some(8000)), + &NetDescriptor(Host::must_parse("deno.land"), Some(8000)), None ) .is_ok()); @@ -1154,25 +1199,63 @@ mod tests { assert!(perms .net .check( - &NetDescriptor("127.0.0.1".parse().unwrap(), Some(8001)), + &NetDescriptor(Host::must_parse("127.0.0.1"), Some(8001)), None ) .is_ok()); assert!(perms .net .check( - &NetDescriptor("deno.land".parse().unwrap(), Some(8000)), + &NetDescriptor(Host::must_parse("deno.land"), Some(8000)), None ) .is_ok()); prompt_value.set(false); - assert!(perms.run.check("cat", None).is_err()); + #[allow(clippy::disallowed_methods)] + let cwd = std::env::current_dir().unwrap(); + assert!(perms + .run + .check( + &RunQueryDescriptor::Path { + requested: "cat".to_string(), + resolved: cwd.join("cat") + }, + None + ) + .is_err()); prompt_value.set(true); - assert!(perms.run.check("cat", None).is_err()); - assert!(perms.run.check("ls", None).is_ok()); + assert!(perms + .run + .check( + &RunQueryDescriptor::Path { + requested: "cat".to_string(), + resolved: cwd.join("cat") + }, + None + ) + .is_err()); + assert!(perms + .run + .check( + &RunQueryDescriptor::Path { + requested: "ls".to_string(), + resolved: cwd.join("ls") + }, + None + ) + .is_ok()); prompt_value.set(false); - assert!(perms.run.check("ls", None).is_ok()); + assert!(perms + .run + .check( + &RunQueryDescriptor::Path { + requested: "ls".to_string(), + resolved: cwd.join("ls") + }, + None + ) + .is_ok()); prompt_value.set(false); assert!(perms.env.check("HOME", None).is_err()); @@ -1183,28 +1266,30 @@ mod tests { assert!(perms.env.check("PATH", None).is_ok()); prompt_value.set(false); - assert!(perms.sys.check("hostname", None).is_err()); + let sys_desc = |name: &str| SysDescriptor::parse(name.to_string()).unwrap(); + assert!(perms.sys.check(&sys_desc("hostname"), None).is_err()); prompt_value.set(true); - assert!(perms.sys.check("hostname", None).is_err()); - assert!(perms.sys.check("osRelease", None).is_ok()); - prompt_value.set(false); - assert!(perms.sys.check("osRelease", None).is_ok()); - + assert!(perms.sys.check(&sys_desc("hostname"), None).is_err()); + assert!(perms.sys.check(&sys_desc("osRelease"), None).is_ok()); prompt_value.set(false); - assert!(perms.hrtime.check().is_err()); - prompt_value.set(true); - assert!(perms.hrtime.check().is_err()); + assert!(perms.sys.check(&sys_desc("osRelease"), None).is_ok()); } #[test] #[cfg(windows)] fn test_env_windows() { + let _locked = TESTMUTEX.lock(); set_prompter(Box::new(TestPrompter)); let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock(); let mut perms = Permissions::allow_all(); perms.env = UnaryPermission { granted_global: false, - ..Permissions::new_unary(&Some(svec!["HOME"]), &None, false).unwrap() + ..Permissions::new_unary( + Some(HashSet::from([EnvDescriptor::new("HOME")])), + None, + false, + ) + .unwrap() }; prompt_value.set(true); @@ -1218,75 +1303,67 @@ mod tests { #[test] fn test_check_partial_denied() { - let mut perms = Permissions { - read: Permissions::new_unary( - &Some(vec![]), - &Some(vec![PathBuf::from("/foo/bar")]), - false, - ) - .unwrap(), - write: Permissions::new_unary( - &Some(vec![]), - &Some(vec![PathBuf::from("/foo/bar")]), - false, - ) - .unwrap(), - ..Permissions::none_without_prompt() - }; + let _locked = TESTMUTEX.lock(); + 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(vec!["allowed.domain".to_string(), "1.1.1.1".to_string()]), - &Some(vec!["denied.domain".to_string(), "2.2.2.2".to_string()]), - false, - ) - .unwrap(), - ..Permissions::none_without_prompt() - }; + let _locked = TESTMUTEX.lock(); + 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); + set_prompter(Box::new(TestPrompter)); + let cases = [ + ("allowed.domain.", true), + ("1.1.1.1", true), + ("denied.domain.", false), + ("2.2.2.2", false), + ]; - perms - .net - .check( - &NetDescriptor("allowed.domain.".parse().unwrap(), None), - None, - ) - .unwrap(); - perms - .net - .check(&NetDescriptor("1.1.1.1".parse().unwrap(), None), None) - .unwrap(); - assert!(perms - .net - .check( - &NetDescriptor("denied.domain.".parse().unwrap(), None), - None - ) - .is_err()); - assert!(perms - .net - .check(&NetDescriptor("2.2.2.2".parse().unwrap(), None), None) - .is_err()); + for (host, is_ok) in cases { + assert_eq!(perms.check_net(&(host, None), "api").is_ok(), is_ok); + } } #[test] fn test_deserialize_child_permissions_arg() { + let _locked = TESTMUTEX.lock(); set_prompter(Box::new(TestPrompter)); assert_eq!( ChildPermissionsArg::inherit(), ChildPermissionsArg { env: ChildUnaryPermissionArg::Inherit, - hrtime: ChildUnitPermissionArg::Inherit, net: ChildUnaryPermissionArg::Inherit, ffi: ChildUnaryPermissionArg::Inherit, + import: ChildUnaryPermissionArg::Inherit, read: ChildUnaryPermissionArg::Inherit, run: ChildUnaryPermissionArg::Inherit, sys: ChildUnaryPermissionArg::Inherit, @@ -1297,9 +1374,9 @@ mod tests { ChildPermissionsArg::none(), ChildPermissionsArg { env: ChildUnaryPermissionArg::NotGranted, - hrtime: ChildUnitPermissionArg::NotGranted, net: ChildUnaryPermissionArg::NotGranted, ffi: ChildUnaryPermissionArg::NotGranted, + import: ChildUnaryPermissionArg::NotGranted, read: ChildUnaryPermissionArg::NotGranted, run: ChildUnaryPermissionArg::NotGranted, sys: ChildUnaryPermissionArg::NotGranted, @@ -1328,31 +1405,12 @@ mod tests { ..ChildPermissionsArg::none() } ); - assert_eq!( - serde_json::from_value::(json!({ - "hrtime": true, - })) - .unwrap(), - ChildPermissionsArg { - hrtime: ChildUnitPermissionArg::Granted, - ..ChildPermissionsArg::none() - } - ); - assert_eq!( - serde_json::from_value::(json!({ - "hrtime": false, - })) - .unwrap(), - ChildPermissionsArg { - hrtime: ChildUnitPermissionArg::NotGranted, - ..ChildPermissionsArg::none() - } - ); assert_eq!( serde_json::from_value::(json!({ "env": true, "net": true, "ffi": true, + "import": true, "read": true, "run": true, "sys": true, @@ -1363,11 +1421,11 @@ mod tests { env: ChildUnaryPermissionArg::Granted, net: ChildUnaryPermissionArg::Granted, ffi: ChildUnaryPermissionArg::Granted, + import: ChildUnaryPermissionArg::Granted, read: ChildUnaryPermissionArg::Granted, run: ChildUnaryPermissionArg::Granted, sys: ChildUnaryPermissionArg::Granted, write: ChildUnaryPermissionArg::Granted, - ..ChildPermissionsArg::none() } ); assert_eq!( @@ -1375,6 +1433,7 @@ mod tests { "env": false, "net": false, "ffi": false, + "import": false, "read": false, "run": false, "sys": false, @@ -1385,11 +1444,11 @@ mod tests { env: ChildUnaryPermissionArg::NotGranted, net: ChildUnaryPermissionArg::NotGranted, ffi: ChildUnaryPermissionArg::NotGranted, + import: ChildUnaryPermissionArg::NotGranted, read: ChildUnaryPermissionArg::NotGranted, run: ChildUnaryPermissionArg::NotGranted, sys: ChildUnaryPermissionArg::NotGranted, write: ChildUnaryPermissionArg::NotGranted, - ..ChildPermissionsArg::none() } ); assert_eq!( @@ -1397,6 +1456,7 @@ mod tests { "env": ["foo", "bar"], "net": ["foo", "bar:8000"], "ffi": ["foo", "file:///bar/baz"], + "import": ["example.com"], "read": ["foo", "file:///bar/baz"], "run": ["foo", "file:///bar/baz", "./qux"], "sys": ["hostname", "osRelease"], @@ -1407,140 +1467,141 @@ mod tests { env: ChildUnaryPermissionArg::GrantedList(svec!["foo", "bar"]), net: ChildUnaryPermissionArg::GrantedList(svec!["foo", "bar:8000"]), ffi: ChildUnaryPermissionArg::GrantedList(svec!["foo", "file:///bar/baz"]), + import: ChildUnaryPermissionArg::GrantedList(svec!["example.com"]), read: ChildUnaryPermissionArg::GrantedList(svec!["foo", "file:///bar/baz"]), run: ChildUnaryPermissionArg::GrantedList(svec!["foo", "file:///bar/baz", "./qux"]), sys: ChildUnaryPermissionArg::GrantedList(svec!["hostname", "osRelease"]), write: ChildUnaryPermissionArg::GrantedList(svec!["foo", "file:///bar/baz"]), - ..ChildPermissionsArg::none() } ); } #[test] fn test_create_child_permissions() { + let _locked = TESTMUTEX.lock(); + let parser = TestPermissionDescriptorParser; + let main_perms = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_env: Some(vec![]), + allow_net: Some(svec!["foo", "bar"]), + ..Default::default() + }, + ) + .unwrap(); + let main_perms = PermissionsContainer::new(Arc::new(parser), main_perms); set_prompter(Box::new(TestPrompter)); - let mut main_perms = Permissions { - env: Permissions::new_unary(&Some(vec![]), &None, false).unwrap(), - hrtime: Permissions::new_hrtime(true, false), - net: Permissions::new_unary(&Some(svec!["foo", "bar"]), &None, false).unwrap(), - ..Permissions::none_without_prompt() - }; assert_eq!( - create_child_permissions( - &mut main_perms.clone(), - ChildPermissionsArg { + main_perms + .create_child_permissions(ChildPermissionsArg { env: ChildUnaryPermissionArg::Inherit, - hrtime: ChildUnitPermissionArg::NotGranted, net: ChildUnaryPermissionArg::GrantedList(svec!["foo"]), ffi: ChildUnaryPermissionArg::NotGranted, ..ChildPermissionsArg::none() - } - ) - .unwrap(), + }) + .unwrap() + .inner + .lock() + .clone(), Permissions { - env: Permissions::new_unary(&Some(vec![]), &None, false).unwrap(), - net: Permissions::new_unary(&Some(svec!["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( - &mut main_perms.clone(), - ChildPermissionsArg { + assert!(main_perms + .create_child_permissions(ChildPermissionsArg { net: ChildUnaryPermissionArg::Granted, ..ChildPermissionsArg::none() - } - ) - .is_err()); - assert!(create_child_permissions( - &mut main_perms.clone(), - ChildPermissionsArg { + }) + .is_err()); + assert!(main_perms + .create_child_permissions(ChildPermissionsArg { net: ChildUnaryPermissionArg::GrantedList(svec!["foo", "bar", "baz"]), ..ChildPermissionsArg::none() - } - ) - .is_err()); - assert!(create_child_permissions( - &mut main_perms, - ChildPermissionsArg { + }) + .is_err()); + assert!(main_perms + .create_child_permissions(ChildPermissionsArg { ffi: ChildUnaryPermissionArg::GrantedList(svec!["foo"]), ..ChildPermissionsArg::none() - } - ) - .is_err()); + }) + .is_err()); } #[test] fn test_create_child_permissions_with_prompt() { - set_prompter(Box::new(TestPrompter)); + let _locked = TESTMUTEX.lock(); let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock(); - let mut main_perms = Permissions::from_options(&PermissionsOptions { - prompt: true, - ..Default::default() - }) + let main_perms = Permissions::from_options( + &TestPermissionDescriptorParser, + &PermissionsOptions { + prompt: true, + ..Default::default() + }, + ) .unwrap(); + let main_perms = + PermissionsContainer::new(Arc::new(TestPermissionDescriptorParser), main_perms); + set_prompter(Box::new(TestPrompter)); prompt_value.set(true); - let worker_perms = create_child_permissions( - &mut main_perms, - ChildPermissionsArg { + let worker_perms = main_perms + .create_child_permissions(ChildPermissionsArg { read: ChildUnaryPermissionArg::Granted, run: ChildUnaryPermissionArg::GrantedList(svec!["foo", "bar"]), ..ChildPermissionsArg::none() - }, - ) - .unwrap(); - assert_eq!(main_perms, worker_perms); + }) + .unwrap(); assert_eq!( - main_perms.run.granted_list, + main_perms.0.inner.lock().clone(), + worker_perms.inner.lock().clone() + ); + assert_eq!( + main_perms.0.inner.lock().run.granted_list, HashSet::from([ - RunDescriptor::Name("bar".to_owned()), - RunDescriptor::Name("foo".to_owned()) + AllowRunDescriptor(PathBuf::from("/bar")), + AllowRunDescriptor(PathBuf::from("/foo")), ]) ); } #[test] fn test_create_child_permissions_with_inherited_denied_list() { - set_prompter(Box::new(TestPrompter)); + let _locked = TESTMUTEX.lock(); 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 main_perms = Permissions::from_options( + &parser, + &PermissionsOptions { + prompt: true, + ..Default::default() + }, + ) .unwrap(); + let main_perms = PermissionsContainer::new(Arc::new(parser.clone()), main_perms); + set_prompter(Box::new(TestPrompter)); prompt_value.set(false); - assert!(main_perms.write.check(&PathBuf::from("foo"), None).is_err()); - let worker_perms = - create_child_permissions(&mut main_perms.clone(), ChildPermissionsArg::none()).unwrap(); + assert!(main_perms + .0 + .inner + .lock() + .write + .check(&parser.parse_path_query("foo").unwrap().into_write(), None) + .is_err()); + let worker_perms = main_perms + .create_child_permissions(ChildPermissionsArg::none()) + .unwrap(); assert_eq!( - worker_perms.write.flag_denied_list, - main_perms.write.flag_denied_list + worker_perms.inner.lock().write.flag_denied_list.clone(), + main_perms.0.inner.lock().write.flag_denied_list ); } - #[test] - fn test_handle_empty_value() { - set_prompter(Box::new(TestPrompter)); - - assert!(Permissions::new_unary::( - &Some(vec![Default::default()]), - &None, - false - ) - .is_err()); - assert!(Permissions::new_unary::( - &Some(vec![Default::default()]), - &None, - false - ) - .is_err()); - assert!(Permissions::new_unary::( - &Some(vec![Default::default()]), - &None, - false - ) - .is_err()); - } - #[test] fn test_host_parse() { let hosts = &[ @@ -1576,7 +1637,7 @@ mod tests { ]; for (host_str, expected) in hosts { - assert_eq!(host_str.parse::().ok(), *expected, "{host_str}"); + assert_eq!(Host::parse(host_str).ok(), *expected, "{host_str}"); } } @@ -1642,7 +1703,38 @@ mod tests { ]; for (input, expected) in cases { - assert_eq!(input.parse::().ok(), *expected, "'{input}'"); + 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/crates/deno/src/prompter.rs b/crates/deno/src/prompter.rs index 7726965..2090eed 100644 --- a/crates/deno/src/prompter.rs +++ b/crates/deno/src/prompter.rs @@ -16,8 +16,8 @@ use bls_permissions::bls_set_prompter; pub use bls_permissions::PermissionPrompter; pub use bls_permissions::PromptCallback; pub use bls_permissions::PromptResponse; -pub use bls_permissions::PERMISSION_EMOJI; pub use bls_permissions::MAX_PERMISSION_PROMPT_LENGTH; +pub use bls_permissions::PERMISSION_EMOJI; /// Helper function to make control characters visible so users can see the underlying filename. fn escape_control_characters(s: &str) -> std::borrow::Cow { @@ -40,7 +40,7 @@ fn escape_control_characters(s: &str) -> std::borrow::Cow { pub fn init_tty_prompter() { static TTYPROMPTER: Once = Once::new(); TTYPROMPTER.call_once(|| { - bls_set_prompter(Box::new(TtyPrompter)); + set_prompter(Box::new(TtyPrompter)); }); } @@ -48,6 +48,10 @@ pub fn set_prompt_callbacks(before_callback: PromptCallback, after_callback: Pro bls_set_prompt_callbacks(before_callback, after_callback); } +pub fn set_prompter(prompter: Box) { + bls_set_prompter(prompter); +} + pub struct TtyPrompter; #[cfg(unix)] fn clear_stdin(_stdin_lock: &mut StdinLock, _stderr_lock: &mut StderrLock) -> Result<(), AnyError> { @@ -62,7 +66,6 @@ fn clear_stdin(_stdin_lock: &mut StdinLock, _stderr_lock: &mut StderrLock) -> Re let mut raw_fd_set = MaybeUninit::::uninit(); libc::FD_ZERO(raw_fd_set.as_mut_ptr()); libc::FD_SET(STDIN_FD, raw_fd_set.as_mut_ptr()); - loop { let r = libc::tcflush(STDIN_FD, libc::TCIFLUSH); if r != 0 { @@ -311,7 +314,7 @@ impl PermissionPrompter for TtyPrompter { let mut input = String::new(); let result = stdin_lock.read_line(&mut input); - let input = input.trim_end_matches(|c| c == '\r' || c == '\n'); + let input = input.trim_end_matches(['\r', '\n']); if result.is_err() || input.len() != 1 { break PromptResponse::Deny; }; @@ -377,9 +380,10 @@ impl PermissionPrompter for TtyPrompter { #[cfg(test)] pub mod tests { - use super::*; use deno_core::parking_lot::Mutex; use once_cell::sync::Lazy; + + use super::*; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; @@ -413,8 +417,4 @@ pub mod tests { STUB_PROMPT_VALUE.store(value, Ordering::SeqCst); } } - - pub fn set_prompter(prompter: Box) { - bls_set_prompter(prompter); - } }