From 31115067cd3156db1ead0dc0f11c78875840dc4f Mon Sep 17 00:00:00 2001 From: Yoshiya Hinosawa Date: Tue, 12 Nov 2019 00:33:29 +0900 Subject: [PATCH] Add permissions.request (#3296) --- cli/js/dispatch.ts | 1 + cli/js/lib.deno_runtime.d.ts | 9 + cli/js/permissions.ts | 13 ++ cli/ops/permissions.rs | 33 +++ cli/permissions.rs | 401 +++++++++++++++++++++++++++++------ 5 files changed, 393 insertions(+), 64 deletions(-) diff --git a/cli/js/dispatch.ts b/cli/js/dispatch.ts index d66467011e..c2690ad325 100644 --- a/cli/js/dispatch.ts +++ b/cli/js/dispatch.ts @@ -38,6 +38,7 @@ export let OP_GLOBAL_TIMER: number; export let OP_NOW: number; export let OP_QUERY_PERMISSION: number; export let OP_REVOKE_PERMISSION: number; +export let OP_REQUEST_PERMISSION: number; export let OP_CREATE_WORKER: number; export let OP_HOST_GET_WORKER_CLOSED: number; export let OP_HOST_POST_MESSAGE: number; diff --git a/cli/js/lib.deno_runtime.d.ts b/cli/js/lib.deno_runtime.d.ts index 1f01f13841..87da83e9a5 100644 --- a/cli/js/lib.deno_runtime.d.ts +++ b/cli/js/lib.deno_runtime.d.ts @@ -933,6 +933,15 @@ declare namespace Deno { * assert(status.state !== "granted") */ revoke(d: PermissionDescriptor): Promise; + /** Requests the permission. + * const status = await Deno.permissions.request({ name: "env" }); + * if (status.state === "granted") { + * console.log(Deno.homeDir()); + * } else { + * console.log("'env' permission is denied."); + * } + */ + request(desc: PermissionDescriptor): Promise; } export const permissions: Permissions; diff --git a/cli/js/permissions.ts b/cli/js/permissions.ts index 16ea3e5c27..c3530e9700 100644 --- a/cli/js/permissions.ts +++ b/cli/js/permissions.ts @@ -68,6 +68,19 @@ export class Permissions { const { state } = sendSync(dispatch.OP_REVOKE_PERMISSION, desc); return new PermissionStatus(state); } + + /** Requests the permission. + * const status = await Deno.permissions.request({ name: "env" }); + * if (status.state === "granted") { + * console.log(Deno.homeDir()); + * } else { + * console.log("'env' permission is denied."); + * } + */ + async request(desc: PermissionDescriptor): Promise { + const { state } = sendSync(dispatch.OP_REQUEST_PERMISSION, desc); + return new PermissionStatus(state); + } } export const permissions = new Permissions(); diff --git a/cli/ops/permissions.rs b/cli/ops/permissions.rs index 823ab678b7..0f40b642c2 100644 --- a/cli/ops/permissions.rs +++ b/cli/ops/permissions.rs @@ -1,5 +1,6 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. use super::dispatch_json::{Deserialize, JsonOp, Value}; +use crate::deno_error::type_error; use crate::ops::json_op; use crate::state::ThreadSafeState; use deno::*; @@ -13,6 +14,10 @@ pub fn init(i: &mut Isolate, s: &ThreadSafeState) { "revoke_permission", s.core_op(json_op(s.stateful_op(op_revoke_permission))), ); + i.register_op( + "request_permission", + s.core_op(json_op(s.stateful_op(op_request_permission))), + ); } #[derive(Deserialize)] @@ -58,3 +63,31 @@ pub fn op_revoke_permission( )?; Ok(JsonOp::Sync(json!({ "state": perm.to_string() }))) } + +pub fn op_request_permission( + state: &ThreadSafeState, + args: Value, + _zero_copy: Option, +) -> Result { + let args: PermissionArgs = serde_json::from_value(args)?; + let perm = match args.name.as_ref() { + "run" => Ok(state.permissions.request_run()), + "read" => Ok( + state + .permissions + .request_read(&args.path.as_ref().map(String::as_str)), + ), + "write" => Ok( + state + .permissions + .request_write(&args.path.as_ref().map(String::as_str)), + ), + "net" => state + .permissions + .request_net(&args.url.as_ref().map(String::as_str)), + "env" => Ok(state.permissions.request_env()), + "hrtime" => Ok(state.permissions.request_hrtime()), + n => Err(type_error(format!("No such permission name: {}", n))), + }?; + Ok(JsonOp::Sync(json!({ "state": perm.to_string() }))) +} diff --git a/cli/permissions.rs b/cli/permissions.rs index 1a470f5517..fe0a7d473c 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 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 { + 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 { + 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>, @@ -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); + } }