From 015fa0bd41ce51afbee4a1413cb90534155c041f Mon Sep 17 00:00:00 2001 From: Nayeem Rahman Date: Tue, 18 Aug 2020 21:29:32 +0100 Subject: [PATCH] refactor: permissions (#7074) --- Cargo.lock | 2 +- cli/Cargo.toml | 3 - cli/op_error.rs | 8 +- cli/ops/permissions.rs | 59 +- cli/ops/timers.rs | 14 +- cli/permissions.rs | 1296 +++++++++-------- cli/state.rs | 5 + cli/tests/057_revoke_permissions.out | 11 - cli/tests/057_revoke_permissions.ts | 36 - cli/tests/061_permissions_request.ts | 6 + cli/tests/061_permissions_request.ts.out | 3 + cli/tests/062_permissions_request_global.ts | 6 + .../062_permissions_request_global.ts.out | 3 + cli/tests/063_permissions_revoke.ts | 6 + cli/tests/063_permissions_revoke.ts.out | 3 + cli/tests/064_permissions_revoke_global.ts | 6 + .../064_permissions_revoke_global.ts.out | 3 + cli/tests/integration_tests.rs | 40 +- docs/examples/permissions.md | 28 - docs/runtime/compiler_apis.md | 6 +- docs/runtime/permission_apis.md | 189 +++ docs/toc.json | 2 +- test_util/Cargo.toml | 3 + test_util/src/lib.rs | 37 +- 24 files changed, 1051 insertions(+), 724 deletions(-) delete mode 100644 cli/tests/057_revoke_permissions.out delete mode 100644 cli/tests/057_revoke_permissions.ts create mode 100644 cli/tests/061_permissions_request.ts create mode 100644 cli/tests/061_permissions_request.ts.out create mode 100644 cli/tests/062_permissions_request_global.ts create mode 100644 cli/tests/062_permissions_request_global.ts.out create mode 100644 cli/tests/063_permissions_revoke.ts create mode 100644 cli/tests/063_permissions_revoke.ts.out create mode 100644 cli/tests/064_permissions_revoke_global.ts create mode 100644 cli/tests/064_permissions_revoke_global.ts.out delete mode 100644 docs/examples/permissions.md create mode 100644 docs/runtime/permission_apis.md diff --git a/Cargo.lock b/Cargo.lock index 7bb83cb03c..cfbe630f18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,7 +346,6 @@ dependencies = [ "nix", "notify", "os_pipe", - "pty", "rand 0.7.3", "regex", "reqwest", @@ -2348,6 +2347,7 @@ dependencies = [ "futures", "lazy_static", "os_pipe", + "pty", "regex", "tempfile", "tokio", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index c59906b26c..c9371352f0 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -83,9 +83,6 @@ os_pipe = "0.9.2" tokio-tungstenite = { version = "0.10.1", features = ["connect"] } test_util = { path = "../test_util" } -[target.'cfg(unix)'.dev-dependencies] -pty = "0.2.2" - [package.metadata.winres] # This section defines the metadata that appears in the deno.exe PE header. OriginalFilename = "deno.exe" diff --git a/cli/op_error.rs b/cli/op_error.rs index 2633631221..32ad9c0f72 100644 --- a/cli/op_error.rs +++ b/cli/op_error.rs @@ -174,7 +174,13 @@ impl OpError { } pub fn invalid_domain_error() -> OpError { - OpError::new(ErrorKind::TypeError, "Invalid domain.".to_string()) + OpError::type_error("Invalid domain.".to_string()) + } + + pub fn permission_escalation_error() -> OpError { + OpError::permission_denied( + "Arguments escalate parent permissions.".to_string(), + ) } } diff --git a/cli/ops/permissions.rs b/cli/ops/permissions.rs index a4ee4120e7..571ea5b21c 100644 --- a/cli/ops/permissions.rs +++ b/cli/ops/permissions.rs @@ -35,12 +35,18 @@ pub fn op_query_permission( ) -> Result { let args: PermissionArgs = serde_json::from_value(args)?; let state = state.borrow(); + let permissions = &state.permissions; let path = args.path.as_deref(); - let perm = state.permissions.get_permission_state( - &args.name, - &args.url.as_deref(), - &path.as_deref().map(Path::new), - )?; + let perm = match args.name.as_ref() { + "read" => permissions.query_read(&path.as_deref().map(Path::new)), + "write" => permissions.query_write(&path.as_deref().map(Path::new)), + "net" => permissions.query_net_url(&args.url.as_deref())?, + "env" => permissions.query_env(), + "run" => permissions.query_run(), + "plugin" => permissions.query_plugin(), + "hrtime" => permissions.query_hrtime(), + n => return Err(OpError::other(format!("No such permission name: {}", n))), + }; Ok(JsonOp::Sync(json!({ "state": perm.to_string() }))) } @@ -52,22 +58,17 @@ pub fn op_revoke_permission( let args: PermissionArgs = serde_json::from_value(args)?; let mut state = state.borrow_mut(); let permissions = &mut state.permissions; - match args.name.as_ref() { - "run" => permissions.allow_run.revoke(), - "read" => permissions.allow_read.revoke(), - "write" => permissions.allow_write.revoke(), - "net" => permissions.allow_net.revoke(), - "env" => permissions.allow_env.revoke(), - "plugin" => permissions.allow_plugin.revoke(), - "hrtime" => permissions.allow_hrtime.revoke(), - _ => {} - }; let path = args.path.as_deref(); - let perm = permissions.get_permission_state( - &args.name, - &args.url.as_deref(), - &path.as_deref().map(Path::new), - )?; + let perm = match args.name.as_ref() { + "read" => permissions.revoke_read(&path.as_deref().map(Path::new)), + "write" => permissions.revoke_write(&path.as_deref().map(Path::new)), + "net" => permissions.revoke_net(&args.url.as_deref())?, + "env" => permissions.revoke_env(), + "run" => permissions.revoke_run(), + "plugin" => permissions.revoke_plugin(), + "hrtime" => permissions.revoke_hrtime(), + n => return Err(OpError::other(format!("No such permission name: {}", n))), + }; Ok(JsonOp::Sync(json!({ "state": perm.to_string() }))) } @@ -81,14 +82,14 @@ pub fn op_request_permission( let permissions = &mut state.permissions; let path = args.path.as_deref(); let perm = match args.name.as_ref() { - "run" => Ok(permissions.request_run()), - "read" => Ok(permissions.request_read(&path.as_deref().map(Path::new))), - "write" => Ok(permissions.request_write(&path.as_deref().map(Path::new))), - "net" => permissions.request_net(&args.url.as_deref()), - "env" => Ok(permissions.request_env()), - "plugin" => Ok(permissions.request_plugin()), - "hrtime" => Ok(permissions.request_hrtime()), - n => Err(OpError::other(format!("No such permission name: {}", n))), - }?; + "read" => permissions.request_read(&path.as_deref().map(Path::new)), + "write" => permissions.request_write(&path.as_deref().map(Path::new)), + "net" => permissions.request_net(&args.url.as_deref())?, + "env" => permissions.request_env(), + "run" => permissions.request_run(), + "plugin" => permissions.request_plugin(), + "hrtime" => permissions.request_hrtime(), + n => return Err(OpError::other(format!("No such permission name: {}", n))), + }; Ok(JsonOp::Sync(json!({ "state": perm.to_string() }))) } diff --git a/cli/ops/timers.rs b/cli/ops/timers.rs index 044c5ea4af..21efa8cf9f 100644 --- a/cli/ops/timers.rs +++ b/cli/ops/timers.rs @@ -59,16 +59,20 @@ fn op_now( _args: Value, _zero_copy: &mut [ZeroCopyBuf], ) -> Result { - let state = state.borrow(); - let seconds = state.start_time.elapsed().as_secs(); - let mut subsec_nanos = state.start_time.elapsed().subsec_nanos(); + let inner_state = state.borrow(); + let seconds = inner_state.start_time.elapsed().as_secs(); + let mut subsec_nanos = inner_state.start_time.elapsed().subsec_nanos(); let reduced_time_precision = 2_000_000; // 2ms in nanoseconds // If the permission is not enabled // Round the nano result on 2 milliseconds // see: https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#Reduced_time_precision - if !state.permissions.allow_hrtime.is_allow() { - subsec_nanos -= subsec_nanos % reduced_time_precision + if let Err(op_error) = state.check_hrtime() { + if op_error.kind_str == "PermissionDenied" { + subsec_nanos -= subsec_nanos % reduced_time_precision; + } else { + return Err(op_error); + } } Ok(JsonOp::Sync(json!({ diff --git a/cli/permissions.rs b/cli/permissions.rs index d0ff31be51..7d9fa1d45b 100644 --- a/cli/permissions.rs +++ b/cli/permissions.rs @@ -3,11 +3,11 @@ use crate::colors; use crate::flags::Flags; use crate::fs::resolve_from_cwd; use crate::op_error::OpError; -use serde::de; use serde::Deserialize; use std::collections::HashSet; use std::env::current_dir; use std::fmt; +use std::hash::Hash; #[cfg(not(test))] use std::io; use std::path::{Path, PathBuf}; @@ -22,65 +22,41 @@ use url::Url; const PERMISSION_EMOJI: &str = "⚠️"; /// Tri-state value for storing permission state -#[derive(PartialEq, Debug, Clone, Copy)] +#[derive(PartialEq, Debug, Clone, Copy, Deserialize)] pub enum PermissionState { - Allow = 0, - Ask = 1, - Deny = 2, + Granted = 0, + Prompt = 1, + Denied = 2, } impl PermissionState { - /// Checks the permission state and returns the result. - pub fn check(self, msg: &str, flag_name: &str) -> Result<(), OpError> { - if self == PermissionState::Allow { + /// Check the permission state. + fn check(self, msg: &str, flag_name: &str) -> Result<(), OpError> { + if self == PermissionState::Granted { log_perm_access(msg); return Ok(()); } let m = format!("{}, run again with the {} flag", msg, flag_name); Err(OpError::permission_denied(m)) } - pub fn is_allow(self) -> bool { - self == PermissionState::Allow - } - /// If the state is "Allow" walk it back to the default "Ask" - /// Don't do anything if state is "Deny" - pub fn revoke(&mut self) { - if *self == PermissionState::Allow { - *self = PermissionState::Ask; - } - } - /// Requests the permission. - pub fn request(&mut self, msg: &str) -> PermissionState { - if *self != PermissionState::Ask { - return *self; - } - if permission_prompt(msg) { - *self = PermissionState::Allow; - } else { - *self = PermissionState::Deny; - } - *self - } - pub fn fork(self, value: bool) -> Result { - if value && self == PermissionState::Deny { - Err(OpError::permission_denied( - "Arguments escalate parent permissions.".to_string(), - )) - } else if value { - Ok(PermissionState::Allow) - } else { - Ok(PermissionState::Deny) + /// Check that the permissions represented by `other` don't escalate ours. + fn check_fork(self, other: &Self) -> Result<(), OpError> { + if self == PermissionState::Denied && other != &PermissionState::Denied + || self == PermissionState::Prompt && other == &PermissionState::Granted + { + return Err(OpError::permission_escalation_error()); } + Ok(()) } } impl From for PermissionState { fn from(val: usize) -> Self { match val { - 0 => PermissionState::Allow, - 1 => PermissionState::Ask, - 2 => PermissionState::Deny, + 0 => PermissionState::Granted, + 1 => PermissionState::Prompt, + 2 => PermissionState::Denied, _ => unreachable!(), } } @@ -89,9 +65,9 @@ impl From for PermissionState { impl From for PermissionState { fn from(val: bool) -> Self { if val { - PermissionState::Allow + PermissionState::Granted } else { - PermissionState::Ask + PermissionState::Prompt } } } @@ -99,68 +75,49 @@ impl From for PermissionState { impl fmt::Display for PermissionState { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - PermissionState::Allow => f.pad("granted"), - PermissionState::Ask => f.pad("prompt"), - PermissionState::Deny => f.pad("denied"), + PermissionState::Granted => f.pad("granted"), + PermissionState::Prompt => f.pad("prompt"), + PermissionState::Denied => f.pad("denied"), } } } impl Default for PermissionState { fn default() -> Self { - PermissionState::Ask + PermissionState::Prompt } } -struct BoolPermVisitor; +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +pub struct UnaryPermission { + pub global_state: PermissionState, + pub granted_list: HashSet, + pub denied_list: HashSet, +} -fn deserialize_permission_state<'de, D>( - d: D, -) -> Result -where - D: de::Deserializer<'de>, -{ - impl<'de> de::Visitor<'de> for BoolPermVisitor { - type Value = PermissionState; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a boolean value") +impl UnaryPermission { + /// Check that the permissions represented by `other` don't escalate ours. + fn check_fork(&self, other: &Self) -> Result<(), OpError> { + self.global_state.check_fork(&other.global_state)?; + if !self.granted_list.is_superset(&other.granted_list) { + return Err(OpError::permission_escalation_error()); } - - fn visit_bool(self, value: bool) -> Result - where - E: de::Error, - { - if value { - Ok(PermissionState::Allow) - } else { - Ok(PermissionState::Deny) - } + if !self.denied_list.is_subset(&other.denied_list) { + return Err(OpError::permission_escalation_error()); } + Ok(()) } - d.deserialize_bool(BoolPermVisitor) } #[derive(Clone, Debug, Default, Deserialize, PartialEq)] pub struct Permissions { - // Keep in sync with cli/js/permissions.ts - #[serde(deserialize_with = "deserialize_permission_state")] - pub allow_read: PermissionState, - pub read_allowlist: HashSet, - #[serde(deserialize_with = "deserialize_permission_state")] - pub allow_write: PermissionState, - pub write_allowlist: HashSet, - #[serde(deserialize_with = "deserialize_permission_state")] - pub allow_net: PermissionState, - pub net_allowlist: HashSet, - #[serde(deserialize_with = "deserialize_permission_state")] - pub allow_env: PermissionState, - #[serde(deserialize_with = "deserialize_permission_state")] - pub allow_run: PermissionState, - #[serde(deserialize_with = "deserialize_permission_state")] - pub allow_plugin: PermissionState, - #[serde(deserialize_with = "deserialize_permission_state")] - pub allow_hrtime: PermissionState, + pub read: UnaryPermission, + pub write: UnaryPermission, + pub net: UnaryPermission, + pub env: PermissionState, + pub run: PermissionState, + pub plugin: PermissionState, + pub hrtime: PermissionState, } fn resolve_fs_allowlist(allowlist: &[PathBuf]) -> HashSet { @@ -173,16 +130,25 @@ fn resolve_fs_allowlist(allowlist: &[PathBuf]) -> HashSet { impl Permissions { pub fn from_flags(flags: &Flags) -> Self { Self { - allow_read: PermissionState::from(flags.allow_read), - read_allowlist: resolve_fs_allowlist(&flags.read_allowlist), - allow_write: PermissionState::from(flags.allow_write), - write_allowlist: resolve_fs_allowlist(&flags.write_allowlist), - allow_net: PermissionState::from(flags.allow_net), - net_allowlist: flags.net_allowlist.iter().cloned().collect(), - allow_env: PermissionState::from(flags.allow_env), - allow_run: PermissionState::from(flags.allow_run), - allow_plugin: PermissionState::from(flags.allow_plugin), - allow_hrtime: PermissionState::from(flags.allow_hrtime), + read: UnaryPermission:: { + global_state: PermissionState::from(flags.allow_read), + granted_list: resolve_fs_allowlist(&flags.read_allowlist), + ..Default::default() + }, + write: UnaryPermission:: { + global_state: PermissionState::from(flags.allow_write), + granted_list: resolve_fs_allowlist(&flags.write_allowlist), + ..Default::default() + }, + net: UnaryPermission:: { + global_state: PermissionState::from(flags.allow_net), + granted_list: flags.net_allowlist.iter().cloned().collect(), + ..Default::default() + }, + env: PermissionState::from(flags.allow_env), + run: PermissionState::from(flags.allow_run), + plugin: PermissionState::from(flags.allow_plugin), + hrtime: PermissionState::from(flags.allow_hrtime), } } @@ -194,7 +160,7 @@ impl Permissions { path.to_path_buf() } else { match self - .get_state_read(&Some(¤t_dir().unwrap())) + .query_read(&Some(¤t_dir().unwrap())) .check("", "") { Ok(_) => resolved_path.clone(), @@ -206,33 +172,363 @@ impl Permissions { pub fn allow_all() -> Self { Self { - allow_read: PermissionState::from(true), - allow_write: PermissionState::from(true), - allow_net: PermissionState::from(true), - allow_env: PermissionState::from(true), - allow_run: PermissionState::from(true), - allow_plugin: PermissionState::from(true), - allow_hrtime: PermissionState::from(true), - ..Default::default() + read: UnaryPermission { + global_state: PermissionState::Granted, + ..Default::default() + }, + write: UnaryPermission { + global_state: PermissionState::Granted, + ..Default::default() + }, + net: UnaryPermission { + global_state: PermissionState::Granted, + ..Default::default() + }, + env: PermissionState::Granted, + run: PermissionState::Granted, + plugin: PermissionState::Granted, + hrtime: PermissionState::Granted, } } - pub fn check_run(&self) -> Result<(), OpError> { - self - .allow_run - .check("access to run a subprocess", "--allow-run") + pub fn query_read(&self, path: &Option<&Path>) -> PermissionState { + let path = path.map(|p| resolve_from_cwd(p).unwrap()); + if self.read.global_state == PermissionState::Denied + && match path.as_ref() { + None => true, + Some(path) => check_path_blocklist(path, &self.read.denied_list), + } + { + return PermissionState::Denied; + } + if self.read.global_state == PermissionState::Granted + || match path.as_ref() { + None => false, + Some(path) => check_path_allowlist(path, &self.read.granted_list), + } + { + return PermissionState::Granted; + } + PermissionState::Prompt } - fn get_state_read(&self, path: &Option<&Path>) -> PermissionState { - if path.map_or(false, |f| check_path_white_list(f, &self.read_allowlist)) { - return PermissionState::Allow; + pub fn query_write(&self, path: &Option<&Path>) -> PermissionState { + let path = path.map(|p| resolve_from_cwd(p).unwrap()); + if self.write.global_state == PermissionState::Denied + && match path.as_ref() { + None => true, + Some(path) => check_path_blocklist(path, &self.write.denied_list), + } + { + return PermissionState::Denied; } - self.allow_read + if self.write.global_state == PermissionState::Granted + || match path.as_ref() { + None => false, + Some(path) => check_path_allowlist(path, &self.write.granted_list), + } + { + return PermissionState::Granted; + } + PermissionState::Prompt + } + + pub fn query_net(&self, host: &str, port: Option) -> PermissionState { + if self.net.global_state == PermissionState::Denied + || check_host_and_port_list(host, port, &self.net.denied_list) + { + return PermissionState::Denied; + } + if self.net.global_state == PermissionState::Granted + || check_host_and_port_list(host, port, &self.net.granted_list) + { + return PermissionState::Granted; + } + PermissionState::Prompt + } + + pub fn query_net_url( + &self, + url: &Option<&str>, + ) -> Result { + if url.is_none() { + return Ok(self.net.global_state); + } + let url: &str = url.unwrap(); + // If url is invalid, then throw a TypeError. + let parsed = Url::parse(url).map_err(OpError::from)?; + // The url may be parsed correctly but still lack a host, i.e. "localhost:235" or "mailto:someone@somewhere.com" or "file:/1.txt" + // Note that host:port combos are parsed as scheme:path + if parsed.host().is_none() { + return Err(OpError::uri_error( + "invalid url, expected format: ://[:port][/subpath]" + .to_owned(), + )); + } + Ok(self.query_net( + &format!("{}", parsed.host().unwrap()), + parsed.port_or_known_default(), + )) + } + + pub fn query_env(&self) -> PermissionState { + self.env + } + + pub fn query_run(&self) -> PermissionState { + self.run + } + + pub fn query_plugin(&self) -> PermissionState { + self.plugin + } + + pub fn query_hrtime(&self) -> PermissionState { + self.hrtime + } + + pub fn request_read(&mut self, path: &Option<&Path>) -> PermissionState { + if let Some(path) = path { + let (resolved_path, display_path) = self.resolved_and_display_path(path); + let state = self.query_read(&Some(&resolved_path)); + if state == PermissionState::Prompt { + if permission_prompt(&format!( + "Deno requests read access to \"{}\"", + display_path.display() + )) { + self + .read + .granted_list + .retain(|path| !path.starts_with(&resolved_path)); + self.read.granted_list.insert(resolved_path); + return PermissionState::Granted; + } else { + self + .read + .denied_list + .retain(|path| !resolved_path.starts_with(path)); + self.read.denied_list.insert(resolved_path); + self.read.global_state = PermissionState::Denied; + return PermissionState::Denied; + } + } + state + } else { + let state = self.query_read(&None); + if state == PermissionState::Prompt { + if permission_prompt("Deno requests read access") { + self.read.granted_list.clear(); + self.read.global_state = PermissionState::Granted; + return PermissionState::Granted; + } else { + self.read.global_state = PermissionState::Denied; + return PermissionState::Denied; + } + } + state + } + } + + pub fn request_write(&mut self, path: &Option<&Path>) -> PermissionState { + if let Some(path) = path { + let (resolved_path, display_path) = self.resolved_and_display_path(path); + let state = self.query_write(&Some(&resolved_path)); + if state == PermissionState::Prompt { + if permission_prompt(&format!( + "Deno requests write access to \"{}\"", + display_path.display() + )) { + self + .write + .granted_list + .retain(|path| !path.starts_with(&resolved_path)); + self.write.granted_list.insert(resolved_path); + return PermissionState::Granted; + } else { + self + .write + .denied_list + .retain(|path| !resolved_path.starts_with(path)); + self.write.denied_list.insert(resolved_path); + self.write.global_state = PermissionState::Denied; + return PermissionState::Denied; + } + } + state + } else { + let state = self.query_write(&None); + if state == PermissionState::Prompt { + if permission_prompt("Deno requests write access") { + self.write.granted_list.clear(); + self.write.global_state = PermissionState::Granted; + return PermissionState::Granted; + } else { + self.write.global_state = PermissionState::Denied; + return PermissionState::Denied; + } + } + state + } + } + + pub fn request_net( + &mut self, + url: &Option<&str>, + ) -> Result { + if let Some(url) = url { + let state = self.query_net_url(&Some(url))?; + if state == PermissionState::Prompt { + if permission_prompt(&format!( + "Deno requests network access to \"{}\"", + url + )) { + self.net.granted_list.insert(url.to_string()); + return Ok(PermissionState::Granted); + } else { + self.net.denied_list.insert(url.to_string()); + self.net.global_state = PermissionState::Denied; + return Ok(PermissionState::Denied); + } + } + Ok(state) + } else { + let state = self.query_net_url(&None)?; + if state == PermissionState::Prompt { + if permission_prompt("Deno requests network access") { + self.net.granted_list.clear(); + self.net.global_state = PermissionState::Granted; + return Ok(PermissionState::Granted); + } else { + self.net.global_state = PermissionState::Denied; + return Ok(PermissionState::Denied); + } + } + Ok(state) + } + } + + pub fn request_env(&mut self) -> PermissionState { + if self.env == PermissionState::Prompt { + if permission_prompt("Deno requests access to environment variables") { + self.env = PermissionState::Granted; + } else { + self.env = PermissionState::Denied; + } + } + self.env + } + + pub fn request_run(&mut self) -> PermissionState { + if self.run == PermissionState::Prompt { + if permission_prompt("Deno requests to access to run a subprocess") { + self.run = PermissionState::Granted; + } else { + self.run = PermissionState::Denied; + } + } + self.run + } + + pub fn request_plugin(&mut self) -> PermissionState { + if self.plugin == PermissionState::Prompt { + if permission_prompt("Deno requests to open plugins") { + self.plugin = PermissionState::Granted; + } else { + self.plugin = PermissionState::Denied; + } + } + self.plugin + } + + pub fn request_hrtime(&mut self) -> PermissionState { + if self.hrtime == PermissionState::Prompt { + if permission_prompt("Deno requests access to high precision time") { + self.hrtime = PermissionState::Granted; + } else { + self.hrtime = PermissionState::Denied; + } + } + self.hrtime + } + + pub fn revoke_read(&mut self, path: &Option<&Path>) -> PermissionState { + if let Some(path) = path { + let path = resolve_from_cwd(path).unwrap(); + self + .read + .granted_list + .retain(|path_| !path_.starts_with(&path)); + } else { + self.read.granted_list.clear(); + if self.read.global_state == PermissionState::Granted { + self.read.global_state = PermissionState::Prompt; + } + } + self.query_read(path) + } + + pub fn revoke_write(&mut self, path: &Option<&Path>) -> PermissionState { + if let Some(path) = path { + let path = resolve_from_cwd(path).unwrap(); + self + .write + .granted_list + .retain(|path_| !path_.starts_with(&path)); + } else { + self.write.granted_list.clear(); + if self.write.global_state == PermissionState::Granted { + self.write.global_state = PermissionState::Prompt; + } + } + self.query_write(path) + } + + pub fn revoke_net( + &mut self, + url: &Option<&str>, + ) -> Result { + if let Some(url) = url { + self.net.granted_list.remove(*url); + } else { + self.net.granted_list.clear(); + if self.net.global_state == PermissionState::Granted { + self.net.global_state = PermissionState::Prompt; + } + } + self.query_net_url(url) + } + + pub fn revoke_env(&mut self) -> PermissionState { + if self.env == PermissionState::Granted { + self.env = PermissionState::Prompt; + } + self.env + } + + pub fn revoke_run(&mut self) -> PermissionState { + if self.run == PermissionState::Granted { + self.run = PermissionState::Prompt; + } + self.run + } + + pub fn revoke_plugin(&mut self) -> PermissionState { + if self.plugin == PermissionState::Granted { + self.plugin = PermissionState::Prompt; + } + self.plugin + } + + pub fn revoke_hrtime(&mut self) -> PermissionState { + if self.hrtime == PermissionState::Granted { + self.hrtime = PermissionState::Prompt; + } + self.hrtime } pub fn check_read(&self, path: &Path) -> Result<(), OpError> { let (resolved_path, display_path) = self.resolved_and_display_path(path); - self.get_state_read(&Some(&resolved_path)).check( + self.query_read(&Some(&resolved_path)).check( &format!("read access to \"{}\"", display_path.display()), "--allow-read", ) @@ -247,58 +543,20 @@ impl Permissions { ) -> Result<(), OpError> { let resolved_path = resolve_from_cwd(path).unwrap(); self - .get_state_read(&Some(&resolved_path)) + .query_read(&Some(&resolved_path)) .check(&format!("read access to <{}>", display), "--allow-read") } - fn get_state_write(&self, path: &Option<&Path>) -> PermissionState { - if path.map_or(false, |f| check_path_white_list(f, &self.write_allowlist)) { - return PermissionState::Allow; - } - self.allow_write - } - pub fn check_write(&self, path: &Path) -> Result<(), OpError> { let (resolved_path, display_path) = self.resolved_and_display_path(path); - self.get_state_write(&Some(&resolved_path)).check( + self.query_write(&Some(&resolved_path)).check( &format!("write access to \"{}\"", display_path.display()), "--allow-write", ) } - fn get_state_net(&self, host: &str, port: Option) -> PermissionState { - if check_host_and_port_allowlist(host, port, &self.net_allowlist) { - return PermissionState::Allow; - } - self.allow_net - } - - fn get_state_net_url( - &self, - url: &Option<&str>, - ) -> Result { - if url.is_none() { - return Ok(self.allow_net); - } - let url: &str = url.unwrap(); - // If url is invalid, then throw a TypeError. - let parsed = Url::parse(url).map_err(OpError::from)?; - // The url may be parsed correctly but still lack a host, i.e. "localhost:235" or "mailto:someone@somewhere.com" or "file:/1.txt" - // Note that host:port combos are parsed as scheme:path - if parsed.host().is_none() { - return Err(OpError::uri_error( - "invalid url, expected format: ://[:port][/subpath]" - .to_owned(), - )); - } - Ok(self.get_state_net( - &format!("{}", parsed.host().unwrap()), - parsed.port_or_known_default(), - )) - } - pub fn check_net(&self, hostname: &str, port: u16) -> Result<(), OpError> { - self.get_state_net(hostname, Some(port)).check( + self.query_net(hostname, Some(port)).check( &format!("network access to \"{}:{}\"", hostname, port), "--allow-net", ) @@ -309,161 +567,61 @@ impl Permissions { .host_str() .ok_or_else(|| OpError::uri_error("missing host".to_owned()))?; self - .get_state_net(host, url.port_or_known_default()) + .query_net(host, url.port_or_known_default()) .check(&format!("network access to \"{}\"", url), "--allow-net") } pub fn check_env(&self) -> Result<(), OpError> { self - .allow_env + .env .check("access to environment variables", "--allow-env") } + pub fn check_run(&self) -> Result<(), OpError> { + self.run.check("access to run a subprocess", "--allow-run") + } + pub fn check_plugin(&self, path: &Path) -> Result<(), OpError> { let (_, display_path) = self.resolved_and_display_path(path); - self.allow_plugin.check( + self.plugin.check( &format!("access to open a plugin: {}", display_path.display()), "--allow-plugin", ) } - pub fn request_run(&mut self) -> PermissionState { + pub fn check_hrtime(&self) -> Result<(), OpError> { self - .allow_run - .request("Deno requests to access to run a subprocess") - } - - pub fn request_read(&mut self, path: &Option<&Path>) -> PermissionState { - let paths = path.map(|p| self.resolved_and_display_path(p)); - if let Some((p, _)) = paths.as_ref() { - if check_path_white_list(&p, &self.read_allowlist) { - return PermissionState::Allow; - } - }; - self.allow_read.request(&match paths { - None => "Deno requests read access".to_string(), - Some((_, display_path)) => format!( - "Deno requests read access to \"{}\"", - display_path.display() - ), - }) - } - - pub fn request_write(&mut self, path: &Option<&Path>) -> PermissionState { - let paths = path.map(|p| self.resolved_and_display_path(p)); - if let Some((p, _)) = paths.as_ref() { - if check_path_white_list(&p, &self.write_allowlist) { - return PermissionState::Allow; - } - }; - self.allow_write.request(&match paths { - None => "Deno requests write access".to_string(), - Some((_, display_path)) => format!( - "Deno requests write access to \"{}\"", - display_path.display() - ), - }) - } - - pub fn request_net( - &mut self, - url: &Option<&str>, - ) -> Result { - if self.get_state_net_url(url)? == PermissionState::Ask { - return Ok(self.allow_net.request(&match url { - None => "Deno requests network access".to_string(), - Some(url) => format!("Deno requests network access to \"{}\"", url), - })); - }; - self.get_state_net_url(url) - } - - pub fn request_env(&mut self) -> PermissionState { - self - .allow_env - .request("Deno requests to access to environment variables") - } - - pub fn request_hrtime(&mut self) -> PermissionState { - self - .allow_hrtime - .request("Deno requests to access to high precision time") - } - - pub fn request_plugin(&mut self) -> PermissionState { - self.allow_plugin.request("Deno requests to open plugins") - } - - pub fn get_permission_state( - &self, - name: &str, - url: &Option<&str>, - path: &Option<&Path>, - ) -> Result { - let path = path.map(|p| resolve_from_cwd(p).unwrap()); - let path = path.as_deref(); - match name { - "run" => Ok(self.allow_run), - "read" => Ok(self.get_state_read(&path)), - "write" => Ok(self.get_state_write(&path)), - "net" => self.get_state_net_url(url), - "env" => Ok(self.allow_env), - "plugin" => Ok(self.allow_plugin), - "hrtime" => Ok(self.allow_hrtime), - n => Err(OpError::other(format!("No such permission name: {}", n))), - } + .hrtime + .check("access to high precision time", "--allow-run") } #[allow(clippy::too_many_arguments)] pub fn fork( &self, - allow_read: bool, - read_allowlist: HashSet, - allow_write: bool, - write_allowlist: HashSet, - allow_net: bool, - net_allowlist: HashSet, - allow_env: bool, - allow_run: bool, - allow_plugin: bool, - allow_hrtime: bool, + read: UnaryPermission, + write: UnaryPermission, + net: UnaryPermission, + env: PermissionState, + run: PermissionState, + plugin: PermissionState, + hrtime: PermissionState, ) -> Result { - let allow_read = self.allow_read.fork(allow_read)?; - let allow_write = self.allow_write.fork(allow_write)?; - let allow_net = self.allow_net.fork(allow_net)?; - let allow_env = self.allow_env.fork(allow_env)?; - let allow_run = self.allow_run.fork(allow_run)?; - let allow_plugin = self.allow_plugin.fork(allow_plugin)?; - let allow_hrtime = self.allow_hrtime.fork(allow_hrtime)?; - if !(read_allowlist.is_subset(&self.read_allowlist)) { - Err(OpError::permission_denied(format!( - "Arguments escalate parent permissions. Parent Permissions have only {:?} in `read_allowlist`", - self.read_allowlist - ))) - } else if !(write_allowlist.is_subset(&self.write_allowlist)) { - Err(OpError::permission_denied(format!( - "Arguments escalate parent permissions. Parent Permissions have only {:?} in `write_allowlist`", - self.write_allowlist - ))) - } else if !(net_allowlist.is_subset(&self.net_allowlist)) { - Err(OpError::permission_denied(format!( - "Arguments escalate parent permissions. Parent Permissions have only {:?} in `net_allowlist`", - self.net_allowlist - ))) - } else { - Ok(Permissions { - allow_read, - read_allowlist, - allow_write, - write_allowlist, - allow_net, - net_allowlist, - allow_env, - allow_run, - allow_plugin, - allow_hrtime, - }) - } + self.read.check_fork(&read)?; + self.write.check_fork(&write)?; + self.net.check_fork(&net)?; + self.env.check_fork(&env)?; + self.run.check_fork(&run)?; + self.plugin.check_fork(&plugin)?; + self.hrtime.check_fork(&hrtime)?; + Ok(Permissions { + read, + write, + net, + env, + run, + plugin, + hrtime, + }) } } @@ -529,20 +687,25 @@ fn log_perm_access(message: &str) { ); } -fn check_path_white_list(path: &Path, white_list: &HashSet) -> bool { - let mut path_buf = PathBuf::from(path); - loop { - if white_list.contains(&path_buf) { +fn check_path_allowlist(path: &Path, allowlist: &HashSet) -> bool { + for path_ in allowlist { + if path.starts_with(path_) { return true; } - if !path_buf.pop() { - break; - } } false } -fn check_host_and_port_allowlist( +fn check_path_blocklist(path: &Path, blocklist: &HashSet) -> bool { + for path_ in blocklist { + if path_.starts_with(path) { + return true; + } + } + false +} + +fn check_host_and_port_list( host: &str, port: Option, allowlist: &HashSet, @@ -708,248 +871,48 @@ mod tests { } } - #[test] - fn test_permissions_request_run() { - let _guard = PERMISSION_PROMPT_GUARD.lock().unwrap(); - let mut perms0 = Permissions::from_flags(&Flags { - ..Default::default() - }); - set_prompt_result(true); - assert_eq!(perms0.request_run(), PermissionState::Allow); - - let mut perms1 = Permissions::from_flags(&Flags { - ..Default::default() - }); - set_prompt_result(false); - assert_eq!(perms1.request_run(), PermissionState::Deny); - } - - #[test] - fn test_permissions_request_read() { - let _guard = PERMISSION_PROMPT_GUARD.lock().unwrap(); - let allowlist = vec![PathBuf::from("/foo/bar")]; - let mut perms0 = Permissions::from_flags(&Flags { - read_allowlist: allowlist.clone(), - ..Default::default() - }); - set_prompt_result(false); - // If the allowlist contains the path, then the result is `allow` - // regardless of prompt result - assert_eq!( - perms0.request_read(&Some(Path::new("/foo/bar"))), - PermissionState::Allow - ); - - let mut perms1 = Permissions::from_flags(&Flags { - read_allowlist: allowlist.clone(), - ..Default::default() - }); - set_prompt_result(true); - assert_eq!( - perms1.request_read(&Some(Path::new("/foo/baz"))), - PermissionState::Allow - ); - - let mut perms2 = Permissions::from_flags(&Flags { - read_allowlist: allowlist, - ..Default::default() - }); - set_prompt_result(false); - assert_eq!( - perms2.request_read(&Some(Path::new("/foo/baz"))), - PermissionState::Deny - ); - } - - #[test] - fn test_permissions_request_write() { - let _guard = PERMISSION_PROMPT_GUARD.lock().unwrap(); - let allowlist = vec![PathBuf::from("/foo/bar")]; - let mut perms0 = Permissions::from_flags(&Flags { - write_allowlist: allowlist.clone(), - ..Default::default() - }); - set_prompt_result(false); - // If the allowlist contains the path, then the result is `allow` - // regardless of prompt result - assert_eq!( - perms0.request_write(&Some(Path::new("/foo/bar"))), - PermissionState::Allow - ); - - let mut perms1 = Permissions::from_flags(&Flags { - write_allowlist: allowlist.clone(), - ..Default::default() - }); - set_prompt_result(true); - assert_eq!( - perms1.request_write(&Some(Path::new("/foo/baz"))), - PermissionState::Allow - ); - - let mut perms2 = Permissions::from_flags(&Flags { - write_allowlist: allowlist, - ..Default::default() - }); - set_prompt_result(false); - assert_eq!( - perms2.request_write(&Some(Path::new("/foo/baz"))), - PermissionState::Deny - ); - } - - #[test] - fn test_permission_request_net() { - let _guard = PERMISSION_PROMPT_GUARD.lock().unwrap(); - let allowlist = svec!["localhost:8080"]; - - let mut perms0 = Permissions::from_flags(&Flags { - net_allowlist: allowlist.clone(), - ..Default::default() - }); - set_prompt_result(false); - // If the url matches the allowlist item, then the result is `allow` - // regardless of prompt result - assert_eq!( - perms0 - .request_net(&Some("http://localhost:8080/")) - .expect("Testing expect"), - PermissionState::Allow - ); - - let mut perms1 = Permissions::from_flags(&Flags { - net_allowlist: allowlist.clone(), - ..Default::default() - }); - set_prompt_result(true); - assert_eq!( - perms1 - .request_net(&Some("http://deno.land/")) - .expect("Testing expect"), - PermissionState::Allow - ); - - let mut perms2 = Permissions::from_flags(&Flags { - net_allowlist: allowlist.clone(), - ..Default::default() - }); - set_prompt_result(false); - assert_eq!( - perms2 - .request_net(&Some("http://deno.land/")) - .expect("Testing expect"), - PermissionState::Deny - ); - - let mut perms3 = Permissions::from_flags(&Flags { - net_allowlist: allowlist.clone(), - ..Default::default() - }); - set_prompt_result(true); - assert!(perms3.request_net(&Some(":")).is_err()); - - let mut perms4 = Permissions::from_flags(&Flags { - net_allowlist: allowlist.clone(), - ..Default::default() - }); - set_prompt_result(false); - assert_eq!( - perms4 - .request_net(&Some("localhost:8080")) - .unwrap_err() - .kind_str, - "URIError" - ); - - let mut perms5 = Permissions::from_flags(&Flags { - net_allowlist: allowlist, - ..Default::default() - }); - set_prompt_result(false); - assert_eq!( - perms5 - .request_net(&Some("file:/1.txt")) - .unwrap_err() - .kind_str, - "URIError" - ); - } - - #[test] - fn test_permissions_request_env() { - let _guard = PERMISSION_PROMPT_GUARD.lock().unwrap(); - let mut perms0 = Permissions::from_flags(&Flags { - ..Default::default() - }); - set_prompt_result(true); - assert_eq!(perms0.request_env(), PermissionState::Allow); - - let mut perms1 = Permissions::from_flags(&Flags { - ..Default::default() - }); - set_prompt_result(false); - assert_eq!(perms1.request_env(), PermissionState::Deny); - } - - #[test] - fn test_permissions_request_plugin() { - let _guard = PERMISSION_PROMPT_GUARD.lock().unwrap(); - let mut perms0 = Permissions::from_flags(&Flags { - ..Default::default() - }); - set_prompt_result(true); - assert_eq!(perms0.request_plugin(), PermissionState::Allow); - - let mut perms1 = Permissions::from_flags(&Flags { - ..Default::default() - }); - set_prompt_result(false); - assert_eq!(perms1.request_plugin(), PermissionState::Deny); - } - - #[test] - fn test_permissions_request_hrtime() { - let _guard = PERMISSION_PROMPT_GUARD.lock().unwrap(); - let mut perms0 = Permissions::from_flags(&Flags { - ..Default::default() - }); - set_prompt_result(true); - assert_eq!(perms0.request_hrtime(), PermissionState::Allow); - - let mut perms1 = Permissions::from_flags(&Flags { - ..Default::default() - }); - set_prompt_result(false); - assert_eq!(perms1.request_hrtime(), PermissionState::Deny); - } - #[test] fn test_deserialize_perms() { let json_perms = r#" { - "allow_read": true, - "read_allowlist": [], - "allow_write": true, - "write_allowlist": [], - "allow_net": true, - "net_allowlist": [], - "allow_env": true, - "allow_run": true, - "allow_plugin": true, - "allow_hrtime": true + "read": { + "global_state": "Granted", + "granted_list": [], + "denied_list": [] + }, + "write": { + "global_state": "Granted", + "granted_list": [], + "denied_list": [] + }, + "net": { + "global_state": "Granted", + "granted_list": [], + "denied_list": [] + }, + "env": "Granted", + "run": "Granted", + "plugin": "Granted", + "hrtime": "Granted" } "#; let perms0 = Permissions { - allow_read: PermissionState::Allow, - allow_write: PermissionState::Allow, - allow_net: PermissionState::Allow, - allow_hrtime: PermissionState::Allow, - allow_env: PermissionState::Allow, - allow_plugin: PermissionState::Allow, - allow_run: PermissionState::Allow, - read_allowlist: HashSet::new(), - write_allowlist: HashSet::new(), - net_allowlist: HashSet::new(), + read: UnaryPermission { + global_state: PermissionState::Granted, + ..Default::default() + }, + write: UnaryPermission { + global_state: PermissionState::Granted, + ..Default::default() + }, + net: UnaryPermission { + global_state: PermissionState::Granted, + ..Default::default() + }, + env: PermissionState::Granted, + run: PermissionState::Granted, + hrtime: PermissionState::Granted, + plugin: PermissionState::Granted, }; let deserialized_perms: Permissions = serde_json::from_str(json_perms).unwrap(); @@ -958,67 +921,208 @@ mod tests { #[test] fn test_fork() { - let _guard = PERMISSION_PROMPT_GUARD.lock().unwrap(); - let perms0 = Permissions::from_flags(&Flags { - ..Default::default() - }); - set_prompt_result(true); - assert_eq!( - perms0 - .fork( - true, - HashSet::new(), - true, - HashSet::new(), - true, - HashSet::new(), - true, - true, - false, - false, - ) - .expect("Testing expect"), - Permissions { - allow_read: PermissionState::Allow, - read_allowlist: HashSet::new(), - allow_write: PermissionState::Allow, - write_allowlist: HashSet::new(), - allow_net: PermissionState::Allow, - net_allowlist: HashSet::new(), - allow_env: PermissionState::Allow, - allow_run: PermissionState::Allow, - allow_plugin: PermissionState::Deny, - allow_hrtime: PermissionState::Deny, - } - ); - set_prompt_result(false); - assert_eq!( - perms0 - .fork( - true, - HashSet::new(), - true, - HashSet::new(), - true, - HashSet::new(), - true, - true, - false, - false, - ) - .expect("Testing expect"), - Permissions { - allow_read: PermissionState::Allow, - read_allowlist: HashSet::new(), - allow_write: PermissionState::Allow, - write_allowlist: HashSet::new(), - allow_net: PermissionState::Allow, - net_allowlist: HashSet::new(), - allow_env: PermissionState::Allow, - allow_run: PermissionState::Allow, - allow_plugin: PermissionState::Deny, - allow_hrtime: PermissionState::Deny, - } - ); + let perms0 = Permissions::from_flags(&Flags::default()); + perms0 + .fork( + UnaryPermission { + global_state: PermissionState::Prompt, + ..Default::default() + }, + UnaryPermission { + global_state: PermissionState::Prompt, + ..Default::default() + }, + UnaryPermission { + global_state: PermissionState::Prompt, + ..Default::default() + }, + PermissionState::Prompt, + PermissionState::Prompt, + PermissionState::Denied, + PermissionState::Denied, + ) + .expect("Fork should succeed."); + perms0 + .fork( + UnaryPermission { + global_state: PermissionState::Granted, + ..Default::default() + }, + UnaryPermission { + global_state: PermissionState::Granted, + ..Default::default() + }, + UnaryPermission { + global_state: PermissionState::Granted, + ..Default::default() + }, + PermissionState::Granted, + PermissionState::Granted, + PermissionState::Denied, + PermissionState::Denied, + ) + .expect_err("Fork should fail."); + } + + #[test] + fn test_query() { + let perms1 = Permissions { + read: UnaryPermission { + global_state: PermissionState::Granted, + ..Default::default() + }, + write: UnaryPermission { + global_state: PermissionState::Granted, + ..Default::default() + }, + net: UnaryPermission { + global_state: PermissionState::Granted, + ..Default::default() + }, + env: PermissionState::Granted, + run: PermissionState::Granted, + plugin: PermissionState::Granted, + hrtime: PermissionState::Granted, + }; + let perms2 = Permissions { + read: UnaryPermission { + global_state: PermissionState::Prompt, + granted_list: resolve_fs_allowlist(&[PathBuf::from("/foo")]), + ..Default::default() + }, + write: UnaryPermission { + global_state: PermissionState::Prompt, + granted_list: resolve_fs_allowlist(&[PathBuf::from("/foo")]), + ..Default::default() + }, + net: UnaryPermission { + global_state: PermissionState::Prompt, + granted_list: ["127.0.0.1:8000".to_string()].iter().cloned().collect(), + ..Default::default() + }, + env: PermissionState::Prompt, + run: PermissionState::Prompt, + plugin: PermissionState::Prompt, + hrtime: PermissionState::Prompt, + }; + #[rustfmt::skip] + { + assert_eq!(perms1.query_read(&None), PermissionState::Granted); + assert_eq!(perms1.query_read(&Some(&Path::new("/foo"))), PermissionState::Granted); + assert_eq!(perms2.query_read(&None), PermissionState::Prompt); + assert_eq!(perms2.query_read(&Some(&Path::new("/foo"))), PermissionState::Granted); + assert_eq!(perms2.query_read(&Some(&Path::new("/foo/bar"))), PermissionState::Granted); + assert_eq!(perms1.query_write(&None), PermissionState::Granted); + assert_eq!(perms1.query_write(&Some(&Path::new("/foo"))), PermissionState::Granted); + assert_eq!(perms2.query_write(&None), PermissionState::Prompt); + assert_eq!(perms2.query_write(&Some(&Path::new("/foo"))), PermissionState::Granted); + assert_eq!(perms2.query_write(&Some(&Path::new("/foo/bar"))), PermissionState::Granted); + assert_eq!(perms1.query_net_url(&None).unwrap(), PermissionState::Granted); + assert_eq!(perms1.query_net_url(&Some("http://127.0.0.1:8000")).unwrap(), PermissionState::Granted); + assert_eq!(perms2.query_net_url(&None).unwrap(), PermissionState::Prompt); + assert_eq!(perms2.query_net_url(&Some("http://127.0.0.1:8000")).unwrap(), PermissionState::Granted); + assert_eq!(perms1.query_env(), PermissionState::Granted); + assert_eq!(perms2.query_env(), PermissionState::Prompt); + assert_eq!(perms1.query_run(), PermissionState::Granted); + assert_eq!(perms2.query_run(), PermissionState::Prompt); + assert_eq!(perms1.query_plugin(), PermissionState::Granted); + assert_eq!(perms2.query_plugin(), PermissionState::Prompt); + assert_eq!(perms1.query_hrtime(), PermissionState::Granted); + assert_eq!(perms2.query_hrtime(), PermissionState::Prompt); + }; + } + + #[test] + fn test_request() { + let mut perms = Permissions { + read: UnaryPermission { + global_state: PermissionState::Prompt, + ..Default::default() + }, + write: UnaryPermission { + global_state: PermissionState::Prompt, + ..Default::default() + }, + net: UnaryPermission { + global_state: PermissionState::Prompt, + ..Default::default() + }, + env: PermissionState::Prompt, + run: PermissionState::Prompt, + plugin: PermissionState::Prompt, + hrtime: PermissionState::Prompt, + }; + #[rustfmt::skip] + { + let _guard = PERMISSION_PROMPT_GUARD.lock().unwrap(); + set_prompt_result(true); + assert_eq!(perms.request_read(&Some(&Path::new("/foo"))), PermissionState::Granted); + assert_eq!(perms.query_read(&None), PermissionState::Prompt); + set_prompt_result(false); + assert_eq!(perms.request_read(&Some(&Path::new("/foo/bar"))), PermissionState::Granted); + set_prompt_result(false); + assert_eq!(perms.request_write(&Some(&Path::new("/foo"))), PermissionState::Denied); + assert_eq!(perms.query_write(&Some(&Path::new("/foo/bar"))), PermissionState::Prompt); + set_prompt_result(true); + assert_eq!(perms.request_write(&None), PermissionState::Denied); + set_prompt_result(true); + assert_eq!(perms.request_net(&None).unwrap(), PermissionState::Granted); + set_prompt_result(false); + assert_eq!(perms.request_net(&Some("http://127.0.0.1:8000")).unwrap(), PermissionState::Granted); + set_prompt_result(true); + assert_eq!(perms.request_env(), PermissionState::Granted); + set_prompt_result(false); + assert_eq!(perms.request_env(), PermissionState::Granted); + set_prompt_result(false); + assert_eq!(perms.request_run(), PermissionState::Denied); + set_prompt_result(true); + assert_eq!(perms.request_run(), PermissionState::Denied); + set_prompt_result(true); + assert_eq!(perms.request_plugin(), PermissionState::Granted); + set_prompt_result(false); + assert_eq!(perms.request_plugin(), PermissionState::Granted); + set_prompt_result(false); + assert_eq!(perms.request_hrtime(), PermissionState::Denied); + set_prompt_result(true); + assert_eq!(perms.request_hrtime(), PermissionState::Denied); + }; + } + + #[test] + fn test_revoke() { + let mut perms = Permissions { + read: UnaryPermission { + global_state: PermissionState::Prompt, + granted_list: resolve_fs_allowlist(&[PathBuf::from("/foo")]), + ..Default::default() + }, + write: UnaryPermission { + global_state: PermissionState::Prompt, + granted_list: resolve_fs_allowlist(&[PathBuf::from("/foo")]), + ..Default::default() + }, + net: UnaryPermission { + global_state: PermissionState::Denied, + ..Default::default() + }, + env: PermissionState::Granted, + run: PermissionState::Granted, + plugin: PermissionState::Prompt, + hrtime: PermissionState::Denied, + }; + #[rustfmt::skip] + { + assert_eq!(perms.revoke_read(&Some(&Path::new("/foo/bar"))), PermissionState::Granted); + assert_eq!(perms.revoke_read(&Some(&Path::new("/foo"))), PermissionState::Prompt); + assert_eq!(perms.query_read(&Some(&Path::new("/foo/bar"))), PermissionState::Prompt); + assert_eq!(perms.revoke_write(&Some(&Path::new("/foo/bar"))), PermissionState::Granted); + assert_eq!(perms.revoke_write(&None), PermissionState::Prompt); + assert_eq!(perms.query_write(&Some(&Path::new("/foo/bar"))), PermissionState::Prompt); + assert_eq!(perms.revoke_net(&None).unwrap(), PermissionState::Denied); + assert_eq!(perms.revoke_env(), PermissionState::Prompt); + assert_eq!(perms.revoke_run(), PermissionState::Prompt); + assert_eq!(perms.revoke_plugin(), PermissionState::Prompt); + assert_eq!(perms.revoke_hrtime(), PermissionState::Denied); + }; } } diff --git a/cli/state.rs b/cli/state.rs index f485bd61f1..5754e8c9d5 100644 --- a/cli/state.rs +++ b/cli/state.rs @@ -560,6 +560,11 @@ impl State { self.borrow().permissions.check_run() } + #[inline] + pub fn check_hrtime(&self) -> Result<(), OpError> { + self.borrow().permissions.check_hrtime() + } + #[inline] pub fn check_plugin(&self, filename: &Path) -> Result<(), OpError> { self.borrow().permissions.check_plugin(filename) diff --git a/cli/tests/057_revoke_permissions.out b/cli/tests/057_revoke_permissions.out deleted file mode 100644 index 8ea5692363..0000000000 --- a/cli/tests/057_revoke_permissions.out +++ /dev/null @@ -1,11 +0,0 @@ -[WILDCARD] -running 7 tests -test runGranted ... ok [WILDCARD] -test readGranted ... ok [WILDCARD] -test writeGranted ... ok [WILDCARD] -test netGranted ... ok [WILDCARD] -test envGranted ... ok [WILDCARD] -test pluginGranted ... ok [WILDCARD] -test hrtimeGranted ... ok [WILDCARD] - -test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out [WILDCARD] diff --git a/cli/tests/057_revoke_permissions.ts b/cli/tests/057_revoke_permissions.ts deleted file mode 100644 index de8deecb4c..0000000000 --- a/cli/tests/057_revoke_permissions.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -const knownPermissions: Deno.PermissionName[] = [ - "run", - "read", - "write", - "net", - "env", - "plugin", - "hrtime", -]; - -export function assert(cond: unknown): asserts cond { - if (!cond) { - throw Error("Assertion failed"); - } -} - -function genFunc(grant: Deno.PermissionName): [string, () => Promise] { - const gen: () => Promise = async function Granted(): Promise { - const status0 = await Deno.permissions.query({ name: grant }); - assert(status0 != null); - assert(status0.state === "granted"); - - const status1 = await Deno.permissions.revoke({ name: grant }); - assert(status1 != null); - assert(status1.state === "prompt"); - }; - const name = grant + "Granted"; - return [name, gen]; -} - -for (const grant of knownPermissions) { - const [name, fn] = genFunc(grant); - Deno.test(name, fn); -} diff --git a/cli/tests/061_permissions_request.ts b/cli/tests/061_permissions_request.ts new file mode 100644 index 0000000000..8fdc2c5904 --- /dev/null +++ b/cli/tests/061_permissions_request.ts @@ -0,0 +1,6 @@ +const status1 = await Deno.permissions.request({ name: "read", path: "foo" }); +const status2 = await Deno.permissions.query({ name: "read", path: "bar" }); +const status3 = await Deno.permissions.request({ name: "read", path: "bar" }); +console.log(status1); +console.log(status2); +console.log(status3); diff --git a/cli/tests/061_permissions_request.ts.out b/cli/tests/061_permissions_request.ts.out new file mode 100644 index 0000000000..de058a9a36 --- /dev/null +++ b/cli/tests/061_permissions_request.ts.out @@ -0,0 +1,3 @@ +[WILDCARD]PermissionStatus { state: "granted" } +PermissionStatus { state: "prompt" } +PermissionStatus { state: "denied" } diff --git a/cli/tests/062_permissions_request_global.ts b/cli/tests/062_permissions_request_global.ts new file mode 100644 index 0000000000..4ed98ff641 --- /dev/null +++ b/cli/tests/062_permissions_request_global.ts @@ -0,0 +1,6 @@ +const status1 = await Deno.permissions.request({ name: "read" }); +const status2 = await Deno.permissions.query({ name: "read", path: "foo" }); +const status3 = await Deno.permissions.query({ name: "read", path: "bar" }); +console.log(status1); +console.log(status2); +console.log(status3); diff --git a/cli/tests/062_permissions_request_global.ts.out b/cli/tests/062_permissions_request_global.ts.out new file mode 100644 index 0000000000..69b5ee50d3 --- /dev/null +++ b/cli/tests/062_permissions_request_global.ts.out @@ -0,0 +1,3 @@ +[WILDCARD]PermissionStatus { state: "granted" } +PermissionStatus { state: "granted" } +PermissionStatus { state: "granted" } diff --git a/cli/tests/063_permissions_revoke.ts b/cli/tests/063_permissions_revoke.ts new file mode 100644 index 0000000000..e618836934 --- /dev/null +++ b/cli/tests/063_permissions_revoke.ts @@ -0,0 +1,6 @@ +const status1 = await Deno.permissions.revoke({ name: "read", path: "foo" }); +const status2 = await Deno.permissions.query({ name: "read", path: "bar" }); +const status3 = await Deno.permissions.revoke({ name: "read", path: "bar" }); +console.log(status1); +console.log(status2); +console.log(status3); diff --git a/cli/tests/063_permissions_revoke.ts.out b/cli/tests/063_permissions_revoke.ts.out new file mode 100644 index 0000000000..803893e9c0 --- /dev/null +++ b/cli/tests/063_permissions_revoke.ts.out @@ -0,0 +1,3 @@ +[WILDCARD]PermissionStatus { state: "prompt" } +PermissionStatus { state: "granted" } +PermissionStatus { state: "prompt" } diff --git a/cli/tests/064_permissions_revoke_global.ts b/cli/tests/064_permissions_revoke_global.ts new file mode 100644 index 0000000000..efe74b8280 --- /dev/null +++ b/cli/tests/064_permissions_revoke_global.ts @@ -0,0 +1,6 @@ +const status1 = await Deno.permissions.revoke({ name: "read" }); +const status2 = await Deno.permissions.query({ name: "read", path: "foo" }); +const status3 = await Deno.permissions.query({ name: "read", path: "bar" }); +console.log(status1); +console.log(status2); +console.log(status3); diff --git a/cli/tests/064_permissions_revoke_global.ts.out b/cli/tests/064_permissions_revoke_global.ts.out new file mode 100644 index 0000000000..a2ea05cb7f --- /dev/null +++ b/cli/tests/064_permissions_revoke_global.ts.out @@ -0,0 +1,3 @@ +[WILDCARD]PermissionStatus { state: "prompt" } +PermissionStatus { state: "prompt" } +PermissionStatus { state: "prompt" } diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index 206664113b..690b30798d 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -1,8 +1,6 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. #[cfg(unix)] extern crate nix; -#[cfg(unix)] -extern crate pty; extern crate tempfile; use test_util as util; @@ -166,8 +164,8 @@ fn no_color() { #[test] #[ignore] pub fn test_raw_tty() { - use pty::fork::*; use std::io::{Read, Write}; + use util::pty::fork::*; let fork = Fork::from_ptmx().unwrap(); @@ -1581,12 +1579,6 @@ itest!(_056_make_temp_file_write_perm { output: "056_make_temp_file_write_perm.out", }); -// TODO(lucacasonato): remove --unstable when permissions goes stable -itest!(_057_revoke_permissions { - args: "test -A --unstable 057_revoke_permissions.ts", - output: "057_revoke_permissions.out", -}); - itest!(_058_tasks_microtasks_close { args: "run --quiet 058_tasks_microtasks_close.ts", output: "058_tasks_microtasks_close.ts.out", @@ -1603,6 +1595,36 @@ itest!(_060_deno_doc_displays_all_overloads_in_details_view { output: "060_deno_doc_displays_all_overloads_in_details_view.ts.out", }); +#[cfg(unix)] +#[test] +fn _061_permissions_request() { + let args = "run --unstable 061_permissions_request.ts"; + let output = "061_permissions_request.ts.out"; + let input = b"g\nd\n"; + + util::test_pty(args, output, input); +} + +#[cfg(unix)] +#[test] +fn _062_permissions_request_global() { + let args = "run --unstable 062_permissions_request_global.ts"; + let output = "062_permissions_request_global.ts.out"; + let input = b"g\n"; + + util::test_pty(args, output, input); +} + +itest!(_063_permissions_revoke { + args: "run --unstable --allow-read=foo,bar 063_permissions_revoke.ts", + output: "063_permissions_revoke.ts.out", +}); + +itest!(_064_permissions_revoke_global { + args: "run --unstable --allow-read=foo,bar 064_permissions_revoke_global.ts", + output: "064_permissions_revoke_global.ts.out", +}); + itest!(js_import_detect { args: "run --quiet --reload js_import_detect.ts", output: "js_import_detect.ts.out", diff --git a/docs/examples/permissions.md b/docs/examples/permissions.md deleted file mode 100644 index 78dfcc59db..0000000000 --- a/docs/examples/permissions.md +++ /dev/null @@ -1,28 +0,0 @@ -## Inspecting and revoking permissions - -> This program makes use of an unstable Deno feature. Learn more about -> [unstable features](../runtime/stability.md). - -Sometimes a program may want to revoke previously granted permissions. When a -program, at a later stage, needs those permissions, it will fail. - -```ts -// lookup a permission -const status = await Deno.permissions.query({ name: "write" }); -if (status.state !== "granted") { - throw new Error("need write permission"); -} - -const log = await Deno.open("request.log", { write: true, append: true }); - -// revoke some permissions -await Deno.permissions.revoke({ name: "read" }); -await Deno.permissions.revoke({ name: "write" }); - -// use the log file -const encoder = new TextEncoder(); -await log.write(encoder.encode("hello\n")); - -// this will fail. -await Deno.remove("request.log"); -``` diff --git a/docs/runtime/compiler_apis.md b/docs/runtime/compiler_apis.md index d9a49a01c2..3424c2b5fd 100644 --- a/docs/runtime/compiler_apis.md +++ b/docs/runtime/compiler_apis.md @@ -1,7 +1,7 @@ -## Compiler API +## Compiler APIs -> This is an unstable Deno feature. Learn more about -> [unstable features](./stability.md). +> This API is unstable. Learn more about +> [unstable features](../runtime/stability.md). Deno supports runtime access to the built-in TypeScript compiler. There are three methods in the `Deno` namespace that provide this access. diff --git a/docs/runtime/permission_apis.md b/docs/runtime/permission_apis.md new file mode 100644 index 0000000000..a76f0c0d44 --- /dev/null +++ b/docs/runtime/permission_apis.md @@ -0,0 +1,189 @@ +## Permission APIs + +> This API is unstable. Learn more about +> [unstable features](../runtime/stability.md). + +Permissions are granted from the CLI when running the `deno` command. User code +will often assume its own set of required permissions, but there is no guarantee +during execution that the set of _granted_ permissions will align with this. + +In some cases, ensuring a fault-tolerant program requires a way to interact with +the permission system at runtime. + +### Permission descriptors + +On the CLI, read permission for `/foo/bar` is represented as +`--allow-read=/foo/bar`. In runtime JS, it is represented as the following: + +```ts +const desc = { name: "read", path: "/foo/bar" }; +``` + +Other examples: + +```ts +// Global write permission. +const desc1 = { name: "write" }; + +// Write permission to `$PWD/foo/bar`. +const desc2 = { name: "write", path: "foo/bar" }; + +// Global net permission. +const desc3 = { name: "net" }; + +// Net permission to 127.0.0.1:8000. +const desc4 = { name: "net", url: "127.0.0.1:8000" }; + +// High-resolution time permission. +const desc5 = { name: "hrtime" }; +``` + +### Query permissions + +Check, by descriptor, if a permission is granted or not. + +```ts +// deno run --unstable --allow-read=/foo main.ts + +const desc1 = { name: "read", path: "/foo" }; +console.log(await Deno.permissions.query(desc1)); +// PermissionStatus { state: "granted" } + +const desc2 = { name: "read", path: "/foo/bar" }; +console.log(await Deno.permissions.query(desc2)); +// PermissionStatus { state: "granted" } + +const desc3 = { name: "read", path: "/bar" }; +console.log(await Deno.permissions.query(desc3)); +// PermissionStatus { state: "prompt" } +``` + +### Permission states + +A permission state can be either "granted", "prompt" or "denied". Permissions +which have been granted from the CLI will query to `{ state: "granted" }`. Those +which have not been granted query to `{ state: "prompt" }` by default, while +`{ state: "denied" }` reserved for those which have been explicitly refused. +This will come up in [Request permissions](#request-permissions). + +### Permission strength + +The intuitive understanding behind the result of the second query in +[Query permissions](#query-permissions) is that read access was granted to +`/foo` and `/foo/bar` is within `/foo` so `/foo/bar` is allowed to be read. + +We can also say that `desc1` is +_[stronger than](https://www.w3.org/TR/permissions/#ref-for-permissiondescriptor-stronger-than)_ +`desc2`. This means that for any set of CLI-granted permissions: + +1. If `desc1` queries to `{ state: "granted" }` then so must `desc2`. +2. If `desc2` queries to `{ state: "denied" }` then so must `desc1`. + +More examples: + +```ts +const desc1 = { name: "write" }; +// is stronger than +const desc2 = { name: "write", path: "/foo" }; + +const desc3 = { name: "net" }; +// is stronger than +const desc4 = { name: "net", url: "127.0.0.1:8000" }; +``` + +### Request permissions + +Request an ungranted permission from the user via CLI prompt. + +```ts +// deno run --unstable main.ts + +const desc1 = { name: "read", path: "/foo" }; +const status1 = await Deno.permissions.request(desc1); +// ⚠️ Deno requests read access to "/foo". Grant? [g/d (g = grant, d = deny)] g +console.log(status1); +// PermissionStatus { state: "granted" } + +const desc2 = { name: "read", path: "/bar" }; +const status2 = await Deno.permissions.request(desc2); +// ⚠️ Deno requests read access to "/bar". Grant? [g/d (g = grant, d = deny)] d +console.log(status2); +// PermissionStatus { state: "denied" } +``` + +If the current permission state is "prompt", a prompt will appear on the user's +terminal asking them if they would like to grant the request. The request for +`desc1` was granted so its new status is returned and execution will continue as +if `--allow-read=/foo` was specified on the CLI. The request for `desc2` was +denied so its permission state is downgraded from "prompt" to "denied". + +If the current permission state is already either "granted" or "denied", the +request will behave like a query and just return the current status. This +prevents prompts both for already granted permissions and previously denied +requests. + +### Revoke permissions + +Downgrade a permission from "granted" to "prompt". + +```ts +// deno run --unstable --allow-read=/foo main.ts + +const desc = { name: "read", path: "/foo" }; +console.log(await Deno.permissions.revoke(desc)); +// PermissionStatus { state: "prompt" } +``` + +However, what happens when you try to revoke a permission which is _partial_ to +one granted on the CLI? + +```ts +// deno run --unstable --allow-read=/foo main.ts + +const desc = { name: "read", path: "/foo/bar" }; +console.log(await Deno.permissions.revoke(desc)); +// PermissionStatus { state: "granted" } +``` + +It was not revoked. + +To understand this behaviour, imagine that Deno stores an internal set of +_explicitly granted permission descriptors_. Specifying `--allow-read=/foo,/bar` +on the CLI initializes this set to: + +```ts +[ + { name: "read", path: "/foo" }, + { name: "read", path: "/bar" }, +]; +``` + +Granting a runtime request for `{ name: "write", path: "/foo" }` updates the set +to: + +```ts +[ + { name: "read", path: "/foo" }, + { name: "read", path: "/bar" }, + { name: "write", path: "/foo" }, +]; +``` + +Deno's permission revocation algorithm works by removing every element from this +set which the argument permission descriptor is _stronger than_. So to ensure +`desc` is not longer granted, pass an argument descriptor _stronger than_ +whichever _explicitly granted permission descriptor_ is _stronger than_ `desc`. + +```ts +// deno run --unstable --allow-read=/foo main.ts + +const desc = { name: "read", path: "/foo/bar" }; +console.log(await Deno.permissions.revoke(desc)); // Insufficient. +// PermissionStatus { state: "granted" } + +const strongDesc = { name: "read", path: "/foo" }; +await Deno.permissions.revoke(strongDesc); // Good. + +console.log(await Deno.permissions.query(desc)); +// PermissionStatus { state: "prompt" } +``` diff --git a/docs/toc.json b/docs/toc.json index 0e5cd01536..2df5be811a 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -19,6 +19,7 @@ "children": { "stability": "Stability", "program_lifecycle": "Program lifecycle", + "permission_apis": "Permission APIs", "compiler_apis": "Compiler APIs", "workers": "Workers" } @@ -75,7 +76,6 @@ "file_server": "File server", "tcp_echo": "TCP echo server", "subprocess": "Creating a subprocess", - "permissions": "Inspecting and revoking permissions", "os_signals": "OS Signals", "file_system_events": "File system events", "testing_if_main": "Checking if file is main" diff --git a/test_util/Cargo.toml b/test_util/Cargo.toml index b41103ed04..c1b4f51ec8 100644 --- a/test_util/Cargo.toml +++ b/test_util/Cargo.toml @@ -18,3 +18,6 @@ os_pipe = "0.9.2" regex = "1.3.9" tempfile = "3.1.0" warp = { version = "0.2.4", features = ["tls"] } + +[target.'cfg(unix)'.dependencies] +pty = "0.2.2" diff --git a/test_util/src/lib.rs b/test_util/src/lib.rs index bd75133b26..9b029c3556 100644 --- a/test_util/src/lib.rs +++ b/test_util/src/lib.rs @@ -7,6 +7,8 @@ extern crate lazy_static; use futures::future::{self, FutureExt}; use os_pipe::pipe; +#[cfg(unix)] +pub use pty; use regex::Regex; use std::env; use std::io::Read; @@ -767,7 +769,7 @@ impl CheckOutputIntegrationTest { } } -fn wildcard_match(pattern: &str, s: &str) -> bool { +pub fn wildcard_match(pattern: &str, s: &str) -> bool { pattern_match(pattern, s, "[WILDCARD]") } @@ -820,6 +822,39 @@ pub fn pattern_match(pattern: &str, s: &str, wildcard: &str) -> bool { t.1.is_empty() } +/// Kind of reflects `itest!()`. Note that the pty's output (which also contains +/// stdin content) is compared against the content of the `output` path. +#[cfg(unix)] +pub fn test_pty(args: &str, output_path: &str, input: &[u8]) { + use pty::fork::Fork; + + let tests_path = tests_path(); + let fork = Fork::from_ptmx().unwrap(); + if let Ok(mut master) = fork.is_parent() { + let mut output_actual = String::new(); + master.write_all(input).unwrap(); + master.read_to_string(&mut output_actual).unwrap(); + fork.wait().unwrap(); + + let output_expected = + std::fs::read_to_string(tests_path.join(output_path)).unwrap(); + if !wildcard_match(&output_expected, &output_actual) { + println!("OUTPUT\n{}\nOUTPUT", output_actual); + println!("EXPECTED\n{}\nEXPECTED", output_expected); + panic!("pattern match failed"); + } + } else { + deno_cmd() + .current_dir(tests_path) + .env("NO_COLOR", "1") + .args(args.split_whitespace()) + .spawn() + .unwrap() + .wait() + .unwrap(); + } +} + #[test] fn test_wildcard_match() { let fixtures = vec![