summaryrefslogtreecommitdiff
path: root/cli/permissions.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/permissions.rs')
-rw-r--r--cli/permissions.rs401
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);
+ }
}