summaryrefslogtreecommitdiff
path: root/cli/permissions.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/permissions.rs')
-rw-r--r--cli/permissions.rs1284
1 files changed, 694 insertions, 590 deletions
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);
+ };
}
}