summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock2
-rw-r--r--cli/Cargo.toml3
-rw-r--r--cli/op_error.rs8
-rw-r--r--cli/ops/permissions.rs59
-rw-r--r--cli/ops/timers.rs14
-rw-r--r--cli/permissions.rs1284
-rw-r--r--cli/state.rs5
-rw-r--r--cli/tests/057_revoke_permissions.out11
-rw-r--r--cli/tests/057_revoke_permissions.ts36
-rw-r--r--cli/tests/061_permissions_request.ts6
-rw-r--r--cli/tests/061_permissions_request.ts.out3
-rw-r--r--cli/tests/062_permissions_request_global.ts6
-rw-r--r--cli/tests/062_permissions_request_global.ts.out3
-rw-r--r--cli/tests/063_permissions_revoke.ts6
-rw-r--r--cli/tests/063_permissions_revoke.ts.out3
-rw-r--r--cli/tests/064_permissions_revoke_global.ts6
-rw-r--r--cli/tests/064_permissions_revoke_global.ts.out3
-rw-r--r--cli/tests/integration_tests.rs40
-rw-r--r--docs/examples/permissions.md28
-rw-r--r--docs/runtime/compiler_apis.md6
-rw-r--r--docs/runtime/permission_apis.md189
-rw-r--r--docs/toc.json2
-rw-r--r--test_util/Cargo.toml3
-rw-r--r--test_util/src/lib.rs37
24 files changed, 1045 insertions, 718 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 7bb83cb03..cfbe630f1 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 c59906b26..c9371352f 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 263363122..32ad9c0f7 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 a4ee4120e..571ea5b21 100644
--- a/cli/ops/permissions.rs
+++ b/cli/ops/permissions.rs
@@ -35,12 +35,18 @@ pub fn op_query_permission(
) -> Result<JsonOp, OpError> {
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 044c5ea4a..21efa8cf9 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<JsonOp, OpError> {
- 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 d0ff31be5..7d9fa1d45 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<PermissionState, OpError> {
- 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<usize> 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<usize> for PermissionState {
impl From<bool> for PermissionState {
fn from(val: bool) -> Self {
if val {
- PermissionState::Allow
+ PermissionState::Granted
} else {
- PermissionState::Ask
+ PermissionState::Prompt
}
}
}
@@ -99,68 +75,49 @@ impl From<bool> 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;
-
-fn deserialize_permission_state<'de, D>(
- d: D,
-) -> Result<PermissionState, D::Error>
-where
- D: de::Deserializer<'de>,
-{
- impl<'de> de::Visitor<'de> for BoolPermVisitor {
- type Value = PermissionState;
+#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
+pub struct UnaryPermission<T: Eq + Hash> {
+ pub global_state: PermissionState,
+ pub granted_list: HashSet<T>,
+ pub denied_list: HashSet<T>,
+}
- fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
- formatter.write_str("a boolean value")
+impl<T: Eq + Hash> UnaryPermission<T> {
+ /// 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<E>(self, value: bool) -> Result<Self::Value, E>
- 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<PathBuf>,
- #[serde(deserialize_with = "deserialize_permission_state")]
- pub allow_write: PermissionState,
- pub write_allowlist: HashSet<PathBuf>,
- #[serde(deserialize_with = "deserialize_permission_state")]
- pub allow_net: PermissionState,
- pub net_allowlist: HashSet<String>,
- #[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<PathBuf>,
+ pub write: UnaryPermission<PathBuf>,
+ pub net: UnaryPermission<String>,
+ pub env: PermissionState,
+ pub run: PermissionState,
+ pub plugin: PermissionState,
+ pub hrtime: PermissionState,
}
fn resolve_fs_allowlist(allowlist: &[PathBuf]) -> HashSet<PathBuf> {
@@ -173,16 +130,25 @@ fn resolve_fs_allowlist(allowlist: &[PathBuf]) -> HashSet<PathBuf> {
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::<PathBuf> {
+ global_state: PermissionState::from(flags.allow_read),
+ granted_list: resolve_fs_allowlist(&flags.read_allowlist),
+ ..Default::default()
+ },
+ write: UnaryPermission::<PathBuf> {
+ global_state: PermissionState::from(flags.allow_write),
+ granted_list: resolve_fs_allowlist(&flags.write_allowlist),
+ ..Default::default()
+ },
+ net: UnaryPermission::<String> {
+ 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(&current_dir().unwrap()))
+ .query_read(&Some(&current_dir().unwrap()))
.check("", "")
{
Ok(_) => resolved_path.clone(),
@@ -206,79 +172,87 @@ 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")
- }
-
- 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_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;
}
- self.allow_read
- }
-
- 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(
- &format!("read access to \"{}\"", display_path.display()),
- "--allow-read",
- )
- }
-
- /// As `check_read()`, but permission error messages will anonymize the path
- /// by replacing it with the given `display`.
- pub fn check_read_blind(
- &self,
- path: &Path,
- display: &str,
- ) -> Result<(), OpError> {
- let resolved_path = resolve_from_cwd(path).unwrap();
- self
- .get_state_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;
+ 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;
}
- self.allow_write
+ PermissionState::Prompt
}
- 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(
- &format!("write access to \"{}\"", display_path.display()),
- "--allow-write",
- )
+ 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;
+ }
+ 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
}
- fn get_state_net(&self, host: &str, port: Option<u16>) -> PermissionState {
- if check_host_and_port_allowlist(host, port, &self.net_allowlist) {
- return PermissionState::Allow;
+ pub fn query_net(&self, host: &str, port: Option<u16>) -> PermissionState {
+ if self.net.global_state == PermissionState::Denied
+ || check_host_and_port_list(host, port, &self.net.denied_list)
+ {
+ return PermissionState::Denied;
}
- self.allow_net
+ if self.net.global_state == PermissionState::Granted
+ || check_host_and_port_list(host, port, &self.net.granted_list)
+ {
+ return PermissionState::Granted;
+ }
+ PermissionState::Prompt
}
- fn get_state_net_url(
+ pub fn query_net_url(
&self,
url: &Option<&str>,
) -> Result<PermissionState, OpError> {
if url.is_none() {
- return Ok(self.allow_net);
+ return Ok(self.net.global_state);
}
let url: &str = url.unwrap();
// If url is invalid, then throw a TypeError.
@@ -291,179 +265,363 @@ impl Permissions {
.to_owned(),
));
}
- Ok(self.get_state_net(
+ Ok(self.query_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(
- &format!("network access to \"{}:{}\"", hostname, port),
- "--allow-net",
- )
+ pub fn query_env(&self) -> PermissionState {
+ self.env
}
- pub fn check_net_url(&self, url: &url::Url) -> Result<(), OpError> {
- let host = url
- .host_str()
- .ok_or_else(|| OpError::uri_error("missing host".to_owned()))?;
- self
- .get_state_net(host, url.port_or_known_default())
- .check(&format!("network access to \"{}\"", url), "--allow-net")
+ pub fn query_run(&self) -> PermissionState {
+ self.run
}
- pub fn check_env(&self) -> Result<(), OpError> {
- self
- .allow_env
- .check("access to environment variables", "--allow-env")
+ pub fn query_plugin(&self) -> PermissionState {
+ self.plugin
}
- pub fn check_plugin(&self, path: &Path) -> Result<(), OpError> {
- let (_, display_path) = self.resolved_and_display_path(path);
- self.allow_plugin.check(
- &format!("access to open a plugin: {}", display_path.display()),
- "--allow-plugin",
- )
- }
-
- pub fn request_run(&mut self) -> PermissionState {
- self
- .allow_run
- .request("Deno requests to access to run a subprocess")
+ pub fn query_hrtime(&self) -> PermissionState {
+ self.hrtime
}
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;
+ 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;
+ }
}
- };
- 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()
- ),
- })
+ 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 {
- 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;
+ 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;
+ }
}
- };
- 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()
- ),
- })
+ 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<PermissionState, OpError> {
- 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)
+ 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 {
- self
- .allow_env
- .request("Deno requests to access to environment variables")
+ 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_hrtime(&mut self) -> PermissionState {
- self
- .allow_hrtime
- .request("Deno requests to access to high precision time")
+ 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 {
- self.allow_plugin.request("Deno requests to open plugins")
+ 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 get_permission_state(
- &self,
- name: &str,
+ 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>,
- path: &Option<&Path>,
) -> Result<PermissionState, OpError> {
- 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))),
+ 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.query_read(&Some(&resolved_path)).check(
+ &format!("read access to \"{}\"", display_path.display()),
+ "--allow-read",
+ )
+ }
+
+ /// As `check_read()`, but permission error messages will anonymize the path
+ /// by replacing it with the given `display`.
+ pub fn check_read_blind(
+ &self,
+ path: &Path,
+ display: &str,
+ ) -> Result<(), OpError> {
+ let resolved_path = resolve_from_cwd(path).unwrap();
+ self
+ .query_read(&Some(&resolved_path))
+ .check(&format!("read access to <{}>", display), "--allow-read")
+ }
+
+ pub fn check_write(&self, path: &Path) -> Result<(), OpError> {
+ let (resolved_path, display_path) = self.resolved_and_display_path(path);
+ self.query_write(&Some(&resolved_path)).check(
+ &format!("write access to \"{}\"", display_path.display()),
+ "--allow-write",
+ )
+ }
+
+ pub fn check_net(&self, hostname: &str, port: u16) -> Result<(), OpError> {
+ self.query_net(hostname, Some(port)).check(
+ &format!("network access to \"{}:{}\"", hostname, port),
+ "--allow-net",
+ )
+ }
+
+ pub fn check_net_url(&self, url: &url::Url) -> Result<(), OpError> {
+ let host = url
+ .host_str()
+ .ok_or_else(|| OpError::uri_error("missing host".to_owned()))?;
+ self
+ .query_net(host, url.port_or_known_default())
+ .check(&format!("network access to \"{}\"", url), "--allow-net")
+ }
+
+ pub fn check_env(&self) -> Result<(), OpError> {
+ self
+ .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.plugin.check(
+ &format!("access to open a plugin: {}", display_path.display()),
+ "--allow-plugin",
+ )
+ }
+
+ pub fn check_hrtime(&self) -> Result<(), OpError> {
+ self
+ .hrtime
+ .check("access to high precision time", "--allow-run")
}
#[allow(clippy::too_many_arguments)]
pub fn fork(
&self,
- allow_read: bool,
- read_allowlist: HashSet<PathBuf>,
- allow_write: bool,
- write_allowlist: HashSet<PathBuf>,
- allow_net: bool,
- net_allowlist: HashSet<String>,
- allow_env: bool,
- allow_run: bool,
- allow_plugin: bool,
- allow_hrtime: bool,
+ read: UnaryPermission<PathBuf>,
+ write: UnaryPermission<PathBuf>,
+ net: UnaryPermission<String>,
+ env: PermissionState,
+ run: PermissionState,
+ plugin: PermissionState,
+ hrtime: PermissionState,
) -> Result<Permissions, OpError> {
- 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<PathBuf>) -> bool {
- let mut path_buf = PathBuf::from(path);
- loop {
- if white_list.contains(&path_buf) {
+fn check_path_allowlist(path: &Path, allowlist: &HashSet<PathBuf>) -> bool {
+ for path_ in allowlist {
+ if path.starts_with(path_) {
return true;
}
- if !path_buf.pop() {
- break;
+ }
+ false
+}
+
+fn check_path_blocklist(path: &Path, blocklist: &HashSet<PathBuf>) -> bool {
+ for path_ in blocklist {
+ if path_.starts_with(path) {
+ return true;
}
}
false
}
-fn check_host_and_port_allowlist(
+fn check_host_and_port_list(
host: &str,
port: Option<u16>,
allowlist: &HashSet<String>,
@@ -709,247 +872,47 @@ 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 f485bd61f..5754e8c9d 100644
--- a/cli/state.rs
+++ b/cli/state.rs
@@ -561,6 +561,11 @@ impl State {
}
#[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 8ea569236..000000000
--- 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 de8deecb4..000000000
--- 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<void>] {
- const gen: () => Promise<void> = async function Granted(): Promise<void> {
- 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 000000000..8fdc2c590
--- /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 000000000..de058a9a3
--- /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 000000000..4ed98ff64
--- /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 000000000..69b5ee50d
--- /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 000000000..e61883693
--- /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 000000000..803893e9c
--- /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 000000000..efe74b828
--- /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 000000000..a2ea05cb7
--- /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 206664113..690b30798 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 78dfcc59d..000000000
--- 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 d9a49a01c..3424c2b5f 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 000000000..a76f0c0d4
--- /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 0e5cd0153..2df5be811 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 b41103ed0..c1b4f51ec 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 bd75133b2..9b029c355 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![