diff options
author | David Sherret <dsherret@users.noreply.github.com> | 2024-09-16 21:39:37 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-09-16 21:39:37 +0100 |
commit | 62e952559f600e72d7498c9b12f906cb0b1ba150 (patch) | |
tree | 6dbcce6592973358ef4bf6341888b0bbbdb98cc5 /runtime/permissions/lib.rs | |
parent | e0b9c745c15720914f14996bf357d5b375e2dbd8 (diff) |
refactor(permissions): split up Descriptor into Allow, Deny, and Query (#25508)
This makes the permission system more versatile.
Diffstat (limited to 'runtime/permissions/lib.rs')
-rw-r--r-- | runtime/permissions/lib.rs | 2619 |
1 files changed, 1585 insertions, 1034 deletions
diff --git a/runtime/permissions/lib.rs b/runtime/permissions/lib.rs index 36750ae38..ad84b9fc3 100644 --- a/runtime/permissions/lib.rs +++ b/runtime/permissions/lib.rs @@ -1,5 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use deno_core::anyhow::bail; use deno_core::anyhow::Context; use deno_core::error::custom_error; use deno_core::error::type_error; @@ -51,14 +52,11 @@ macro_rules! skip_check_if_is_permission_fully_granted { } #[inline] -fn resolve_from_cwd(path: &Path) -> Result<PathBuf, AnyError> { +fn resolve_from_known_cwd(path: &Path, cwd: &Path) -> PathBuf { if path.is_absolute() { - Ok(normalize_path(path)) + normalize_path(path) } else { - #[allow(clippy::disallowed_methods)] - let cwd = std::env::current_dir() - .context("Failed to get current working directory")?; - Ok(normalize_path(cwd.join(path))) + normalize_path(cwd.join(path)) } } @@ -310,12 +308,17 @@ impl AsRef<str> for EnvVarName { } } -pub trait Descriptor: Eq + Clone + Hash { - type Arg: From<String>; +pub trait QueryDescriptor: Debug { + type AllowDesc: Debug + Eq + Clone + Hash; + type DenyDesc: Debug + Eq + Clone + Hash; + + fn flag_name() -> &'static str; + fn display_name(&self) -> Cow<str>; + + 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<&[Self::Arg]>) -> Result<HashSet<Self>, AnyError>; + fn as_allow(&self) -> Option<Self::AllowDesc>; + fn as_deny(&self) -> Self::DenyDesc; /// Generic check function to check this descriptor against a `UnaryPermission`. fn check_in_permission( @@ -324,27 +327,35 @@ pub trait Descriptor: Eq + Clone + Hash { api_name: Option<&str>, ) -> Result<(), AnyError>; - fn flag_name() -> &'static str; - fn name(&self) -> Cow<str>; - // By default, specifies no-stronger-than relationship. - // As this is not strict, it's only true when descriptors are the same. - fn stronger_than(&self, other: &Self) -> bool { - self == other + fn matches_allow(&self, other: &Self::AllowDesc) -> bool; + fn matches_deny(&self, other: &Self::DenyDesc) -> bool; + + /// Gets if this query descriptor should revoke the provided allow descriptor. + fn revokes(&self, other: &Self::AllowDesc) -> bool; + fn stronger_than_deny(&self, other: &Self::DenyDesc) -> bool; + fn overlaps_deny(&self, other: &Self::DenyDesc) -> bool; +} + +fn format_display_name(display_name: Cow<str>) -> String { + if display_name.starts_with('<') && display_name.ends_with('>') { + display_name.into_owned() + } else { + format!("\"{}\"", display_name) } } -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct UnaryPermission<T: Descriptor + Hash> { +#[derive(Debug, Eq, PartialEq)] +pub struct UnaryPermission<TQuery: QueryDescriptor + ?Sized> { granted_global: bool, - granted_list: HashSet<T>, + granted_list: HashSet<TQuery::AllowDesc>, flag_denied_global: bool, - flag_denied_list: HashSet<T>, + flag_denied_list: HashSet<TQuery::DenyDesc>, prompt_denied_global: bool, - prompt_denied_list: HashSet<T>, + prompt_denied_list: HashSet<TQuery::DenyDesc>, prompt: bool, } -impl<T: Descriptor + Hash> Default for UnaryPermission<T> { +impl<TQuery: QueryDescriptor> Default for UnaryPermission<TQuery> { fn default() -> Self { UnaryPermission { granted_global: Default::default(), @@ -358,7 +369,21 @@ impl<T: Descriptor + Hash> Default for UnaryPermission<T> { } } -impl<T: Descriptor + Hash> UnaryPermission<T> { +impl<TQuery: QueryDescriptor> Clone for UnaryPermission<TQuery> { + 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<TQuery: QueryDescriptor> UnaryPermission<TQuery> { pub fn allow_all() -> Self { Self { granted_global: true, @@ -377,26 +402,21 @@ impl<T: Descriptor + Hash> UnaryPermission<T> { 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<String>, ) -> Result<(), AnyError> { - skip_check_if_is_permission_fully_granted!(self); let (result, prompted, is_allow_all) = self .query_desc(desc, AllowPartial::from(!assert_non_partial)) .check2( - T::flag_name(), + TQuery::flag_name(), api_name, - || match get_display_name() { - Some(display_name) => Some(display_name), - None => desc.map(|d| format!("\"{}\"", d.name())), - }, + || desc.map(|d| format_display_name(d.display_name())), self.prompt, ); if prompted { @@ -404,10 +424,10 @@ impl<T: Descriptor + Hash> UnaryPermission<T> { if is_allow_all { self.insert_granted(None); } else { - self.insert_granted(desc.cloned()); + self.insert_granted(desc); } } else { - self.insert_prompt_denied(desc.cloned()); + self.insert_prompt_denied(desc.map(|d| d.as_deny())); } } result @@ -415,7 +435,7 @@ impl<T: Descriptor + Hash> UnaryPermission<T> { fn query_desc( &self, - desc: Option<&T>, + desc: Option<&TQuery>, allow_partial: AllowPartial, ) -> PermissionState { if self.is_flag_denied(desc) || self.is_prompt_denied(desc) { @@ -447,43 +467,33 @@ impl<T: Descriptor + Hash> UnaryPermission<T> { } } - fn request_desc( - &mut self, - desc: Option<&T>, - get_display_name: impl Fn() -> Option<String>, - ) -> PermissionState { + fn request_desc(&mut self, desc: Option<&TQuery>) -> PermissionState { let state = self.query_desc(desc, AllowPartial::TreatAsPartialGranted); if state == PermissionState::Granted { - self.insert_granted(desc.cloned()); + self.insert_granted(desc); return state; } if state != PermissionState::Prompt { return state; } let mut message = String::with_capacity(40); - message.push_str(&format!("{} access", T::flag_name())); - match get_display_name() { - Some(display_name) => { - message.push_str(&format!(" to \"{}\"", display_name)) - } - None => { - if let Some(desc) = desc { - message.push_str(&format!(" to \"{}\"", desc.name())); - } - } + message.push_str(&format!("{} access", TQuery::flag_name())); + if let Some(desc) = desc { + message + .push_str(&format!(" to {}", format_display_name(desc.display_name()))); } match permission_prompt( &message, - T::flag_name(), + TQuery::flag_name(), Some("Deno.permissions.request()"), true, ) { PromptResponse::Allow => { - self.insert_granted(desc.cloned()); + self.insert_granted(desc); PermissionState::Granted } PromptResponse::Deny => { - self.insert_prompt_denied(desc.cloned()); + self.insert_prompt_denied(desc.map(|d| d.as_deny())); PermissionState::Denied } PromptResponse::AllowAll => { @@ -493,10 +503,10 @@ impl<T: Descriptor + Hash> UnaryPermission<T> { } } - fn revoke_desc(&mut self, desc: Option<&T>) -> PermissionState { + fn revoke_desc(&mut self, desc: Option<&TQuery>) -> PermissionState { match desc { Some(desc) => { - self.granted_list.retain(|v| !v.stronger_than(desc)); + self.granted_list.retain(|v| !desc.revokes(v)); } None => { self.granted_global = false; @@ -509,47 +519,61 @@ impl<T: Descriptor + Hash> UnaryPermission<T> { 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<T>, - ) -> 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<T>) { + 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<T>) { + fn insert_prompt_denied(&mut self, desc: Option<TQuery::DenyDesc>) { Self::list_insert( desc, &mut self.prompt_denied_global, @@ -557,7 +581,7 @@ impl<T: Descriptor + Hash> UnaryPermission<T> { ); } - fn list_insert( + fn list_insert<T: Hash + Eq>( desc: Option<T>, list_global: &mut bool, list: &mut HashSet<T>, @@ -573,7 +597,8 @@ impl<T: Descriptor + Hash> UnaryPermission<T> { fn create_child_permissions( &mut self, flag: ChildUnaryPermissionArg, - ) -> Result<UnaryPermission<T>, AnyError> { + parse: impl Fn(&str) -> Result<Option<TQuery::AllowDesc>, AnyError>, + ) -> Result<UnaryPermission<TQuery>, AnyError> { let mut perms = Self::default(); match flag { @@ -588,14 +613,15 @@ impl<T: Descriptor + Hash> UnaryPermission<T> { } ChildUnaryPermissionArg::NotGranted => {} ChildUnaryPermissionArg::GrantedList(granted_list) => { - let granted: Vec<T::Arg> = - 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::<Result<_, _>>()?; + if !perms.granted_list.iter().all(|desc| { + TQuery::from_allow(desc) + .check_in_permission(self, None) + .is_ok() + }) { return Err(escalation_error()); } } @@ -613,10 +639,55 @@ impl<T: Descriptor + Hash> UnaryPermission<T> { } #[derive(Clone, Eq, PartialEq, Hash, Debug)] -pub struct ReadDescriptor(pub PathBuf); +pub struct PathQueryDescriptor { + pub requested: String, + pub resolved: PathBuf, +} -impl Descriptor for ReadDescriptor { - type Arg = PathBuf; +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<str> { + 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<Self::AllowDesc> { + Some(ReadDescriptor(self.0.resolved.clone())) + } + + fn as_deny(&self) -> Self::DenyDesc { + ReadDescriptor(self.0.resolved.clone()) + } fn check_in_permission( &self, @@ -624,31 +695,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<&[Self::Arg]>) -> Result<HashSet<Self>, 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 name(&self) -> Cow<str> { - Cow::from(self.0.display().to_string()) + fn revokes(&self, other: &Self::AllowDesc) -> bool { + self.matches_allow(other) } - fn stronger_than(&self, other: &Self) -> bool { - other.0.starts_with(&self.0) + fn stronger_than_deny(&self, other: &Self::DenyDesc) -> bool { + other.0.starts_with(&self.0.resolved) + } + + fn overlaps_deny(&self, other: &Self::DenyDesc) -> bool { + self.stronger_than_deny(other) } } #[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 Descriptor for WriteDescriptor { - type Arg = PathBuf; +impl QueryDescriptor for WriteQueryDescriptor { + type AllowDesc = WriteDescriptor; + type DenyDesc = WriteDescriptor; + + fn flag_name() -> &'static str { + "write" + } + + fn display_name(&self) -> Cow<str> { + 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<Self::AllowDesc> { + Some(WriteDescriptor(self.0.resolved.clone())) + } + + fn as_deny(&self) -> Self::DenyDesc { + WriteDescriptor(self.0.resolved.clone()) + } fn check_in_permission( &self, @@ -656,27 +758,34 @@ impl Descriptor for WriteDescriptor { api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(perm); - perm.check_desc(Some(self), true, api_name, || None) + perm.check_desc(Some(self), true, api_name) } - fn parse(args: Option<&[Self::Arg]>) -> Result<HashSet<Self>, 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 name(&self) -> Cow<str> { - Cow::from(self.0.display().to_string()) + fn revokes(&self, other: &Self::AllowDesc) -> bool { + self.matches_allow(other) } - fn stronger_than(&self, other: &Self) -> bool { - other.0.starts_with(&self.0) + fn stronger_than_deny(&self, other: &Self::DenyDesc) -> bool { + other.0.starts_with(&self.0.resolved) + } + + fn overlaps_deny(&self, other: &Self::DenyDesc) -> bool { + self.stronger_than_deny(other) } } #[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub struct WriteDescriptor(pub PathBuf); + +#[derive(Clone, Eq, PartialEq, Hash, Debug)] pub enum Host { Fqdn(FQDN), Ip(IpAddr), @@ -728,8 +837,29 @@ impl Host { #[derive(Clone, Eq, PartialEq, Hash, Debug)] pub struct NetDescriptor(pub Host, pub Option<u16>); -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<str> { + Cow::from(format!("{}", self)) + } + + fn from_allow(allow: &Self::AllowDesc) -> Self { + allow.clone() + } + + fn as_allow(&self) -> Option<Self::AllowDesc> { + Some(self.clone()) + } + + fn as_deny(&self) -> Self::DenyDesc { + self.clone() + } fn check_in_permission( &self, @@ -737,23 +867,27 @@ impl Descriptor for NetDescriptor { api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(perm); - perm.check_desc(Some(self), false, api_name, || None) + perm.check_desc(Some(self), false, api_name) } - fn parse(args: Option<&[Self::Arg]>) -> Result<HashSet<Self>, 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<str> { - Cow::from(format!("{}", self)) + fn revokes(&self, other: &Self::AllowDesc) -> bool { + self.matches_allow(other) + } + + fn stronger_than_deny(&self, other: &Self::DenyDesc) -> bool { + self.matches_deny(other) } - fn stronger_than(&self, other: &Self) -> bool { - self.0 == other.0 && (self.1.is_none() || self.1 == other.1) + fn overlaps_deny(&self, _other: &Self::DenyDesc) -> bool { + false } } @@ -837,8 +971,29 @@ impl EnvDescriptor { } } -impl Descriptor for EnvDescriptor { - type Arg = String; +impl QueryDescriptor for EnvDescriptor { + type AllowDesc = EnvDescriptor; + type DenyDesc = EnvDescriptor; + + fn flag_name() -> &'static str { + "env" + } + + fn display_name(&self) -> Cow<str> { + Cow::from(self.0.as_ref()) + } + + fn from_allow(allow: &Self::AllowDesc) -> Self { + allow.clone() + } + + fn as_allow(&self) -> Option<Self::AllowDesc> { + Some(self.clone()) + } + + fn as_deny(&self) -> Self::DenyDesc { + self.clone() + } fn check_in_permission( &self, @@ -846,19 +1001,27 @@ impl Descriptor for EnvDescriptor { api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(perm); - perm.check_desc(Some(self), false, api_name, || None) + perm.check_desc(Some(self), false, api_name) } - fn parse(list: Option<&[Self::Arg]>) -> Result<HashSet<Self>, AnyError> { - parse_env_list(list) + fn matches_allow(&self, other: &Self::AllowDesc) -> bool { + self == other } - fn flag_name() -> &'static str { - "env" + fn matches_deny(&self, other: &Self::DenyDesc) -> bool { + self == other } - fn name(&self) -> Cow<str> { - Cow::from(self.0.as_ref()) + fn revokes(&self, other: &Self::AllowDesc) -> bool { + self == other + } + + fn stronger_than_deny(&self, other: &Self::DenyDesc) -> bool { + self == other + } + + fn overlaps_deny(&self, _other: &Self::DenyDesc) -> bool { + false } } @@ -869,46 +1032,94 @@ impl AsRef<str> for EnvDescriptor { } #[derive(Clone, Eq, PartialEq, Hash, Debug, Serialize, Deserialize)] -pub struct RunPathQuery<'a> { - pub requested: &'a str, - pub resolved: &'a Path, -} - -pub enum RunDescriptorArg { - Name(String), - Path(PathBuf), -} - -#[derive(Clone, Eq, PartialEq, Hash, Debug)] -pub enum RunDescriptor { - /// Warning: You may want to construct with `RunDescriptor::from()` for case - /// handling. +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 From<String> for RunDescriptorArg { - fn from(s: String) -> Self { - #[cfg(windows)] - let s = s.to_lowercase(); - let is_path = s.contains('/'); - #[cfg(windows)] - let is_path = is_path || s.contains('\\') || Path::new(&s).is_absolute(); - if is_path { - Self::Path(resolve_from_cwd(Path::new(&s)).unwrap()) +impl RunQueryDescriptor { + pub fn parse(requested: &str) -> Result<RunQueryDescriptor, AnyError> { + if is_path(requested) { + let path = PathBuf::from(requested); + let resolved = if path.is_absolute() { + normalize_path(path) + } else { + let cwd = std::env::current_dir().context("failed resolving cwd")?; + normalize_path(cwd.join(path)) + }; + Ok(RunQueryDescriptor::Path { + requested: requested.to_string(), + resolved, + }) } else { - match which::which(&s) { - Ok(path) => Self::Path(path), - Err(_) => Self::Name(s), + match which::which(requested) { + Ok(resolved) => Ok(RunQueryDescriptor::Path { + requested: requested.to_string(), + resolved, + }), + Err(_) => Ok(RunQueryDescriptor::Name(requested.to_string())), } } } } -impl Descriptor for RunDescriptor { - type Arg = RunDescriptorArg; +impl QueryDescriptor for RunQueryDescriptor { + type AllowDesc = AllowRunDescriptor; + type DenyDesc = DenyRunDescriptor; + + fn flag_name() -> &'static str { + "run" + } + + fn display_name(&self) -> Cow<str> { + 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<Self::AllowDesc> { + 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, @@ -916,71 +1127,180 @@ 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) + perm.check_desc(Some(self), false, api_name) } - fn parse(args: Option<&[Self::Arg]>) -> Result<HashSet<Self>, AnyError> { - parse_run_list(args) + fn matches_allow(&self, other: &Self::AllowDesc) -> bool { + match self { + RunQueryDescriptor::Path { resolved, .. } => *resolved == other.0, + RunQueryDescriptor::Name(_) => false, + } } - fn flag_name() -> &'static str { - "run" + fn matches_deny(&self, other: &Self::DenyDesc) -> bool { + match other { + DenyRunDescriptor::Name(deny_desc) => match self { + RunQueryDescriptor::Path { resolved, .. } => { + denies_run_name(deny_desc, resolved) + } + RunQueryDescriptor::Name(query) => query == deny_desc, + }, + DenyRunDescriptor::Path(deny_desc) => match self { + RunQueryDescriptor::Path { resolved, .. } => { + resolved.starts_with(deny_desc) + } + RunQueryDescriptor::Name(query) => denies_run_name(query, deny_desc), + }, + } + } + + fn revokes(&self, other: &Self::AllowDesc) -> bool { + match self { + RunQueryDescriptor::Path { + resolved, + requested, + } => { + if *resolved == other.0 { + return true; + } + if is_path(requested) { + false + } else { + denies_run_name(requested, &other.0) + } + } + RunQueryDescriptor::Name(query) => denies_run_name(query, &other.0), + } } - fn name(&self) -> Cow<str> { - Cow::from(self.to_string()) + 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. + Unresolved(Box<which::Error>), + Descriptor(AllowRunDescriptor), } -impl From<String> 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 { + pub fn parse( + text: &str, + cwd: &Path, + ) -> Result<AllowRunDescriptorParseResult, which::Error> { + let is_path = is_path(text); + // todo(dsherret): canonicalize in #25458 + let path = if is_path { + resolve_from_known_cwd(Path::new(text), cwd) } else { - match which::which(&s) { - Ok(path) => Self::Path(path), - Err(_) => 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), + )) } } -impl From<PathBuf> for RunDescriptor { - fn from(p: PathBuf) -> Self { - #[cfg(windows)] - let p = PathBuf::from(p.to_string_lossy().to_string().to_lowercase()); - Self::Path(resolve_from_cwd(&p).unwrap()) - } +#[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 std::fmt::Display for RunDescriptor { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - RunDescriptor::Name(s) => f.write_str(s), - RunDescriptor::Path(p) => f.write_str(&p.display().to_string()), +impl 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 { + DenyRunDescriptor::Name(text.to_string()) } } } -impl AsRef<Path> for RunDescriptor { - fn as_ref(&self) -> &Path { - match self { - RunDescriptor::Name(s) => s.as_ref(), - RunDescriptor::Path(s) => s.as_ref(), - } +fn is_path(text: &str) -> bool { + if cfg!(windows) { + text.contains('/') || text.contains('\\') || Path::new(text).is_absolute() + } else { + text.contains('/') + } +} + +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(pub String); -impl Descriptor for SysDescriptor { - type Arg = String; +impl QueryDescriptor for SysDescriptor { + type AllowDesc = SysDescriptor; + type DenyDesc = SysDescriptor; + + fn flag_name() -> &'static str { + "sys" + } + + fn display_name(&self) -> Cow<str> { + Cow::from(self.0.to_string()) + } + + fn from_allow(allow: &Self::AllowDesc) -> Self { + allow.clone() + } + + fn as_allow(&self) -> Option<Self::AllowDesc> { + Some(self.clone()) + } + + fn as_deny(&self) -> Self::DenyDesc { + self.clone() + } fn check_in_permission( &self, @@ -988,19 +1308,27 @@ impl Descriptor for SysDescriptor { api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(perm); - perm.check_desc(Some(self), false, api_name, || None) + perm.check_desc(Some(self), false, api_name) } - fn parse(list: Option<&[Self::Arg]>) -> Result<HashSet<Self>, 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<str> { - Cow::from(self.0.to_string()) + fn revokes(&self, other: &Self::AllowDesc) -> bool { + self == other + } + + fn stronger_than_deny(&self, other: &Self::DenyDesc) -> bool { + self == other + } + + fn overlaps_deny(&self, _other: &Self::DenyDesc) -> bool { + false } } @@ -1014,10 +1342,35 @@ pub fn parse_sys_kind(kind: &str) -> Result<&str, AnyError> { } #[derive(Clone, Eq, PartialEq, Hash, Debug)] -pub struct FfiDescriptor(pub PathBuf); +pub struct FfiQueryDescriptor(pub PathQueryDescriptor); + +impl QueryDescriptor for FfiQueryDescriptor { + type AllowDesc = FfiDescriptor; + type DenyDesc = FfiDescriptor; + + fn flag_name() -> &'static str { + "ffi" + } + + fn display_name(&self) -> Cow<str> { + Cow::Borrowed(&self.0.requested) + } -impl Descriptor for FfiDescriptor { - type Arg = PathBuf; + 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<Self::AllowDesc> { + Some(FfiDescriptor(self.0.resolved.clone())) + } + + fn as_deny(&self) -> Self::DenyDesc { + FfiDescriptor(self.0.resolved.clone()) + } fn check_in_permission( &self, @@ -1025,175 +1378,118 @@ impl Descriptor for FfiDescriptor { api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(perm); - perm.check_desc(Some(self), true, api_name, || None) + perm.check_desc(Some(self), true, api_name) } - fn parse(list: Option<&[Self::Arg]>) -> Result<HashSet<Self>, 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 revokes(&self, other: &Self::AllowDesc) -> bool { + self.matches_allow(other) } - fn name(&self) -> Cow<str> { - 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) } } -impl UnaryPermission<ReadDescriptor> { - pub fn query(&self, path: Option<&Path>) -> PermissionState { - self.query_desc( - path - .map(|p| ReadDescriptor(resolve_from_cwd(p).unwrap())) - .as_ref(), - AllowPartial::TreatAsPartialGranted, - ) +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub struct FfiDescriptor(pub PathBuf); + +impl UnaryPermission<ReadQueryDescriptor> { + pub fn query(&self, desc: Option<&ReadQueryDescriptor>) -> PermissionState { + self.query_desc(desc, AllowPartial::TreatAsPartialGranted) } - pub fn request(&mut self, path: Option<&Path>) -> PermissionState { - self.request_desc( - path - .map(|p| ReadDescriptor(resolve_from_cwd(p).unwrap())) - .as_ref(), - || Some(path?.display().to_string()), - ) + pub fn request( + &mut self, + path: Option<&ReadQueryDescriptor>, + ) -> PermissionState { + self.request_desc(path) } - pub fn revoke(&mut self, path: Option<&Path>) -> PermissionState { - self.revoke_desc( - path - .map(|p| ReadDescriptor(resolve_from_cwd(p).unwrap())) - .as_ref(), - ) + pub fn revoke( + &mut self, + desc: Option<&ReadQueryDescriptor>, + ) -> PermissionState { + self.revoke_desc(desc) } pub fn check( &mut self, - path: &Path, + desc: &ReadQueryDescriptor, api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc( - Some(&ReadDescriptor(resolve_from_cwd(path)?)), - true, - api_name, - || Some(format!("\"{}\"", path.display())), - ) + self.check_desc(Some(desc), true, api_name) } #[inline] pub fn check_partial( &mut self, - path: &Path, + desc: &ReadQueryDescriptor, api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - let desc = ReadDescriptor(resolve_from_cwd(path)?); - self.check_desc(Some(&desc), false, api_name, || { - Some(format!("\"{}\"", path.display())) - }) - } - - /// As `check()`, but permission error messages will anonymize the path - /// by replacing it with the given `display`. - pub fn check_blind( - &mut self, - path: &Path, - display: &str, - api_name: &str, - ) -> Result<(), AnyError> { - skip_check_if_is_permission_fully_granted!(self); - let desc = ReadDescriptor(resolve_from_cwd(path)?); - self.check_desc(Some(&desc), false, Some(api_name), || { - Some(format!("<{display}>")) - }) + self.check_desc(Some(desc), false, api_name) } pub fn check_all(&mut self, api_name: Option<&str>) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc(None, false, api_name, || None) + self.check_desc(None, false, api_name) } } -impl UnaryPermission<WriteDescriptor> { - 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<WriteQueryDescriptor> { + pub fn query(&self, path: Option<&WriteQueryDescriptor>) -> PermissionState { + self.query_desc(path, AllowPartial::TreatAsPartialGranted) } - pub fn request(&mut self, path: Option<&Path>) -> PermissionState { - self.request_desc( - path - .map(|p| WriteDescriptor(resolve_from_cwd(p).unwrap())) - .as_ref(), - || Some(path?.display().to_string()), - ) + pub fn request( + &mut self, + path: Option<&WriteQueryDescriptor>, + ) -> PermissionState { + self.request_desc(path) } - pub fn revoke(&mut self, path: Option<&Path>) -> PermissionState { - self.revoke_desc( - path - .map(|p| WriteDescriptor(resolve_from_cwd(p).unwrap())) - .as_ref(), - ) + pub fn revoke( + &mut self, + path: Option<&WriteQueryDescriptor>, + ) -> PermissionState { + self.revoke_desc(path) } pub fn check( &mut self, - path: &Path, + path: &WriteQueryDescriptor, api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc( - Some(&WriteDescriptor(resolve_from_cwd(path)?)), - true, - api_name, - || Some(format!("\"{}\"", path.display())), - ) + self.check_desc(Some(path), true, api_name) } #[inline] pub fn check_partial( &mut self, - path: &Path, + path: &WriteQueryDescriptor, api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc( - Some(&WriteDescriptor(resolve_from_cwd(path)?)), - false, - api_name, - || Some(format!("\"{}\"", path.display())), - ) - } - - /// As `check()`, but permission error messages will anonymize the path - /// by replacing it with the given `display`. - pub fn check_blind( - &mut self, - path: &Path, - display: &str, - api_name: &str, - ) -> Result<(), AnyError> { - skip_check_if_is_permission_fully_granted!(self); - let desc = WriteDescriptor(resolve_from_cwd(path)?); - self.check_desc(Some(&desc), false, Some(api_name), || { - Some(format!("<{display}>")) - }) + self.check_desc(Some(path), false, api_name) } pub fn check_all(&mut self, api_name: Option<&str>) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc(None, false, api_name, || None) + self.check_desc(None, false, api_name) } } @@ -1203,7 +1499,7 @@ impl UnaryPermission<NetDescriptor> { } pub fn request(&mut self, host: Option<&NetDescriptor>) -> PermissionState { - self.request_desc(host, || None) + self.request_desc(host) } pub fn revoke(&mut self, host: Option<&NetDescriptor>) -> PermissionState { @@ -1216,7 +1512,7 @@ impl UnaryPermission<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( @@ -1231,14 +1527,12 @@ impl UnaryPermission<NetDescriptor> { let host = Host::parse(host)?; let port = url.port_or_known_default(); let descriptor = NetDescriptor(host, port); - self.check_desc(Some(&descriptor), false, api_name, || { - Some(format!("\"{descriptor}\"")) - }) + self.check_desc(Some(&descriptor), false, api_name) } pub fn check_all(&mut self) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc(None, false, None, || None) + self.check_desc(None, false, None) } } @@ -1251,7 +1545,7 @@ impl UnaryPermission<EnvDescriptor> { } pub fn request(&mut self, env: Option<&str>) -> PermissionState { - self.request_desc(env.map(EnvDescriptor::new).as_ref(), || None) + self.request_desc(env.map(EnvDescriptor::new).as_ref()) } pub fn revoke(&mut self, env: Option<&str>) -> PermissionState { @@ -1264,12 +1558,12 @@ impl UnaryPermission<EnvDescriptor> { api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc(Some(&EnvDescriptor::new(env)), false, api_name, || None) + self.check_desc(Some(&EnvDescriptor::new(env)), false, api_name) } pub fn check_all(&mut self) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc(None, false, None, || None) + self.check_desc(None, false, None) } } @@ -1282,10 +1576,7 @@ impl UnaryPermission<SysDescriptor> { } pub fn request(&mut self, kind: Option<&str>) -> PermissionState { - self - .request_desc(kind.map(|k| SysDescriptor(k.to_string())).as_ref(), || { - None - }) + self.request_desc(kind.map(|k| SysDescriptor(k.to_string())).as_ref()) } pub fn revoke(&mut self, kind: Option<&str>) -> PermissionState { @@ -1298,57 +1589,44 @@ impl UnaryPermission<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(&SysDescriptor(kind.to_string())), false, api_name) } pub fn check_all(&mut self) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc(None, false, None, || None) + self.check_desc(None, false, None) } } -impl UnaryPermission<RunDescriptor> { - 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<RunQueryDescriptor> { + pub fn query(&self, cmd: Option<&RunQueryDescriptor>) -> PermissionState { + self.query_desc(cmd, AllowPartial::TreatAsPartialGranted) } - pub fn request(&mut self, cmd: Option<&str>) -> PermissionState { - self.request_desc( - cmd.map(|c| RunDescriptor::from(c.to_string())).as_ref(), - || Some(cmd?.to_string()), - ) + pub fn request( + &mut self, + cmd: Option<&RunQueryDescriptor>, + ) -> PermissionState { + self.request_desc(cmd) } - pub fn revoke(&mut self, cmd: Option<&str>) -> PermissionState { - self.revoke_desc(cmd.map(|c| RunDescriptor::from(c.to_string())).as_ref()) + pub fn revoke( + &mut self, + cmd: Option<&RunQueryDescriptor>, + ) -> PermissionState { + self.revoke_desc(cmd) } pub fn check( &mut self, - cmd: RunPathQuery, + cmd: &RunQueryDescriptor, api_name: Option<&str>, ) -> Result<(), AnyError> { - debug_assert!(cmd.resolved.is_absolute()); - skip_check_if_is_permission_fully_granted!(self); - self.check_desc( - Some(&RunDescriptor::Path(cmd.resolved.to_path_buf())), - false, - api_name, - || Some(format!("\"{}\"", cmd.requested)), - ) + self.check_desc(Some(cmd), false, api_name) } pub fn check_all(&mut self, api_name: Option<&str>) -> Result<(), AnyError> { - skip_check_if_is_permission_fully_granted!(self); - self.check_desc(None, false, api_name, || None) + self.check_desc(None, false, api_name) } /// Queries without prompting @@ -1358,7 +1636,7 @@ impl UnaryPermission<RunDescriptor> { } let (result, _prompted, _is_allow_all) = self.query_desc(None, AllowPartial::TreatAsDenied).check2( - RunDescriptor::flag_name(), + RunQueryDescriptor::flag_name(), api_name, || None, /* prompt */ false, @@ -1367,73 +1645,57 @@ impl UnaryPermission<RunDescriptor> { } } -impl UnaryPermission<FfiDescriptor> { - 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<FfiQueryDescriptor> { + pub fn query(&self, path: Option<&FfiQueryDescriptor>) -> PermissionState { + self.query_desc(path, AllowPartial::TreatAsPartialGranted) } - pub fn request(&mut self, path: Option<&Path>) -> PermissionState { - self.request_desc( - path - .map(|p| FfiDescriptor(resolve_from_cwd(p).unwrap())) - .as_ref(), - || Some(path?.display().to_string()), - ) + pub fn request( + &mut self, + path: Option<&FfiQueryDescriptor>, + ) -> PermissionState { + self.request_desc(path) } - pub fn revoke(&mut self, path: Option<&Path>) -> PermissionState { - self.revoke_desc( - path - .map(|p| FfiDescriptor(resolve_from_cwd(p).unwrap())) - .as_ref(), - ) + pub fn revoke( + &mut self, + path: Option<&FfiQueryDescriptor>, + ) -> PermissionState { + self.revoke_desc(path) } pub fn check( &mut self, - path: &Path, + path: &FfiQueryDescriptor, api_name: Option<&str>, ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc( - Some(&FfiDescriptor(resolve_from_cwd(path)?)), - true, - api_name, - || Some(format!("\"{}\"", path.display())), - ) + self.check_desc(Some(path), true, api_name) } - pub fn check_partial(&mut self, path: Option<&Path>) -> Result<(), AnyError> { + pub fn check_partial( + &mut self, + path: Option<&FfiQueryDescriptor>, + ) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - let desc = match path { - Some(path) => Some(FfiDescriptor(resolve_from_cwd(path)?)), - None => None, - }; - self.check_desc(desc.as_ref(), false, None, || { - Some(format!("\"{}\"", path?.display())) - }) + self.check_desc(path, false, None) } pub fn check_all(&mut self) -> Result<(), AnyError> { skip_check_if_is_permission_fully_granted!(self); - self.check_desc(None, false, Some("all"), || None) + self.check_desc(None, false, Some("all")) } } #[derive(Clone, Debug, Eq, PartialEq)] pub struct Permissions { - pub read: UnaryPermission<ReadDescriptor>, - pub write: UnaryPermission<WriteDescriptor>, + pub read: UnaryPermission<ReadQueryDescriptor>, + pub write: UnaryPermission<WriteQueryDescriptor>, pub net: UnaryPermission<NetDescriptor>, pub env: UnaryPermission<EnvDescriptor>, pub sys: UnaryPermission<SysDescriptor>, - pub run: UnaryPermission<RunDescriptor>, - pub ffi: UnaryPermission<FfiDescriptor>, + pub run: UnaryPermission<RunQueryDescriptor>, + pub ffi: UnaryPermission<FfiQueryDescriptor>, pub all: UnitPermission, } @@ -1444,33 +1706,33 @@ pub struct PermissionsOptions { pub deny_env: Option<Vec<String>>, pub allow_net: Option<Vec<String>>, pub deny_net: Option<Vec<String>>, - pub allow_ffi: Option<Vec<PathBuf>>, - pub deny_ffi: Option<Vec<PathBuf>>, - pub allow_read: Option<Vec<PathBuf>>, - pub deny_read: Option<Vec<PathBuf>>, - pub allow_run: Option<Vec<PathBuf>>, + pub allow_ffi: Option<Vec<String>>, + pub deny_ffi: Option<Vec<String>>, + pub allow_read: Option<Vec<String>>, + pub deny_read: Option<Vec<String>>, + pub allow_run: Option<Vec<String>>, pub deny_run: Option<Vec<String>>, pub allow_sys: Option<Vec<String>>, pub deny_sys: Option<Vec<String>>, - pub allow_write: Option<Vec<PathBuf>>, - pub deny_write: Option<Vec<PathBuf>>, + pub allow_write: Option<Vec<String>>, + pub deny_write: Option<Vec<String>>, pub prompt: bool, } impl Permissions { - pub fn new_unary<T>( - allow_list: Option<&[T::Arg]>, - deny_list: Option<&[T::Arg]>, + pub fn new_unary<TQuery>( + allow_list: Option<HashSet<TQuery::AllowDesc>>, + deny_list: Option<HashSet<TQuery::DenyDesc>>, prompt: bool, - ) -> Result<UnaryPermission<T>, AnyError> + ) -> Result<UnaryPermission<TQuery>, AnyError> where - T: Descriptor + Hash, + TQuery: QueryDescriptor, { - Ok(UnaryPermission::<T> { - 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::<TQuery> { + granted_global: global_from_option(allow_list.as_ref()), + granted_list: allow_list.unwrap_or_default(), + flag_denied_global: global_from_option(deny_list.as_ref()), + flag_denied_list: deny_list.unwrap_or_default(), prompt, ..Default::default() }) @@ -1486,57 +1748,140 @@ impl Permissions { ) } - pub fn from_options(opts: &PermissionsOptions) -> Result<Self, AnyError> { + pub fn from_options( + parser: &dyn PermissionDescriptorParser, + opts: &PermissionsOptions, + ) -> Result<Self, AnyError> { + fn resolve_allow_run( + parser: &dyn PermissionDescriptorParser, + allow_run: &[String], + ) -> Result<HashSet<AllowRunDescriptor>, 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<T: Eq + PartialEq + Hash>( + items: Option<&[String]>, + parse: impl Fn(&str) -> Result<T, AnyError>, + ) -> Result<Option<HashSet<T>>, AnyError> { + match items { + Some(items) => Ok(Some( + items + .iter() + .map(|item| parse(item)) + .collect::<Result<HashSet<_>, _>>()?, + )), + None => Ok(None), + } + } + + let mut deny_write = parse_maybe_vec(opts.deny_write.as_deref(), |item| { + parser.parse_write_descriptor(item) + })?; + let allow_run = opts + .allow_run + .as_ref() + .and_then(|raw_allow_run| { + match resolve_allow_run(parser, raw_allow_run) { + Ok(resolved_allow_run) => { + if resolved_allow_run.is_empty() && !raw_allow_run.is_empty() { + None // convert to no permissions if now empty + } else { + Some(Ok(resolved_allow_run)) + } + } + Err(err) => Some(Err(err)), + } + }) + .transpose()?; + // add the allow_run list to deny_write + if let Some(allow_run_vec) = &allow_run { + if !allow_run_vec.is_empty() { + let deny_write = deny_write.get_or_insert_with(Default::default); + deny_write.extend( + allow_run_vec + .iter() + .map(|item| WriteDescriptor(item.0.clone())), + ); + } + } + Ok(Self { read: Permissions::new_unary( - opts.allow_read.as_deref(), - opts.deny_read.as_deref(), + parse_maybe_vec(opts.allow_read.as_deref(), |item| { + parser.parse_read_descriptor(item) + })?, + parse_maybe_vec(opts.deny_read.as_deref(), |item| { + parser.parse_read_descriptor(item) + })?, opts.prompt, )?, write: Permissions::new_unary( - opts.allow_write.as_deref(), - opts.deny_write.as_deref(), + parse_maybe_vec(opts.allow_write.as_deref(), |item| { + parser.parse_write_descriptor(item) + })?, + deny_write, opts.prompt, )?, net: Permissions::new_unary( - opts.allow_net.as_deref(), - opts.deny_net.as_deref(), + parse_maybe_vec(opts.allow_net.as_deref(), |item| { + parser.parse_net_descriptor(item) + })?, + parse_maybe_vec(opts.deny_net.as_deref(), |item| { + parser.parse_net_descriptor(item) + })?, opts.prompt, )?, env: Permissions::new_unary( - opts.allow_env.as_deref(), - opts.deny_env.as_deref(), + parse_maybe_vec(opts.allow_env.as_deref(), |item| { + parser.parse_env_descriptor(item) + })?, + parse_maybe_vec(opts.deny_env.as_deref(), |text| { + parser.parse_env_descriptor(text) + })?, opts.prompt, )?, sys: Permissions::new_unary( - opts.allow_sys.as_deref(), - opts.deny_sys.as_deref(), + parse_maybe_vec(opts.allow_sys.as_deref(), |text| { + parser.parse_sys_descriptor(text) + })?, + parse_maybe_vec(opts.deny_sys.as_deref(), |text| { + parser.parse_sys_descriptor(text) + })?, opts.prompt, )?, run: Permissions::new_unary( - opts - .allow_run - .as_ref() - .map(|d| { - d.iter() - .map(|s| RunDescriptorArg::Path(s.clone())) - .collect::<Vec<_>>() - }) - .as_deref(), - opts - .deny_run - .as_ref() - .map(|d| { - d.iter() - .map(|s| RunDescriptorArg::from(s.clone())) - .collect::<Vec<_>>() - }) - .as_deref(), + allow_run, + parse_maybe_vec(opts.deny_run.as_deref(), |text| { + parser.parse_deny_run_descriptor(text) + })?, opts.prompt, )?, ffi: Permissions::new_unary( - opts.allow_ffi.as_deref(), - opts.deny_ffi.as_deref(), + parse_maybe_vec(opts.allow_ffi.as_deref(), |text| { + parser.parse_ffi_descriptor(text) + })?, + parse_maybe_vec(opts.deny_ffi.as_deref(), |text| { + parser.parse_ffi_descriptor(text) + })?, opts.prompt, )?, all: Permissions::new_all(opts.allow_all), @@ -1588,7 +1933,14 @@ impl Permissions { ) -> Result<(), AnyError> { match specifier.scheme() { "file" => match specifier.to_file_path() { - Ok(path) => self.read.check(&path, Some("import()")), + Ok(path) => self.read.check( + &PathQueryDescriptor { + requested: path.to_string_lossy().into_owned(), + resolved: path, + } + .into_read(), + Some("import()"), + ), Err(_) => Err(uri_error(format!( "Invalid file path.\n Specifier: {specifier}" ))), @@ -1607,15 +1959,30 @@ impl Permissions { /// case might need to be mutated). Also for the Web Worker API we need a way /// to send permissions to a new thread. #[derive(Clone, Debug)] -pub struct PermissionsContainer(pub Arc<Mutex<Permissions>>); +pub struct PermissionsContainer { + // todo(dsherret): make both of these private as the functionality + // can just be methods on PermissionsContainer. Additionally, a separate + // struct should be created in here that handles creating child permissions + // so that the code is not so verbose elsewhere. + pub descriptor_parser: Arc<dyn PermissionDescriptorParser>, + pub inner: Arc<Mutex<Permissions>>, +} impl PermissionsContainer { - pub fn new(perms: Permissions) -> Self { - Self(Arc::new(Mutex::new(perms))) + pub fn new( + descriptor_parser: Arc<dyn PermissionDescriptorParser>, + perms: Permissions, + ) -> Self { + Self { + descriptor_parser, + inner: Arc::new(Mutex::new(perms)), + } } - pub fn allow_all() -> Self { - Self::new(Permissions::allow_all()) + pub fn allow_all( + descriptor_parser: Arc<dyn PermissionDescriptorParser>, + ) -> Self { + Self::new(descriptor_parser, Permissions::allow_all()) } #[inline(always)] @@ -1623,27 +1990,61 @@ impl PermissionsContainer { &self, specifier: &ModuleSpecifier, ) -> Result<(), AnyError> { - self.0.lock().check_specifier(specifier) + self.inner.lock().check_specifier(specifier) } + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] #[inline(always)] pub fn check_read( - &mut self, - path: &Path, + &self, + path: &str, api_name: &str, - ) -> Result<(), AnyError> { - self.0.lock().read.check(path, Some(api_name)) + ) -> Result<PathBuf, AnyError> { + 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<PathBuf, AnyError> { + let mut inner = self.inner.lock(); + let inner = &mut inner.read; + if inner.is_allow_all() { + Ok(PathBuf::from(path)) + } else { + let desc = self.descriptor_parser.parse_path_query(path)?.into_read(); + inner.check(&desc, api_name)?; + Ok(desc.0.resolved) + } + } + + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] + #[inline(always)] + pub fn check_read_path<'a>( + &self, + path: &'a Path, + api_name: Option<&str>, + ) -> Result<Cow<'a, Path>, AnyError> { + let mut inner = self.inner.lock(); + let inner = &mut inner.read; + if inner.is_allow_all() { + Ok(Cow::Borrowed(path)) + } else { + let desc = PathQueryDescriptor { + requested: path.to_string_lossy().into_owned(), + resolved: path.to_path_buf(), + } + .into_read(); + inner.check(&desc, api_name)?; + Ok(Cow::Owned(desc.0.resolved)) + } } + /// As `check_read()`, but permission error messages will anonymize the path + /// by replacing it with the given `display`. #[inline(always)] pub fn check_read_blind( &mut self, @@ -1651,105 +2052,167 @@ impl PermissionsContainer { display: &str, api_name: &str, ) -> Result<(), AnyError> { - self.0.lock().read.check_blind(path, display, api_name) + let mut inner = self.inner.lock(); + let inner = &mut inner.read; + skip_check_if_is_permission_fully_granted!(inner); + inner.check( + &PathQueryDescriptor { + requested: format!("<{}>", display), + resolved: path.to_path_buf(), + } + .into_read(), + Some(api_name), + ) } #[inline(always)] - pub fn check_read_all(&mut self, api_name: &str) -> Result<(), AnyError> { - self.0.lock().read.check_all(Some(api_name)) + pub fn check_read_all(&self, api_name: &str) -> Result<(), AnyError> { + self.inner.lock().read.check_all(Some(api_name)) } + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] #[inline(always)] pub fn check_write( - &mut self, - path: &Path, + &self, + path: &str, api_name: &str, - ) -> Result<(), AnyError> { - self.0.lock().write.check(path, Some(api_name)) + ) -> Result<PathBuf, AnyError> { + 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<PathBuf, AnyError> { + 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<Cow<'a, Path>, AnyError> { + let mut inner = self.inner.lock(); + let inner = &mut inner.write; + if inner.is_allow_all() { + Ok(Cow::Borrowed(path)) + } else { + let desc = PathQueryDescriptor { + requested: path.to_string_lossy().into_owned(), + resolved: path.to_path_buf(), + } + .into_write(); + inner.check(&desc, Some(api_name))?; + Ok(Cow::Owned(desc.0.resolved)) + } } #[inline(always)] - pub fn check_write_all(&mut self, api_name: &str) -> Result<(), AnyError> { - self.0.lock().write.check_all(Some(api_name)) + pub fn check_write_all(&self, api_name: &str) -> Result<(), AnyError> { + self.inner.lock().write.check_all(Some(api_name)) } + /// As `check_write()`, but permission error messages will anonymize the path + /// by replacing it with the given `display`. #[inline(always)] pub fn check_write_blind( - &mut self, + &self, path: &Path, display: &str, api_name: &str, ) -> Result<(), AnyError> { - self.0.lock().write.check_blind(path, display, api_name) + let mut inner = self.inner.lock(); + let inner = &mut inner.write; + skip_check_if_is_permission_fully_granted!(inner); + inner.check( + &PathQueryDescriptor { + requested: format!("<{}>", display), + resolved: path.to_path_buf(), + } + .into_write(), + Some(api_name), + ) } #[inline(always)] pub fn check_write_partial( &mut self, - path: &Path, + path: &str, api_name: &str, - ) -> Result<(), AnyError> { - self.0.lock().write.check_partial(path, Some(api_name)) + ) -> Result<PathBuf, AnyError> { + let mut inner = self.inner.lock(); + let inner = &mut inner.write; + if inner.is_allow_all() { + Ok(PathBuf::from(path)) + } else { + let desc = self.descriptor_parser.parse_path_query(path)?.into_write(); + inner.check_partial(&desc, Some(api_name))?; + Ok(desc.0.resolved) + } } #[inline(always)] pub fn check_run( &mut self, - cmd: RunPathQuery, + cmd: &RunQueryDescriptor, api_name: &str, ) -> Result<(), AnyError> { - self.0.lock().run.check(cmd, Some(api_name)) + self.inner.lock().run.check(cmd, Some(api_name)) } #[inline(always)] pub fn check_run_all(&mut self, api_name: &str) -> Result<(), AnyError> { - self.0.lock().run.check_all(Some(api_name)) + self.inner.lock().run.check_all(Some(api_name)) } #[inline(always)] pub fn query_run_all(&mut self, api_name: &str) -> bool { - self.0.lock().run.query_all(Some(api_name)) + self.inner.lock().run.query_all(Some(api_name)) } #[inline(always)] pub fn check_sys(&self, kind: &str, api_name: &str) -> Result<(), AnyError> { - self.0.lock().sys.check(kind, Some(api_name)) + self.inner.lock().sys.check(kind, Some(api_name)) } #[inline(always)] pub fn check_env(&mut self, var: &str) -> Result<(), AnyError> { - self.0.lock().env.check(var, None) + self.inner.lock().env.check(var, None) } #[inline(always)] pub fn check_env_all(&mut self) -> Result<(), AnyError> { - self.0.lock().env.check_all() + self.inner.lock().env.check_all() } #[inline(always)] pub fn check_sys_all(&mut self) -> Result<(), AnyError> { - self.0.lock().sys.check_all() + self.inner.lock().sys.check_all() } #[inline(always)] pub fn check_ffi_all(&mut self) -> Result<(), AnyError> { - self.0.lock().ffi.check_all() + self.inner.lock().ffi.check_all() } /// This checks to see if the allow-all flag was passed, not whether all /// permissions are enabled! #[inline(always)] pub fn check_was_allow_all_flag_passed(&mut self) -> Result<(), AnyError> { - self.0.lock().all.check() + self.inner.lock().all.check() } /// Checks special file access, returning the failed permission type if @@ -1862,7 +2325,7 @@ impl PermissionsContainer { url: &Url, api_name: &str, ) -> Result<(), AnyError> { - self.0.lock().net.check_url(url, Some(api_name)) + self.inner.lock().net.check_url(url, Some(api_name)) } #[inline(always)] @@ -1871,22 +2334,54 @@ impl PermissionsContainer { host: &(T, Option<u16>), api_name: &str, ) -> Result<(), AnyError> { + let mut inner = self.inner.lock(); + let inner = &mut inner.net; + skip_check_if_is_permission_fully_granted!(inner); let hostname = Host::parse(host.0.as_ref())?; let descriptor = NetDescriptor(hostname, host.1); - self.0.lock().net.check(&descriptor, Some(api_name)) + inner.check(&descriptor, Some(api_name)) } #[inline(always)] - pub fn check_ffi(&mut self, path: Option<&Path>) -> Result<(), AnyError> { - self.0.lock().ffi.check(path.unwrap(), None) + pub fn check_ffi(&mut self, path: &str) -> Result<PathBuf, AnyError> { + 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(&mut self) -> Result<(), AnyError> { + let mut inner = self.inner.lock(); + let inner = &mut inner.ffi; + if inner.is_allow_all() { + Ok(()) + } else { + inner.check_partial(None) + } } + #[must_use = "the resolved return value to mitigate time-of-check to time-of-use issues"] #[inline(always)] - pub fn check_ffi_partial( + pub fn check_ffi_partial_with_path( &mut self, - path: Option<&Path>, - ) -> Result<(), AnyError> { - self.0.lock().ffi.check_partial(path) + path: &str, + ) -> Result<PathBuf, AnyError> { + let mut inner = self.inner.lock(); + let inner = &mut inner.ffi; + if inner.is_allow_all() { + Ok(PathBuf::from(path)) + } else { + let desc = self.descriptor_parser.parse_path_query(path)?.into_ffi(); + inner.check_partial(Some(&desc))?; + Ok(desc.0.resolved) + } } } @@ -1911,93 +2406,10 @@ const fn unit_permission_from_flag_bools( } } -fn global_from_option<T>(flag: Option<&[T]>) -> bool { +fn global_from_option<T>(flag: Option<&HashSet<T>>) -> bool { matches!(flag, Some(v) if v.is_empty()) } -fn parse_net_list( - list: Option<&[String]>, -) -> Result<HashSet<NetDescriptor>, AnyError> { - if let Some(v) = list { - v.iter() - .map(|x| NetDescriptor::parse(x)) - .collect::<Result<HashSet<NetDescriptor>, AnyError>>() - } else { - Ok(HashSet::new()) - } -} - -fn parse_env_list( - list: Option<&[String]>, -) -> Result<HashSet<EnvDescriptor>, AnyError> { - if let Some(v) = list { - v.iter() - .map(|x| { - if x.is_empty() { - Err(AnyError::msg("Empty path is not allowed")) - } else { - Ok(EnvDescriptor::new(x)) - } - }) - .collect() - } else { - Ok(HashSet::new()) - } -} - -fn parse_path_list<T: Descriptor + Hash>( - list: Option<&[PathBuf]>, - f: fn(PathBuf) -> T, -) -> Result<HashSet<T>, AnyError> { - if let Some(v) = list { - v.iter() - .map(|raw_path| { - if raw_path.as_os_str().is_empty() { - Err(AnyError::msg("Empty path is not allowed")) - } else { - resolve_from_cwd(Path::new(&raw_path)).map(f) - } - }) - .collect() - } else { - Ok(HashSet::new()) - } -} - -fn parse_sys_list( - list: Option<&[String]>, -) -> Result<HashSet<SysDescriptor>, AnyError> { - if let Some(v) = list { - v.iter() - .map(|x| { - if x.is_empty() { - Err(AnyError::msg("empty")) - } else { - Ok(SysDescriptor(x.to_string())) - } - }) - .collect() - } else { - Ok(HashSet::new()) - } -} - -fn parse_run_list( - list: Option<&[RunDescriptorArg]>, -) -> Result<HashSet<RunDescriptor>, AnyError> { - let Some(v) = list else { - return Ok(HashSet::new()); - }; - Ok( - v.iter() - .map(|arg| match arg { - RunDescriptorArg::Name(s) => RunDescriptor::Name(s.clone()), - RunDescriptorArg::Path(l) => RunDescriptor::Path(l.clone()), - }) - .collect(), - ) -} - fn escalation_error() -> AnyError { custom_error("NotCapable", "Can't escalate parent thread permissions") } @@ -2246,7 +2658,58 @@ impl<'de> Deserialize<'de> for ChildPermissionsArg { } } +/// Parses and normalizes permissions. +/// +/// This trait is necessary because this crate doesn't have access +/// to the file system. +pub trait PermissionDescriptorParser: Debug + Send + Sync { + fn parse_read_descriptor( + &self, + text: &str, + ) -> Result<ReadDescriptor, AnyError>; + + fn parse_write_descriptor( + &self, + text: &str, + ) -> Result<WriteDescriptor, AnyError>; + + fn parse_net_descriptor(&self, text: &str) + -> Result<NetDescriptor, AnyError>; + + fn parse_env_descriptor(&self, text: &str) + -> Result<EnvDescriptor, AnyError>; + + fn parse_sys_descriptor(&self, text: &str) + -> Result<SysDescriptor, AnyError>; + + fn parse_allow_run_descriptor( + &self, + text: &str, + ) -> Result<AllowRunDescriptorParseResult, AnyError>; + + fn parse_deny_run_descriptor( + &self, + text: &str, + ) -> Result<DenyRunDescriptor, AnyError>; + + fn parse_ffi_descriptor(&self, text: &str) + -> Result<FfiDescriptor, AnyError>; + + // queries + + fn parse_path_query( + &self, + path: &str, + ) -> Result<PathQueryDescriptor, AnyError>; + + fn parse_run_query( + &self, + requested: &str, + ) -> Result<RunQueryDescriptor, AnyError>; +} + pub fn create_child_permissions( + parser: &dyn PermissionDescriptorParser, main_perms: &mut Permissions, child_permissions_arg: ChildPermissionsArg, ) -> Result<Permissions, AnyError> { @@ -2287,25 +2750,41 @@ pub fn create_child_permissions( // in the worker_perms.all block above worker_perms.read = main_perms .read - .create_child_permissions(child_permissions_arg.read)?; + .create_child_permissions(child_permissions_arg.read, |text| { + Ok(Some(parser.parse_read_descriptor(text)?)) + })?; worker_perms.write = main_perms .write - .create_child_permissions(child_permissions_arg.write)?; + .create_child_permissions(child_permissions_arg.write, |text| { + Ok(Some(parser.parse_write_descriptor(text)?)) + })?; worker_perms.net = main_perms .net - .create_child_permissions(child_permissions_arg.net)?; + .create_child_permissions(child_permissions_arg.net, |text| { + Ok(Some(parser.parse_net_descriptor(text)?)) + })?; worker_perms.env = main_perms .env - .create_child_permissions(child_permissions_arg.env)?; + .create_child_permissions(child_permissions_arg.env, |text| { + Ok(Some(parser.parse_env_descriptor(text)?)) + })?; worker_perms.sys = main_perms .sys - .create_child_permissions(child_permissions_arg.sys)?; - worker_perms.run = main_perms - .run - .create_child_permissions(child_permissions_arg.run)?; + .create_child_permissions(child_permissions_arg.sys, |text| { + Ok(Some(parser.parse_sys_descriptor(text)?)) + })?; + worker_perms.run = main_perms.run.create_child_permissions( + child_permissions_arg.run, + |text| match parser.parse_allow_run_descriptor(text)? { + AllowRunDescriptorParseResult::Unresolved(_) => Ok(None), + AllowRunDescriptorParseResult::Descriptor(desc) => Ok(Some(desc)), + }, + )?; worker_perms.ffi = main_perms .ffi - .create_child_permissions(child_permissions_arg.ffi)?; + .create_child_permissions(child_permissions_arg.ffi, |text| { + Ok(Some(parser.parse_ffi_descriptor(text)?)) + })?; Ok(worker_perms) } @@ -2332,128 +2811,167 @@ mod tests { macro_rules! svec { ($($x:expr),*) => (vec![$($x.to_string()),*]); } - macro_rules! sarr { - ($($x:expr),*) => ([$($x.to_string()),*]); + + #[derive(Debug)] + struct TestPermissionDescriptorParser; + + impl TestPermissionDescriptorParser { + fn join_path_with_root(&self, path: &str) -> PathBuf { + if path.starts_with("C:\\") { + PathBuf::from(path) + } else { + PathBuf::from("/").join(path) + } + } } - #[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<ReadDescriptor, AnyError> { + 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<WriteDescriptor, AnyError> { + 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, AnyError> { + 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_env_descriptor( + &self, + text: &str, + ) -> Result<EnvDescriptor, AnyError> { + Ok(EnvDescriptor::new(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_sys_descriptor( + &self, + text: &str, + ) -> Result<SysDescriptor, AnyError> { + Ok(SysDescriptor(text.to_string())) + } - // 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_allow_run_descriptor( + &self, + text: &str, + ) -> Result<AllowRunDescriptorParseResult, AnyError> { + Ok(AllowRunDescriptorParseResult::Descriptor( + AllowRunDescriptor(self.join_path_with_root(text)), + )) + } - // 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_deny_run_descriptor( + &self, + text: &str, + ) -> Result<DenyRunDescriptor, AnyError> { + if text.contains("/") { + Ok(DenyRunDescriptor::Path(self.join_path_with_root(text))) + } else { + Ok(DenyRunDescriptor::Name(text.to_string())) + } + } - // 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_ffi_descriptor( + &self, + text: &str, + ) -> Result<FfiDescriptor, AnyError> { + Ok(FfiDescriptor(self.join_path_with_root(text))) + } - // 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_path_query( + &self, + path: &str, + ) -> Result<PathQueryDescriptor, AnyError> { + Ok(PathQueryDescriptor { + resolved: self.join_path_with_root(path), + requested: path.to_string(), + }) + } - // 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()); + fn parse_run_query( + &self, + requested: &str, + ) -> Result<RunQueryDescriptor, AnyError> { + RunQueryDescriptor::parse(requested) + } + } - // 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()); + #[test] + fn check_paths() { + set_prompter(Box::new(TestPrompter)); + let allowlist = svec!["/a/specific/dir/name", "/a/specific", "/b/c"]; + + let parser = TestPermissionDescriptorParser; + let perms = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_read: Some(allowlist.clone()), + allow_write: Some(allowlist.clone()), + allow_ffi: Some(allowlist), + ..Default::default() + }, + ) + .unwrap(); + let mut perms = PermissionsContainer::new(Arc::new(parser), perms); + + let cases = [ + // Inside of /a/specific and /a/specific/dir/name + ("/a/specific/dir/name", true), + // Inside of /a/specific but outside of /a/specific/dir/name + ("/a/specific/dir", true), + // Inside of /a/specific and /a/specific/dir/name + ("/a/specific/dir/name/inner", true), + // Inside of /a/specific but outside of /a/specific/dir/name + ("/a/specific/other/dir", true), + // Exact match with /b/c + ("/b/c", true), + // Sub path within /b/c + ("/b/c/sub/path", true), + // Sub path within /b/c, needs normalizing + ("/b/c/sub/path/../path/.", true), + // Inside of /b but outside of /b/c + ("/b/e", false), + // Inside of /a but outside of /a/specific + ("/a/b", false), + ]; + + for (path, is_ok) in cases { + assert_eq!(perms.check_read(path, "api").is_ok(), is_ok); + assert_eq!(perms.check_write(path, "api").is_ok(), is_ok); + assert_eq!(perms.check_ffi(path).is_ok(), is_ok); + } } #[test] fn test_check_net_with_values() { set_prompter(Box::new(TestPrompter)); - let mut perms = Permissions::from_options(&PermissionsOptions { - allow_net: Some(svec![ - "localhost", - "deno.land", - "github.com:3000", - "127.0.0.1", - "172.16.0.2:8000", - "www.github.com:443", - "80.example.com:80", - "443.example.com:443" - ]), - ..Default::default() - }) + let parser = TestPermissionDescriptorParser; + let mut perms = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_net: Some(svec![ + "localhost", + "deno.land", + "github.com:3000", + "127.0.0.1", + "172.16.0.2:8000", + "www.github.com:443", + "80.example.com:80", + "443.example.com:443" + ]), + ..Default::default() + }, + ) .unwrap(); let domain_tests = vec![ @@ -2496,10 +3014,14 @@ mod tests { #[test] fn test_check_net_only_flag() { set_prompter(Box::new(TestPrompter)); - let mut perms = Permissions::from_options(&PermissionsOptions { - allow_net: Some(svec![]), // this means `--allow-net` is present without values following `=` sign - ..Default::default() - }) + let parser = TestPermissionDescriptorParser; + let mut perms = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_net: Some(svec![]), // this means `--allow-net` is present without values following `=` sign + ..Default::default() + }, + ) .unwrap(); let domain_tests = vec![ @@ -2537,10 +3059,14 @@ mod tests { #[test] fn test_check_net_no_flag() { set_prompter(Box::new(TestPrompter)); - let mut perms = Permissions::from_options(&PermissionsOptions { - allow_net: None, - ..Default::default() - }) + let parser = TestPermissionDescriptorParser; + let mut perms = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_net: None, + ..Default::default() + }, + ) .unwrap(); let domain_tests = vec![ @@ -2577,17 +3103,20 @@ mod tests { #[test] fn test_check_net_url() { - let mut perms = Permissions::from_options(&PermissionsOptions { - allow_net: Some(svec![ - "localhost", - "deno.land", - "github.com:3000", - "127.0.0.1", - "172.16.0.2:8000", - "www.github.com:443" - ]), - ..Default::default() - }) + let mut perms = Permissions::from_options( + &TestPermissionDescriptorParser, + &PermissionsOptions { + allow_net: Some(svec![ + "localhost", + "deno.land", + "github.com:3000", + "127.0.0.1", + "172.16.0.2:8000", + "www.github.com:443" + ]), + ..Default::default() + }, + ) .unwrap(); let url_tests = vec![ @@ -2639,15 +3168,18 @@ mod tests { fn check_specifiers() { set_prompter(Box::new(TestPrompter)); let read_allowlist = if cfg!(target_os = "windows") { - vec![PathBuf::from("C:\\a")] + svec!["C:\\a"] } else { - vec![PathBuf::from("/a")] + svec!["/a"] }; - let mut perms = Permissions::from_options(&PermissionsOptions { - allow_read: Some(read_allowlist), - allow_net: Some(svec!["localhost"]), - ..Default::default() - }) + let mut perms = Permissions::from_options( + &TestPermissionDescriptorParser, + &PermissionsOptions { + allow_read: Some(read_allowlist), + allow_net: Some(svec!["localhost"]), + ..Default::default() + }, + ) .unwrap(); let mut fixtures = vec![ @@ -2713,130 +3245,99 @@ mod tests { #[test] fn test_query() { set_prompter(Box::new(TestPrompter)); + let parser = TestPermissionDescriptorParser; let perms1 = Permissions::allow_all(); - let perms2 = Permissions { - read: Permissions::new_unary(Some(&[PathBuf::from("/foo")]), None, false) - .unwrap(), - write: Permissions::new_unary( - Some(&[PathBuf::from("/foo")]), - None, - false, - ) - .unwrap(), - ffi: Permissions::new_unary(Some(&[PathBuf::from("/foo")]), None, false) - .unwrap(), - net: Permissions::new_unary(Some(&sarr!["127.0.0.1:8000"]), None, false) - .unwrap(), - env: Permissions::new_unary(Some(&sarr!["HOME"]), None, false).unwrap(), - sys: Permissions::new_unary(Some(&sarr!["hostname"]), None, false) - .unwrap(), - run: Permissions::new_unary( - Some(&["deno".to_string().into()]), - None, - false, - ) - .unwrap(), - all: Permissions::new_all(false), - }; - let perms3 = Permissions { - read: Permissions::new_unary(None, Some(&[PathBuf::from("/foo")]), false) - .unwrap(), - write: Permissions::new_unary( - None, - Some(&[PathBuf::from("/foo")]), - false, - ) - .unwrap(), - ffi: Permissions::new_unary(None, Some(&[PathBuf::from("/foo")]), false) - .unwrap(), - net: Permissions::new_unary(None, Some(&sarr!["127.0.0.1:8000"]), false) - .unwrap(), - env: Permissions::new_unary(None, Some(&sarr!["HOME"]), false).unwrap(), - sys: Permissions::new_unary(None, Some(&sarr!["hostname"]), false) - .unwrap(), - run: Permissions::new_unary( - None, - Some(&["deno".to_string().into()]), - false, - ) - .unwrap(), - all: Permissions::new_all(false), - }; - let perms4 = Permissions { - read: Permissions::new_unary( - Some(&[]), - Some(&[PathBuf::from("/foo")]), - false, - ) - .unwrap(), - write: Permissions::new_unary( - Some(&[]), - Some(&[PathBuf::from("/foo")]), - false, - ) - .unwrap(), - ffi: Permissions::new_unary( - Some(&[]), - Some(&[PathBuf::from("/foo")]), - false, - ) - .unwrap(), - net: Permissions::new_unary( - Some(&[]), - Some(&sarr!["127.0.0.1:8000"]), - false, - ) - .unwrap(), - env: Permissions::new_unary(Some(&[]), Some(&sarr!["HOME"]), false) - .unwrap(), - sys: Permissions::new_unary(Some(&[]), Some(&sarr!["hostname"]), false) - .unwrap(), - run: Permissions::new_unary( - Some(&[]), - Some(&["deno".to_string().into()]), - false, - ) - .unwrap(), - all: Permissions::new_all(false), - }; + let perms2 = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_read: Some(svec!["/foo"]), + allow_write: Some(svec!["/foo"]), + allow_ffi: Some(svec!["/foo"]), + allow_net: Some(svec!["127.0.0.1:8000"]), + allow_env: Some(svec!["HOME"]), + allow_sys: Some(svec!["hostname"]), + allow_run: Some(svec!["/deno"]), + allow_all: false, + ..Default::default() + }, + ) + .unwrap(); + let perms3 = Permissions::from_options( + &parser, + &PermissionsOptions { + deny_read: Some(svec!["/foo"]), + deny_write: Some(svec!["/foo"]), + deny_ffi: Some(svec!["/foo"]), + deny_net: Some(svec!["127.0.0.1:8000"]), + deny_env: Some(svec!["HOME"]), + deny_sys: Some(svec!["hostname"]), + deny_run: Some(svec!["deno"]), + ..Default::default() + }, + ) + .unwrap(); + let perms4 = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_read: Some(vec![]), + deny_read: Some(svec!["/foo"]), + allow_write: Some(vec![]), + deny_write: Some(svec!["/foo"]), + allow_ffi: Some(vec![]), + deny_ffi: Some(svec!["/foo"]), + allow_net: Some(vec![]), + deny_net: Some(svec!["127.0.0.1:8000"]), + allow_env: Some(vec![]), + deny_env: Some(svec!["HOME"]), + allow_sys: Some(vec![]), + deny_sys: Some(svec!["hostname"]), + allow_run: Some(vec![]), + deny_run: Some(svec!["deno"]), + ..Default::default() + }, + ) + .unwrap(); #[rustfmt::skip] { + let read_query = |path: &str| parser.parse_path_query(path).unwrap().into_read(); + let write_query = |path: &str| parser.parse_path_query(path).unwrap().into_write(); + let ffi_query = |path: &str| parser.parse_path_query(path).unwrap().into_ffi(); assert_eq!(perms1.read.query(None), PermissionState::Granted); - assert_eq!(perms1.read.query(Some(Path::new("/foo"))), PermissionState::Granted); + assert_eq!(perms1.read.query(Some(&read_query("/foo"))), PermissionState::Granted); assert_eq!(perms2.read.query(None), PermissionState::Prompt); - assert_eq!(perms2.read.query(Some(Path::new("/foo"))), PermissionState::Granted); - assert_eq!(perms2.read.query(Some(Path::new("/foo/bar"))), PermissionState::Granted); + assert_eq!(perms2.read.query(Some(&read_query("/foo"))), PermissionState::Granted); + assert_eq!(perms2.read.query(Some(&read_query("/foo/bar"))), PermissionState::Granted); assert_eq!(perms3.read.query(None), PermissionState::Prompt); - assert_eq!(perms3.read.query(Some(Path::new("/foo"))), PermissionState::Denied); - assert_eq!(perms3.read.query(Some(Path::new("/foo/bar"))), PermissionState::Denied); + assert_eq!(perms3.read.query(Some(&read_query("/foo"))), PermissionState::Denied); + assert_eq!(perms3.read.query(Some(&read_query("/foo/bar"))), PermissionState::Denied); assert_eq!(perms4.read.query(None), PermissionState::GrantedPartial); - assert_eq!(perms4.read.query(Some(Path::new("/foo"))), PermissionState::Denied); - assert_eq!(perms4.read.query(Some(Path::new("/foo/bar"))), PermissionState::Denied); - assert_eq!(perms4.read.query(Some(Path::new("/bar"))), PermissionState::Granted); + assert_eq!(perms4.read.query(Some(&read_query("/foo"))), PermissionState::Denied); + assert_eq!(perms4.read.query(Some(&read_query("/foo/bar"))), PermissionState::Denied); + assert_eq!(perms4.read.query(Some(&read_query("/bar"))), PermissionState::Granted); assert_eq!(perms1.write.query(None), PermissionState::Granted); - assert_eq!(perms1.write.query(Some(Path::new("/foo"))), PermissionState::Granted); + assert_eq!(perms1.write.query(Some(&write_query("/foo"))), PermissionState::Granted); assert_eq!(perms2.write.query(None), PermissionState::Prompt); - assert_eq!(perms2.write.query(Some(Path::new("/foo"))), PermissionState::Granted); - assert_eq!(perms2.write.query(Some(Path::new("/foo/bar"))), PermissionState::Granted); + assert_eq!(perms2.write.query(Some(&write_query("/foo"))), PermissionState::Granted); + assert_eq!(perms2.write.query(Some(&write_query("/foo/bar"))), PermissionState::Granted); assert_eq!(perms3.write.query(None), PermissionState::Prompt); - assert_eq!(perms3.write.query(Some(Path::new("/foo"))), PermissionState::Denied); - assert_eq!(perms3.write.query(Some(Path::new("/foo/bar"))), PermissionState::Denied); + assert_eq!(perms3.write.query(Some(&write_query("/foo"))), PermissionState::Denied); + assert_eq!(perms3.write.query(Some(&write_query("/foo/bar"))), PermissionState::Denied); assert_eq!(perms4.write.query(None), PermissionState::GrantedPartial); - assert_eq!(perms4.write.query(Some(Path::new("/foo"))), PermissionState::Denied); - assert_eq!(perms4.write.query(Some(Path::new("/foo/bar"))), PermissionState::Denied); - assert_eq!(perms4.write.query(Some(Path::new("/bar"))), PermissionState::Granted); + assert_eq!(perms4.write.query(Some(&write_query("/foo"))), PermissionState::Denied); + assert_eq!(perms4.write.query(Some(&write_query("/foo/bar"))), PermissionState::Denied); + assert_eq!(perms4.write.query(Some(&write_query("/bar"))), PermissionState::Granted); assert_eq!(perms1.ffi.query(None), PermissionState::Granted); - assert_eq!(perms1.ffi.query(Some(Path::new("/foo"))), PermissionState::Granted); + assert_eq!(perms1.ffi.query(Some(&ffi_query("/foo"))), PermissionState::Granted); assert_eq!(perms2.ffi.query(None), PermissionState::Prompt); - assert_eq!(perms2.ffi.query(Some(Path::new("/foo"))), PermissionState::Granted); - assert_eq!(perms2.ffi.query(Some(Path::new("/foo/bar"))), PermissionState::Granted); + assert_eq!(perms2.ffi.query(Some(&ffi_query("/foo"))), PermissionState::Granted); + assert_eq!(perms2.ffi.query(Some(&ffi_query("/foo/bar"))), PermissionState::Granted); assert_eq!(perms3.ffi.query(None), PermissionState::Prompt); - assert_eq!(perms3.ffi.query(Some(Path::new("/foo"))), PermissionState::Denied); - assert_eq!(perms3.ffi.query(Some(Path::new("/foo/bar"))), PermissionState::Denied); + assert_eq!(perms3.ffi.query(Some(&ffi_query("/foo"))), PermissionState::Denied); + assert_eq!(perms3.ffi.query(Some(&ffi_query("/foo/bar"))), PermissionState::Denied); assert_eq!(perms4.ffi.query(None), PermissionState::GrantedPartial); - assert_eq!(perms4.ffi.query(Some(Path::new("/foo"))), PermissionState::Denied); - assert_eq!(perms4.ffi.query(Some(Path::new("/foo/bar"))), PermissionState::Denied); - assert_eq!(perms4.ffi.query(Some(Path::new("/bar"))), PermissionState::Granted); + assert_eq!(perms4.ffi.query(Some(&ffi_query("/foo"))), PermissionState::Denied); + assert_eq!(perms4.ffi.query(Some(&ffi_query("/foo/bar"))), PermissionState::Denied); + assert_eq!(perms4.ffi.query(Some(&ffi_query("/bar"))), PermissionState::Granted); assert_eq!(perms1.net.query(None), PermissionState::Granted); assert_eq!(perms1.net.query(Some(&NetDescriptor(Host::must_parse("127.0.0.1"), None))), PermissionState::Granted); assert_eq!(perms2.net.query(None), PermissionState::Prompt); @@ -2865,37 +3366,54 @@ mod tests { assert_eq!(perms4.sys.query(Some("hostname")), PermissionState::Denied); assert_eq!(perms4.sys.query(Some("uid")), PermissionState::Granted); assert_eq!(perms1.run.query(None), PermissionState::Granted); - assert_eq!(perms1.run.query(Some("deno")), PermissionState::Granted); + let deno_run_query = RunQueryDescriptor::Path { + requested: "deno".to_string(), + resolved: PathBuf::from("/deno"), + }; + let node_run_query = RunQueryDescriptor::Path { + requested: "node".to_string(), + resolved: PathBuf::from("/node"), + }; + assert_eq!(perms1.run.query(Some(&deno_run_query)), PermissionState::Granted); + assert_eq!(perms1.write.query(Some(&write_query("/deno"))), PermissionState::Granted); assert_eq!(perms2.run.query(None), PermissionState::Prompt); - assert_eq!(perms2.run.query(Some("deno")), PermissionState::Granted); + assert_eq!(perms2.run.query(Some(&deno_run_query)), PermissionState::Granted); + assert_eq!(perms2.write.query(Some(&write_query("/deno"))), PermissionState::Denied); assert_eq!(perms3.run.query(None), PermissionState::Prompt); - assert_eq!(perms3.run.query(Some("deno")), PermissionState::Denied); + assert_eq!(perms3.run.query(Some(&deno_run_query)), PermissionState::Denied); assert_eq!(perms4.run.query(None), PermissionState::GrantedPartial); - assert_eq!(perms4.run.query(Some("deno")), PermissionState::Denied); - assert_eq!(perms4.run.query(Some("node")), PermissionState::Granted); + assert_eq!(perms4.run.query(Some(&deno_run_query)), PermissionState::Denied); + assert_eq!(perms4.run.query(Some(&node_run_query)), PermissionState::Granted); }; } #[test] fn test_request() { set_prompter(Box::new(TestPrompter)); + let parser = TestPermissionDescriptorParser; let mut perms: Permissions = Permissions::none_without_prompt(); + let read_query = + |path: &str| parser.parse_path_query(path).unwrap().into_read(); + let write_query = + |path: &str| parser.parse_path_query(path).unwrap().into_write(); + let ffi_query = + |path: &str| parser.parse_path_query(path).unwrap().into_ffi(); #[rustfmt::skip] { let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock(); prompt_value.set(true); - assert_eq!(perms.read.request(Some(Path::new("/foo"))), PermissionState::Granted); + assert_eq!(perms.read.request(Some(&read_query("/foo"))), PermissionState::Granted); assert_eq!(perms.read.query(None), PermissionState::Prompt); prompt_value.set(false); - assert_eq!(perms.read.request(Some(Path::new("/foo/bar"))), PermissionState::Granted); + assert_eq!(perms.read.request(Some(&read_query("/foo/bar"))), PermissionState::Granted); prompt_value.set(false); - assert_eq!(perms.write.request(Some(Path::new("/foo"))), PermissionState::Denied); - assert_eq!(perms.write.query(Some(Path::new("/foo/bar"))), PermissionState::Prompt); + assert_eq!(perms.write.request(Some(&write_query("/foo"))), PermissionState::Denied); + assert_eq!(perms.write.query(Some(&write_query("/foo/bar"))), PermissionState::Prompt); prompt_value.set(true); assert_eq!(perms.write.request(None), PermissionState::Denied); prompt_value.set(false); - assert_eq!(perms.ffi.request(Some(Path::new("/foo"))), PermissionState::Denied); - assert_eq!(perms.ffi.query(Some(Path::new("/foo/bar"))), PermissionState::Prompt); + assert_eq!(perms.ffi.request(Some(&ffi_query("/foo"))), PermissionState::Denied); + assert_eq!(perms.ffi.query(Some(&ffi_query("/foo/bar"))), PermissionState::Prompt); prompt_value.set(true); assert_eq!(perms.ffi.request(None), PermissionState::Denied); prompt_value.set(true); @@ -2913,69 +3431,62 @@ mod tests { prompt_value.set(false); assert_eq!(perms.sys.request(Some("hostname")), PermissionState::Granted); prompt_value.set(true); - assert_eq!(perms.run.request(Some("deno")), PermissionState::Granted); + let run_query = RunQueryDescriptor::Path { + requested: "deno".to_string(), + resolved: PathBuf::from("/deno"), + }; + assert_eq!(perms.run.request(Some(&run_query)), PermissionState::Granted); assert_eq!(perms.run.query(None), PermissionState::Prompt); prompt_value.set(false); - assert_eq!(perms.run.request(Some("deno")), PermissionState::Granted); + assert_eq!(perms.run.request(Some(&run_query)), PermissionState::Granted); }; } #[test] fn test_revoke() { set_prompter(Box::new(TestPrompter)); - let mut perms = Permissions { - read: Permissions::new_unary( - Some(&[PathBuf::from("/foo"), PathBuf::from("/foo/baz")]), - None, - false, - ) - .unwrap(), - write: Permissions::new_unary( - Some(&[PathBuf::from("/foo"), PathBuf::from("/foo/baz")]), - None, - false, - ) - .unwrap(), - ffi: Permissions::new_unary( - Some(&[PathBuf::from("/foo"), PathBuf::from("/foo/baz")]), - None, - false, - ) - .unwrap(), - net: Permissions::new_unary( - Some(&sarr!["127.0.0.1", "127.0.0.1:8000"]), - None, - false, - ) - .unwrap(), - env: Permissions::new_unary(Some(&sarr!["HOME"]), None, false).unwrap(), - sys: Permissions::new_unary(Some(&sarr!["hostname"]), None, false) - .unwrap(), - run: Permissions::new_unary( - Some(&["deno".to_string().into()]), - None, - false, - ) - .unwrap(), - all: Permissions::new_all(false), - }; + let parser = TestPermissionDescriptorParser; + let mut perms = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_read: Some(svec!["/foo", "/foo/baz"]), + allow_write: Some(svec!["/foo", "/foo/baz"]), + allow_ffi: Some(svec!["/foo", "/foo/baz"]), + allow_net: Some(svec!["127.0.0.1", "127.0.0.1:8000"]), + allow_env: Some(svec!["HOME"]), + allow_sys: Some(svec!["hostname"]), + allow_run: Some(svec!["/deno"]), + ..Default::default() + }, + ) + .unwrap(); + let read_query = + |path: &str| parser.parse_path_query(path).unwrap().into_read(); + let write_query = + |path: &str| parser.parse_path_query(path).unwrap().into_write(); + let ffi_query = + |path: &str| parser.parse_path_query(path).unwrap().into_ffi(); #[rustfmt::skip] { - assert_eq!(perms.read.revoke(Some(Path::new("/foo/bar"))), PermissionState::Prompt); - assert_eq!(perms.read.query(Some(Path::new("/foo"))), PermissionState::Prompt); - assert_eq!(perms.read.query(Some(Path::new("/foo/baz"))), PermissionState::Granted); - assert_eq!(perms.write.revoke(Some(Path::new("/foo/bar"))), PermissionState::Prompt); - assert_eq!(perms.write.query(Some(Path::new("/foo"))), PermissionState::Prompt); - assert_eq!(perms.write.query(Some(Path::new("/foo/baz"))), PermissionState::Granted); - assert_eq!(perms.ffi.revoke(Some(Path::new("/foo/bar"))), PermissionState::Prompt); - assert_eq!(perms.ffi.query(Some(Path::new("/foo"))), PermissionState::Prompt); - assert_eq!(perms.ffi.query(Some(Path::new("/foo/baz"))), PermissionState::Granted); + assert_eq!(perms.read.revoke(Some(&read_query("/foo/bar"))), PermissionState::Prompt); + assert_eq!(perms.read.query(Some(&read_query("/foo"))), PermissionState::Prompt); + assert_eq!(perms.read.query(Some(&read_query("/foo/baz"))), PermissionState::Granted); + assert_eq!(perms.write.revoke(Some(&write_query("/foo/bar"))), PermissionState::Prompt); + assert_eq!(perms.write.query(Some(&write_query("/foo"))), PermissionState::Prompt); + assert_eq!(perms.write.query(Some(&write_query("/foo/baz"))), PermissionState::Granted); + assert_eq!(perms.ffi.revoke(Some(&ffi_query("/foo/bar"))), PermissionState::Prompt); + assert_eq!(perms.ffi.query(Some(&ffi_query("/foo"))), PermissionState::Prompt); + assert_eq!(perms.ffi.query(Some(&ffi_query("/foo/baz"))), PermissionState::Granted); assert_eq!(perms.net.revoke(Some(&NetDescriptor(Host::must_parse("127.0.0.1"), Some(9000)))), PermissionState::Prompt); assert_eq!(perms.net.query(Some(&NetDescriptor(Host::must_parse("127.0.0.1"), None))), PermissionState::Prompt); assert_eq!(perms.net.query(Some(&NetDescriptor(Host::must_parse("127.0.0.1"), Some(8000)))), PermissionState::Granted); assert_eq!(perms.env.revoke(Some("HOME")), PermissionState::Prompt); assert_eq!(perms.env.revoke(Some("hostname")), PermissionState::Prompt); - assert_eq!(perms.run.revoke(Some("deno")), PermissionState::Prompt); + let run_query = RunQueryDescriptor::Path { + requested: "deno".to_string(), + resolved: PathBuf::from("/deno"), + }; + assert_eq!(perms.run.revoke(Some(&run_query)), PermissionState::Prompt); }; } @@ -2984,24 +3495,31 @@ mod tests { set_prompter(Box::new(TestPrompter)); let mut perms = Permissions::none_with_prompt(); let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock(); + let parser = TestPermissionDescriptorParser; + let read_query = + |path: &str| parser.parse_path_query(path).unwrap().into_read(); + let write_query = + |path: &str| parser.parse_path_query(path).unwrap().into_write(); + let ffi_query = + |path: &str| parser.parse_path_query(path).unwrap().into_ffi(); prompt_value.set(true); - assert!(perms.read.check(Path::new("/foo"), None).is_ok()); + assert!(perms.read.check(&read_query("/foo"), None).is_ok()); prompt_value.set(false); - assert!(perms.read.check(Path::new("/foo"), None).is_ok()); - assert!(perms.read.check(Path::new("/bar"), None).is_err()); + assert!(perms.read.check(&read_query("/foo"), None).is_ok()); + assert!(perms.read.check(&read_query("/bar"), None).is_err()); prompt_value.set(true); - assert!(perms.write.check(Path::new("/foo"), None).is_ok()); + assert!(perms.write.check(&write_query("/foo"), None).is_ok()); prompt_value.set(false); - assert!(perms.write.check(Path::new("/foo"), None).is_ok()); - assert!(perms.write.check(Path::new("/bar"), None).is_err()); + assert!(perms.write.check(&write_query("/foo"), None).is_ok()); + assert!(perms.write.check(&write_query("/bar"), None).is_err()); prompt_value.set(true); - assert!(perms.ffi.check(Path::new("/foo"), None).is_ok()); + assert!(perms.ffi.check(&ffi_query("/foo"), None).is_ok()); prompt_value.set(false); - assert!(perms.ffi.check(Path::new("/foo"), None).is_ok()); - assert!(perms.ffi.check(Path::new("/bar"), None).is_err()); + assert!(perms.ffi.check(&ffi_query("/foo"), None).is_ok()); + assert!(perms.ffi.check(&ffi_query("/bar"), None).is_err()); prompt_value.set(true); assert!(perms @@ -3048,9 +3566,9 @@ mod tests { assert!(perms .run .check( - RunPathQuery { - requested: "cat", - resolved: &cwd.join("cat") + &RunQueryDescriptor::Path { + requested: "cat".to_string(), + resolved: cwd.join("cat") }, None ) @@ -3059,9 +3577,9 @@ mod tests { assert!(perms .run .check( - RunPathQuery { - requested: "cat", - resolved: &cwd.join("cat") + &RunQueryDescriptor::Path { + requested: "cat".to_string(), + resolved: cwd.join("cat") }, None ) @@ -3069,9 +3587,9 @@ mod tests { assert!(perms .run .check( - RunPathQuery { - requested: "ls", - resolved: &cwd.join("ls") + &RunQueryDescriptor::Path { + requested: "ls".to_string(), + resolved: cwd.join("ls") }, None ) @@ -3095,30 +3613,37 @@ mod tests { set_prompter(Box::new(TestPrompter)); let mut perms = Permissions::none_with_prompt(); let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock(); + let parser = TestPermissionDescriptorParser; + let read_query = + |path: &str| parser.parse_path_query(path).unwrap().into_read(); + let write_query = + |path: &str| parser.parse_path_query(path).unwrap().into_write(); + let ffi_query = + |path: &str| parser.parse_path_query(path).unwrap().into_ffi(); prompt_value.set(false); - assert!(perms.read.check(Path::new("/foo"), None).is_err()); + assert!(perms.read.check(&read_query("/foo"), None).is_err()); prompt_value.set(true); - assert!(perms.read.check(Path::new("/foo"), None).is_err()); - assert!(perms.read.check(Path::new("/bar"), None).is_ok()); + assert!(perms.read.check(&read_query("/foo"), None).is_err()); + assert!(perms.read.check(&read_query("/bar"), None).is_ok()); prompt_value.set(false); - assert!(perms.read.check(Path::new("/bar"), None).is_ok()); + assert!(perms.read.check(&read_query("/bar"), None).is_ok()); prompt_value.set(false); - assert!(perms.write.check(Path::new("/foo"), None).is_err()); + assert!(perms.write.check(&write_query("/foo"), None).is_err()); prompt_value.set(true); - assert!(perms.write.check(Path::new("/foo"), None).is_err()); - assert!(perms.write.check(Path::new("/bar"), None).is_ok()); + assert!(perms.write.check(&write_query("/foo"), None).is_err()); + assert!(perms.write.check(&write_query("/bar"), None).is_ok()); prompt_value.set(false); - assert!(perms.write.check(Path::new("/bar"), None).is_ok()); + assert!(perms.write.check(&write_query("/bar"), None).is_ok()); prompt_value.set(false); - assert!(perms.ffi.check(Path::new("/foo"), None).is_err()); + assert!(perms.ffi.check(&ffi_query("/foo"), None).is_err()); prompt_value.set(true); - assert!(perms.ffi.check(Path::new("/foo"), None).is_err()); - assert!(perms.ffi.check(Path::new("/bar"), None).is_ok()); + assert!(perms.ffi.check(&ffi_query("/foo"), None).is_err()); + assert!(perms.ffi.check(&ffi_query("/bar"), None).is_ok()); prompt_value.set(false); - assert!(perms.ffi.check(Path::new("/bar"), None).is_ok()); + assert!(perms.ffi.check(&ffi_query("/bar"), None).is_ok()); prompt_value.set(false); assert!(perms @@ -3172,9 +3697,9 @@ mod tests { assert!(perms .run .check( - RunPathQuery { - requested: "cat", - resolved: &cwd.join("cat") + &RunQueryDescriptor::Path { + requested: "cat".to_string(), + resolved: cwd.join("cat") }, None ) @@ -3183,9 +3708,9 @@ mod tests { assert!(perms .run .check( - RunPathQuery { - requested: "cat", - resolved: &cwd.join("cat") + &RunQueryDescriptor::Path { + requested: "cat".to_string(), + resolved: cwd.join("cat") }, None ) @@ -3193,9 +3718,9 @@ mod tests { assert!(perms .run .check( - RunPathQuery { - requested: "ls", - resolved: &cwd.join("ls") + &RunQueryDescriptor::Path { + requested: "ls".to_string(), + resolved: cwd.join("ls") }, None ) @@ -3204,9 +3729,9 @@ mod tests { assert!(perms .run .check( - RunPathQuery { - requested: "ls", - resolved: &cwd.join("ls") + &RunQueryDescriptor::Path { + requested: "ls".to_string(), + resolved: cwd.join("ls") }, None ) @@ -3237,7 +3762,12 @@ mod tests { let mut perms = Permissions::allow_all(); perms.env = UnaryPermission { granted_global: false, - ..Permissions::new_unary(Some(&sarr!["HOME"]), None, false).unwrap() + ..Permissions::new_unary( + Some(HashSet::from([EnvDescriptor::new("HOME")])), + None, + false, + ) + .unwrap() }; prompt_value.set(true); @@ -3251,63 +3781,52 @@ mod tests { #[test] fn test_check_partial_denied() { - let mut perms = Permissions { - read: Permissions::new_unary( - Some(&[]), - Some(&[PathBuf::from("/foo/bar")]), - false, - ) - .unwrap(), - write: Permissions::new_unary( - Some(&[]), - Some(&[PathBuf::from("/foo/bar")]), - false, - ) - .unwrap(), - ..Permissions::none_without_prompt() - }; + let parser = TestPermissionDescriptorParser; + let mut perms = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_read: Some(vec![]), + deny_read: Some(svec!["/foo/bar"]), + allow_write: Some(vec![]), + deny_write: Some(svec!["/foo/bar"]), + ..Default::default() + }, + ) + .unwrap(); - perms.read.check_partial(Path::new("/foo"), None).unwrap(); - assert!(perms.read.check(Path::new("/foo"), None).is_err()); + let read_query = parser.parse_path_query("/foo").unwrap().into_read(); + perms.read.check_partial(&read_query, None).unwrap(); + assert!(perms.read.check(&read_query, None).is_err()); - perms.write.check_partial(Path::new("/foo"), None).unwrap(); - assert!(perms.write.check(Path::new("/foo"), None).is_err()); + let write_query = parser.parse_path_query("/foo").unwrap().into_write(); + perms.write.check_partial(&write_query, None).unwrap(); + assert!(perms.write.check(&write_query, None).is_err()); } #[test] fn test_net_fully_qualified_domain_name() { - let mut perms = Permissions { - net: Permissions::new_unary( - Some(&["allowed.domain".to_string(), "1.1.1.1".to_string()]), - Some(&["denied.domain".to_string(), "2.2.2.2".to_string()]), - false, - ) - .unwrap(), - ..Permissions::none_without_prompt() - }; + set_prompter(Box::new(TestPrompter)); + let parser = TestPermissionDescriptorParser; + let perms = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_net: Some(svec!["allowed.domain", "1.1.1.1"]), + deny_net: Some(svec!["denied.domain", "2.2.2.2"]), + ..Default::default() + }, + ) + .unwrap(); + let mut perms = PermissionsContainer::new(Arc::new(parser), perms); + let cases = [ + ("allowed.domain.", true), + ("1.1.1.1", true), + ("denied.domain.", false), + ("2.2.2.2", false), + ]; - perms - .net - .check( - &NetDescriptor(Host::must_parse("allowed.domain."), None), - None, - ) - .unwrap(); - perms - .net - .check(&NetDescriptor(Host::must_parse("1.1.1.1"), None), None) - .unwrap(); - assert!(perms - .net - .check( - &NetDescriptor(Host::must_parse("denied.domain."), None), - None - ) - .is_err()); - assert!(perms - .net - .check(&NetDescriptor(Host::must_parse("2.2.2.2"), None), None) - .is_err()); + for (host, is_ok) in cases { + assert_eq!(perms.check_net(&(host, None), "api").is_ok(), is_ok); + } } #[test] @@ -3443,14 +3962,19 @@ mod tests { #[test] fn test_create_child_permissions() { set_prompter(Box::new(TestPrompter)); - let mut main_perms = Permissions { - env: Permissions::new_unary(Some(&[]), None, false).unwrap(), - net: Permissions::new_unary(Some(&sarr!["foo", "bar"]), None, false) - .unwrap(), - ..Permissions::none_without_prompt() - }; + let parser = TestPermissionDescriptorParser; + let mut main_perms = Permissions::from_options( + &parser, + &PermissionsOptions { + allow_env: Some(vec![]), + allow_net: Some(svec!["foo", "bar"]), + ..Default::default() + }, + ) + .unwrap(); assert_eq!( create_child_permissions( + &parser, &mut main_perms.clone(), ChildPermissionsArg { env: ChildUnaryPermissionArg::Inherit, @@ -3461,12 +3985,18 @@ mod tests { ) .unwrap(), Permissions { - env: Permissions::new_unary(Some(&[]), None, false).unwrap(), - net: Permissions::new_unary(Some(&sarr!["foo"]), None, false).unwrap(), + env: Permissions::new_unary(Some(HashSet::new()), None, false).unwrap(), + net: Permissions::new_unary( + Some(HashSet::from([NetDescriptor::parse("foo").unwrap()])), + None, + false + ) + .unwrap(), ..Permissions::none_without_prompt() } ); assert!(create_child_permissions( + &parser, &mut main_perms.clone(), ChildPermissionsArg { net: ChildUnaryPermissionArg::Granted, @@ -3475,6 +4005,7 @@ mod tests { ) .is_err()); assert!(create_child_permissions( + &parser, &mut main_perms.clone(), ChildPermissionsArg { net: ChildUnaryPermissionArg::GrantedList(svec!["foo", "bar", "baz"]), @@ -3483,6 +4014,7 @@ mod tests { ) .is_err()); assert!(create_child_permissions( + &parser, &mut main_perms, ChildPermissionsArg { ffi: ChildUnaryPermissionArg::GrantedList(svec!["foo"]), @@ -3496,13 +4028,17 @@ mod tests { fn test_create_child_permissions_with_prompt() { set_prompter(Box::new(TestPrompter)); let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock(); - let mut main_perms = Permissions::from_options(&PermissionsOptions { - prompt: true, - ..Default::default() - }) + let mut main_perms = Permissions::from_options( + &TestPermissionDescriptorParser, + &PermissionsOptions { + prompt: true, + ..Default::default() + }, + ) .unwrap(); prompt_value.set(true); let worker_perms = create_child_permissions( + &TestPermissionDescriptorParser, &mut main_perms, ChildPermissionsArg { read: ChildUnaryPermissionArg::Granted, @@ -3515,8 +4051,8 @@ mod tests { assert_eq!( main_perms.run.granted_list, HashSet::from([ - RunDescriptor::Name("bar".to_owned()), - RunDescriptor::Name("foo".to_owned()) + AllowRunDescriptor(PathBuf::from("/bar")), + AllowRunDescriptor(PathBuf::from("/foo")), ]) ); } @@ -3525,14 +4061,22 @@ mod tests { fn test_create_child_permissions_with_inherited_denied_list() { set_prompter(Box::new(TestPrompter)); let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock(); - let mut main_perms = Permissions::from_options(&PermissionsOptions { - prompt: true, - ..Default::default() - }) + let parser = TestPermissionDescriptorParser; + let mut main_perms = Permissions::from_options( + &parser, + &PermissionsOptions { + prompt: true, + ..Default::default() + }, + ) .unwrap(); prompt_value.set(false); - assert!(main_perms.write.check(&PathBuf::from("foo"), None).is_err()); + assert!(main_perms + .write + .check(&parser.parse_path_query("foo").unwrap().into_write(), None) + .is_err()); let worker_perms = create_child_permissions( + &TestPermissionDescriptorParser, &mut main_perms.clone(), ChildPermissionsArg::none(), ) @@ -3544,30 +4088,6 @@ mod tests { } #[test] - fn test_handle_empty_value() { - set_prompter(Box::new(TestPrompter)); - - assert!(Permissions::new_unary::<ReadDescriptor>( - Some(&[Default::default()]), - None, - false - ) - .is_err()); - assert!(Permissions::new_unary::<EnvDescriptor>( - Some(&[Default::default()]), - None, - false - ) - .is_err()); - assert!(Permissions::new_unary::<NetDescriptor>( - Some(&[Default::default()]), - None, - false - ) - .is_err()); - } - - #[test] fn test_host_parse() { let hosts = &[ ("deno.land", Some(Host::Fqdn(fqdn!("deno.land")))), @@ -3671,4 +4191,35 @@ mod tests { assert_eq!(NetDescriptor::parse(input).ok(), *expected, "'{input}'"); } } + + #[test] + fn test_denies_run_name() { + let cases = [ + #[cfg(windows)] + ("deno", "C:\\deno.exe", true), + #[cfg(windows)] + ("deno", "C:\\sub\\deno.cmd", true), + #[cfg(windows)] + ("deno", "C:\\sub\\DeNO.cmd", true), + #[cfg(windows)] + ("DEno", "C:\\sub\\deno.cmd", true), + #[cfg(windows)] + ("deno", "C:\\other\\sub\\deno.batch", true), + #[cfg(windows)] + ("deno", "C:\\other\\sub\\deno", true), + #[cfg(windows)] + ("denort", "C:\\other\\sub\\deno.exe", false), + ("deno", "/home/test/deno", true), + ("deno", "/home/test/denot", false), + ]; + for (name, cmd_path, denies) in cases { + assert_eq!( + denies_run_name(name, &PathBuf::from(cmd_path)), + denies, + "{} {}", + name, + cmd_path + ); + } + } } |