summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/js/dispatch.ts1
-rw-r--r--cli/js/lib.deno_runtime.d.ts9
-rw-r--r--cli/js/permissions.ts13
-rw-r--r--cli/ops/permissions.rs33
-rw-r--r--cli/permissions.rs401
5 files changed, 393 insertions, 64 deletions
diff --git a/cli/js/dispatch.ts b/cli/js/dispatch.ts
index d66467011..c2690ad32 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 1f01f1384..87da83e9a 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<PermissionStatus>;
+ /** 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<PermissionStatus>;
}
export const permissions: Permissions;
diff --git a/cli/js/permissions.ts b/cli/js/permissions.ts
index 16ea3e5c2..c3530e970 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<PermissionStatus> {
+ 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 823ab678b..0f40b642c 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<PinnedBuf>,
+) -> Result<JsonOp, ErrBox> {
+ 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 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);
+ }
}