diff options
Diffstat (limited to 'cli/permissions.rs')
-rw-r--r-- | cli/permissions.rs | 401 |
1 files changed, 337 insertions, 64 deletions
diff --git a/cli/permissions.rs b/cli/permissions.rs index 1a470f551..fe0a7d473 100644 --- a/cli/permissions.rs +++ b/cli/permissions.rs @@ -2,11 +2,17 @@ use crate::deno_error::{permission_denied_msg, type_error}; use crate::flags::DenoFlags; use ansi_term::Style; +#[cfg(not(test))] +use atty; use deno::ErrBox; use log; use std::collections::HashSet; use std::fmt; +#[cfg(not(test))] +use std::io; use std::path::PathBuf; +#[cfg(test)] +use std::sync::atomic::AtomicBool; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use url::Url; @@ -14,13 +20,24 @@ use url::Url; const PERMISSION_EMOJI: &str = "⚠️"; /// Tri-state value for storing permission state -#[derive(PartialEq)] +#[derive(PartialEq, Debug)] pub enum PermissionAccessorState { Allow = 0, Ask = 1, Deny = 2, } +impl PermissionAccessorState { + /// Checks the permission state and returns the result. + pub fn check(self, msg: &str, err_msg: &str) -> Result<(), ErrBox> { + if self == PermissionAccessorState::Allow { + log_perm_access(msg); + return Ok(()); + } + Err(permission_denied_msg(err_msg.to_string())) + } +} + impl From<usize> for PermissionAccessorState { fn from(val: usize) -> Self { match val { @@ -64,27 +81,30 @@ impl PermissionAccessor { } } - pub fn is_allow(&self) -> bool { - match self.get_state() { - PermissionAccessorState::Allow => true, - _ => false, - } - } - /// If the state is "Allow" walk it back to the default "Ask" /// Don't do anything if state is "Deny" pub fn revoke(&self) { if self.is_allow() { - self.ask(); + self.set_state(PermissionAccessorState::Ask) } } - pub fn allow(&self) { - self.set_state(PermissionAccessorState::Allow) + /// Requests the permission. + pub fn request(&self, msg: &str) -> PermissionAccessorState { + let state = self.get_state(); + if state != PermissionAccessorState::Ask { + return state; + } + self.set_state(if permission_prompt(msg) { + PermissionAccessorState::Allow + } else { + PermissionAccessorState::Deny + }); + self.get_state() } - pub fn ask(&self) { - self.set_state(PermissionAccessorState::Ask) + pub fn is_allow(&self) -> bool { + self.get_state() == PermissionAccessorState::Allow } #[inline] @@ -141,23 +161,8 @@ impl DenoPermissions { } } - /** Checks the permission state and returns the result. */ - fn check_permission_state( - &self, - state: PermissionAccessorState, - msg: &str, - err_msg: &str, - ) -> Result<(), ErrBox> { - if state == PermissionAccessorState::Allow { - self.log_perm_access(msg); - return Ok(()); - } - Err(permission_denied_msg(err_msg.to_string())) - } - pub fn check_run(&self) -> Result<(), ErrBox> { - self.check_permission_state( - self.allow_run.get_state(), + self.allow_run.get_state().check( "access to run a subprocess", "run again with the --allow-run flag", ) @@ -171,8 +176,7 @@ impl DenoPermissions { } pub fn check_read(&self, filename: &str) -> Result<(), ErrBox> { - self.check_permission_state( - self.get_state_read(&Some(filename)), + self.get_state_read(&Some(filename)).check( &format!("read access to \"{}\"", filename), "run again with the --allow-read flag", ) @@ -189,8 +193,7 @@ impl DenoPermissions { } pub fn check_write(&self, filename: &str) -> Result<(), ErrBox> { - self.check_permission_state( - self.get_state_write(&Some(filename)), + self.get_state_write(&Some(filename)).check( &format!("write access to \"{}\"", filename), "run again with the --allow-write flag", ) @@ -207,39 +210,94 @@ impl DenoPermissions { self.allow_net.get_state() } + fn get_state_net_url( + &self, + url: &Option<&str>, + ) -> Result<PermissionAccessorState, ErrBox> { + if url.is_none() { + return Ok(self.allow_net.get_state()); + } + let url: &str = url.unwrap(); + // If url is invalid, then throw a TypeError. + let parsed = Url::parse(url) + .map_err(|_| type_error(format!("Invalid url: {}", url)))?; + Ok( + self.get_state_net(&format!("{}", parsed.host().unwrap()), parsed.port()), + ) + } + pub fn check_net(&self, hostname: &str, port: u16) -> Result<(), ErrBox> { - self.check_permission_state( - self.get_state_net(hostname, Some(port)), + self.get_state_net(hostname, Some(port)).check( &format!("network access to \"{}:{}\"", hostname, port), "run again with the --allow-net flag", ) } pub fn check_net_url(&self, url: &url::Url) -> Result<(), ErrBox> { - self.check_permission_state( - self.get_state_net(&format!("{}", url.host().unwrap()), url.port()), - &format!("network access to \"{}\"", url), - "run again with the --allow-net flag", - ) + self + .get_state_net(&format!("{}", url.host().unwrap()), url.port()) + .check( + &format!("network access to \"{}\"", url), + "run again with the --allow-net flag", + ) } pub fn check_env(&self) -> Result<(), ErrBox> { - self.check_permission_state( - self.allow_env.get_state(), + self.allow_env.get_state().check( "access to environment variables", "run again with the --allow-env flag", ) } - fn log_perm_access(&self, message: &str) { - if log_enabled!(log::Level::Info) { - eprintln!( - "{}", - Style::new() - .bold() - .paint(format!("{}️ Granted {}", PERMISSION_EMOJI, message)) - ); - } + pub fn request_run(&self) -> PermissionAccessorState { + self + .allow_run + .request("Deno requests to access to run a subprocess.") + } + + pub fn request_read(&self, path: &Option<&str>) -> PermissionAccessorState { + if check_path_white_list(path, &self.read_whitelist) { + return PermissionAccessorState::Allow; + }; + self.allow_write.request(&match path { + None => "Deno requests read access.".to_string(), + Some(path) => format!("Deno requests read access to \"{}\".", path), + }) + } + + pub fn request_write(&self, path: &Option<&str>) -> PermissionAccessorState { + if check_path_white_list(path, &self.write_whitelist) { + return PermissionAccessorState::Allow; + }; + self.allow_write.request(&match path { + None => "Deno requests write access.".to_string(), + Some(path) => format!("Deno requests write access to \"{}\".", path), + }) + } + + pub fn request_net( + &self, + url: &Option<&str>, + ) -> Result<PermissionAccessorState, ErrBox> { + if self.get_state_net_url(url)? == PermissionAccessorState::Ask { + return Ok(self.allow_run.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(&self) -> PermissionAccessorState { + self + .allow_env + .request("Deno requests to access to environment variables.") + } + + pub fn request_hrtime(&self) -> PermissionAccessorState { + self + .allow_hrtime + .request("Deno requests to access to high precision time.") } pub fn get_permission_state( @@ -252,19 +310,7 @@ impl DenoPermissions { "run" => Ok(self.allow_run.get_state()), "read" => Ok(self.get_state_read(path)), "write" => Ok(self.get_state_write(path)), - "net" => { - // If url is not given, then just check the entire net permission - if url.is_none() { - return Ok(self.allow_net.get_state()); - } - let url: &str = url.unwrap(); - // If url is invalid, then throw a TypeError. - let parsed = Url::parse(url) - .map_err(|_| type_error(format!("Invalid url: {}", url)))?; - let state = self - .get_state_net(&format!("{}", parsed.host().unwrap()), parsed.port()); - Ok(state) - } + "net" => self.get_state_net_url(url), "env" => Ok(self.allow_env.get_state()), "hrtime" => Ok(self.allow_hrtime.get_state()), n => Err(type_error(format!("No such permission name: {}", n))), @@ -272,6 +318,66 @@ impl DenoPermissions { } } +/// Shows the permission prompt and returns the answer according to the user input. +/// This loops until the user gives the proper input. +#[cfg(not(test))] +fn permission_prompt(message: &str) -> bool { + if !atty::is(atty::Stream::Stdin) || !atty::is(atty::Stream::Stderr) { + return false; + }; + let msg = format!( + "️{} {}. Grant? [g/d (g = grant, d = deny)] ", + PERMISSION_EMOJI, message + ); + // print to stderr so that if deno is > to a file this is still displayed. + eprint!("{}", Style::new().bold().paint(msg)); + loop { + let mut input = String::new(); + let stdin = io::stdin(); + let result = stdin.read_line(&mut input); + if result.is_err() { + return false; + }; + let ch = input.chars().next().unwrap(); + match ch.to_ascii_lowercase() { + 'g' => return true, + 'd' => return false, + _ => { + // If we don't get a recognized option try again. + let msg_again = + format!("Unrecognized option '{}' [g/d (g = grant, d = deny)] ", ch); + eprint!("{}", Style::new().bold().paint(msg_again)); + } + }; + } +} + +#[cfg(test)] +static STUB_PROMPT_VALUE: AtomicBool = AtomicBool::new(true); + +#[cfg(test)] +fn set_prompt_result(value: bool) { + STUB_PROMPT_VALUE.store(value, Ordering::SeqCst); +} + +// When testing, permission prompt returns the value of STUB_PROMPT_VALUE +// which we set from the test functions. +#[cfg(test)] +fn permission_prompt(_message: &str) -> bool { + STUB_PROMPT_VALUE.load(Ordering::SeqCst) +} + +fn log_perm_access(message: &str) { + if log_enabled!(log::Level::Info) { + eprintln!( + "{}", + Style::new() + .bold() + .paint(format!("{}️ Granted {}", PERMISSION_EMOJI, message)) + ); + } +} + fn check_path_white_list( filename: &Option<&str>, white_list: &Arc<HashSet<String>>, @@ -435,4 +541,171 @@ mod tests { assert_eq!(*is_ok, perms.check_net(host, *port).is_ok()); } } + + #[test] + fn test_permissions_request_run() { + let perms0 = DenoPermissions::from_flags(&DenoFlags { + ..Default::default() + }); + set_prompt_result(true); + assert_eq!(perms0.request_run(), PermissionAccessorState::Allow); + + let perms1 = DenoPermissions::from_flags(&DenoFlags { + ..Default::default() + }); + set_prompt_result(false); + assert_eq!(perms1.request_run(), PermissionAccessorState::Deny); + } + + #[test] + fn test_permissions_request_read() { + let whitelist = svec!["/foo/bar"]; + let perms0 = DenoPermissions::from_flags(&DenoFlags { + read_whitelist: whitelist.clone(), + ..Default::default() + }); + set_prompt_result(false); + // If the whitelist contains the path, then the result is `allow` + // regardless of prompt result + assert_eq!( + perms0.request_read(&Some("/foo/bar")), + PermissionAccessorState::Allow + ); + + let perms1 = DenoPermissions::from_flags(&DenoFlags { + read_whitelist: whitelist.clone(), + ..Default::default() + }); + set_prompt_result(true); + assert_eq!( + perms1.request_read(&Some("/foo/baz")), + PermissionAccessorState::Allow + ); + + let perms2 = DenoPermissions::from_flags(&DenoFlags { + read_whitelist: whitelist.clone(), + ..Default::default() + }); + set_prompt_result(false); + assert_eq!( + perms2.request_read(&Some("/foo/baz")), + PermissionAccessorState::Deny + ); + } + + #[test] + fn test_permissions_request_write() { + let whitelist = svec!["/foo/bar"]; + let perms0 = DenoPermissions::from_flags(&DenoFlags { + write_whitelist: whitelist.clone(), + ..Default::default() + }); + set_prompt_result(false); + // If the whitelist contains the path, then the result is `allow` + // regardless of prompt result + assert_eq!( + perms0.request_write(&Some("/foo/bar")), + PermissionAccessorState::Allow + ); + + let perms1 = DenoPermissions::from_flags(&DenoFlags { + write_whitelist: whitelist.clone(), + ..Default::default() + }); + set_prompt_result(true); + assert_eq!( + perms1.request_write(&Some("/foo/baz")), + PermissionAccessorState::Allow + ); + + let perms2 = DenoPermissions::from_flags(&DenoFlags { + write_whitelist: whitelist.clone(), + ..Default::default() + }); + set_prompt_result(false); + assert_eq!( + perms2.request_write(&Some("/foo/baz")), + PermissionAccessorState::Deny + ); + } + + #[test] + fn test_permission_request_net() { + let whitelist = svec!["localhost:8080"]; + + let perms0 = DenoPermissions::from_flags(&DenoFlags { + net_whitelist: whitelist.clone(), + ..Default::default() + }); + set_prompt_result(false); + // If the url matches the whitelist item, then the result is `allow` + // regardless of prompt result + assert_eq!( + perms0 + .request_net(&Some("http://localhost:8080/")) + .expect("Testing expect"), + PermissionAccessorState::Allow + ); + + let perms1 = DenoPermissions::from_flags(&DenoFlags { + net_whitelist: whitelist.clone(), + ..Default::default() + }); + set_prompt_result(true); + assert_eq!( + perms1 + .request_net(&Some("http://deno.land/")) + .expect("Testing expect"), + PermissionAccessorState::Allow + ); + + let perms2 = DenoPermissions::from_flags(&DenoFlags { + net_whitelist: whitelist.clone(), + ..Default::default() + }); + set_prompt_result(false); + assert_eq!( + perms2 + .request_net(&Some("http://deno.land/")) + .expect("Testing expect"), + PermissionAccessorState::Deny + ); + + let perms3 = DenoPermissions::from_flags(&DenoFlags { + net_whitelist: whitelist.clone(), + ..Default::default() + }); + set_prompt_result(true); + assert!(perms3.request_net(&Some(":")).is_err()); + } + + #[test] + fn test_permissions_request_env() { + let perms0 = DenoPermissions::from_flags(&DenoFlags { + ..Default::default() + }); + set_prompt_result(true); + assert_eq!(perms0.request_env(), PermissionAccessorState::Allow); + + let perms1 = DenoPermissions::from_flags(&DenoFlags { + ..Default::default() + }); + set_prompt_result(false); + assert_eq!(perms1.request_env(), PermissionAccessorState::Deny); + } + + #[test] + fn test_permissions_request_hrtime() { + let perms0 = DenoPermissions::from_flags(&DenoFlags { + ..Default::default() + }); + set_prompt_result(true); + assert_eq!(perms0.request_hrtime(), PermissionAccessorState::Allow); + + let perms1 = DenoPermissions::from_flags(&DenoFlags { + ..Default::default() + }); + set_prompt_result(false); + assert_eq!(perms1.request_hrtime(), PermissionAccessorState::Deny); + } } |